<?php

declare(strict_types=1);

namespace CloudXM\NFe\Services;

use CloudXM\NFe\Config\Configuration;
use CloudXM\NFe\DTOs\IssueInvoiceRequest;
use CloudXM\NFe\DTOs\CancelInvoiceRequest;
use CloudXM\NFe\DTOs\InvoiceResource;
use CloudXM\NFe\DTOs\TenantConfigResource;
use CloudXM\SDK\Support\HasLogger;

/**
 * Robust API Client for NFE Server
 *
 * Handles all API communication with retry logic, error handling, and logging
 */
class ApiClient
{
    use HasLogger;

    /**
     * Configuration instance
     */
    private Configuration $config;

    /**
     * Maximum number of retry attempts
     */
    private int $maxRetries = 3;

    /**
     * Delay between retries in milliseconds
     */
    private int $retryDelay = 1000;

    /**
     * Request timeout in seconds
     */
    private int $timeout = 30;

    /**
     * Return a clone of this client with a different timeout.
     * Useful for non-critical admin UI calls that should fail fast.
     */
    public function withTimeout(int $seconds): static
    {
        $clone = clone $this;
        $clone->timeout = $seconds;
        $clone->maxRetries = 1;
        return $clone;
    }

    /**
     * Constructor
     */
    public function __construct(Configuration $config)
    {
        $this->config = $config;
    }

    /**
     * Get API base URL
     *
     * @return string
     */
    public function getApiBaseUrl(): string
    {
        return $this->config->getApiBaseUrl();
    }

    /**
     * Get API token for current environment
     *
     * @return string
     */
    public function getApiToken(): string
    {
        return $this->config->getApiToken();
    }

    /**
     * Make a POST request
     *
     * @param string $endpoint API endpoint
     * @param array $data Request data
     * @return array
     */
    public function post(string $endpoint, array $data = []): array
    {
        return $this->request('POST', $endpoint, $data);
    }

    /**
     * Make a GET request
     *
     * @param string $endpoint API endpoint
     * @param array $params Query parameters
     * @return array
     */
    public function get(string $endpoint, array $params = []): array
    {
        return $this->request('GET', $endpoint, $params);
    }

    /**
     * Make a DELETE request
     *
     * @param string $endpoint API endpoint
     * @return array
     */
    public function delete(string $endpoint): array
    {
        return $this->request('DELETE', $endpoint);
    }

    /**
     * Make an HTTP request with retry logic
     *
     * @param string $method HTTP method
     * @param string $endpoint API endpoint
     * @param array $data Request data
     * @return array
     */
    private function request(string $method, string $endpoint, array $data = []): array
    {
        $baseUrl = $this->getApiBaseUrl();
        $token = $this->getApiToken();
        $url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');

        $args = [
            'method' => $method,
            'timeout' => $this->timeout,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ],
        ];

        if ($method === 'GET' && !empty($data)) {
            $url = add_query_arg($data, $url);
        } elseif ($method === 'POST' || $method === 'DELETE') {
            $args['body'] = wp_json_encode($data);
        }

        $attempt = 0;
        $lastError = null;

        while ($attempt < $this->maxRetries) {
            $response = wp_remote_request($url, $args);

            if (is_wp_error($response)) {
                $lastError = $response->get_error_message();
                
                if ($attempt < $this->maxRetries - 1 && 
                    $this->shouldRetry($response)) {
                    $attempt++;
                    usleep($this->retryDelay * 1000);
                    continue;
                }
                
                break;
            }

            $statusCode = wp_remote_retrieve_response_code($response);
            if ($statusCode >= 200 && $statusCode < 300) {
                $body = wp_remote_retrieve_body($response);
                $result = json_decode($body, true);
                
                if (json_last_error() === JSON_ERROR_NONE) {
                    return $result['data'] ?? $result;
                }
            }

            $body = wp_remote_retrieve_body($response);
            $parsed = json_decode($body, true);
            $errorMsg = $this->extractErrorMessage($parsed, $statusCode, $body);

            if ($statusCode >= 400 && $statusCode < 500) {
                return ['status' => $statusCode, 'body' => ['error' => $errorMsg]];
            }

            $lastError = $errorMsg;
            $attempt++;
            usleep($this->retryDelay * 1000);
        }

        return ['status' => 0, 'body' => ['error' => $lastError ?? 'Request failed']];
    }

    /**
     * Extract a detailed, human-readable error message from an API response.
     *
     * Handles various server response shapes:
     *  - {"message": "..."} (Laravel default)
     *  - {"error": "..."}
     *  - {"message": "...", "errors": {"field": ["..."], ...}}  (Laravel 422)
     *  - {"message": "...", "details": "..."}
     *  - raw non-JSON body
     */
    private function extractErrorMessage(?array $parsed, int $statusCode, string $rawBody): string
    {
        if ($parsed === null) {
            $trimmed = trim($rawBody);
            return $trimmed !== '' ? "HTTP $statusCode: {$trimmed}" : "HTTP $statusCode";
        }

        // Handle nested error structures from the NFe API:
        // { data: { message: "...", data: { erros: [{ Codigo, Descricao }] } } }
        // { structured_errors: [{ code, message }] }
        // { data: { message: "..." } }
        $errorSource = $parsed;

        // Unwrap nested data.data if present
        if (isset($parsed['data']['data'])) {
            $errorSource = $parsed['data'];
        } elseif (isset($parsed['data']) && is_array($parsed['data']) && isset($parsed['data']['message'])) {
            $errorSource = $parsed['data'];
        }

        $message = $errorSource['message'] ?? $parsed['message'] ?? $parsed['error'] ?? '';

        // Ensure message is a string (server may return arrays for validation errors)
        if (is_array($message)) {
            $message = implode(' | ', array_map(
                fn($v) => is_array($v) ? implode('; ', $v) : (string) $v,
                $message
            ));
        } else {
            $message = (string) $message;
        }

        // Extract structured_errors (code + message pairs)
        $structuredErrors = $errorSource['structured_errors'] ?? $parsed['structured_errors'] ?? null;
        if (is_array($structuredErrors) && !empty($structuredErrors)) {
            $parts = array_map(
                fn($e) => '[' . ($e['code'] ?? ($e['Codigo'] ?? '?')) . '] ' . ($e['message'] ?? ($e['Descricao'] ?? '')),
                $structuredErrors
            );
            $detail = implode(' | ', $parts);
            $message = $message !== '' ? $message . ' — ' . $detail : $detail;
        }

        // Extract erros array from nested data
        $nestedErrors = $errorSource['data']['erros'] ?? $parsed['erros'] ?? null;
        if (is_array($nestedErrors) && !empty($nestedErrors)) {
            $parts = array_map(
                fn($e) => '[' . ($e['Codigo'] ?? ($e['code'] ?? '?')) . '] ' . ($e['Descricao'] ?? ($e['message'] ?? '')),
                $nestedErrors
            );
            $detail = implode(' | ', $parts);
            $message = $message !== '' ? $message . ' — ' . $detail : $detail;
        }

        // Extract standard validation errors
        if (isset($parsed['errors']) && is_array($parsed['errors'])) {
            $detailParts = [];
            foreach ($parsed['errors'] as $field => $msgs) {
                if (is_array($msgs)) {
                    $detailParts[] = $field . ': ' . implode('; ', $msgs);
                } else {
                    $detailParts[] = (string) $field . ': ' . (string) $msgs;
                }
            }
            if (!empty($detailParts)) {
                $detail = implode(' | ', $detailParts);
                $message = $message !== '' ? $message . ' — ' . $detail : $detail;
            }
        }

        if (isset($parsed['details']) && is_string($parsed['details']) && $parsed['details'] !== '') {
            $message = $message !== '' ? $message . ' — ' . $parsed['details'] : $parsed['details'];
        }

        return $message !== '' ? $message : "HTTP $statusCode";
    }

    /**
     * Check if request should be retried
     *
     * @param array|WP_Error $response
     * @return bool
     */
    private function shouldRetry($response): bool
    {
        $code = wp_remote_retrieve_response_code($response);
        $message = $response->get_error_message();

        // Retry on timeout or connection errors
        if ($code === 0 || $code === 52 || strpos($message, 'timed out') !== false) {
            return true;
        }

        // Don't retry on client errors (4xx, 5xx)
        if ($code >= 400 && $code < 600) {
            return false;
        }

        // Don't retry on server errors (5xx)
        if ($code >= 500 && $code < 600) {
            return true;
        }

        return false;
    }

    /**
     * Make a POST request and return raw status + decoded body.
     * Used when the caller needs to distinguish 200 vs 202.
     * Protected (not private) to allow test doubles.
     *
     * @param string $endpoint
     * @param array  $data
     * @return array{status: int, body: array}
     */
    protected function postRaw(string $endpoint, array $data = []): array
    {
        $baseUrl = $this->getApiBaseUrl();
        $token   = $this->getApiToken();
        $url     = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');

        $args = [
            'method'  => 'POST',
            'timeout' => $this->timeout,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type'  => 'application/json',
                'Accept'        => 'application/json',
            ],
            'body' => wp_json_encode($data),
        ];

        $attempt   = 0;
        $lastError = null;

        while ($attempt < $this->maxRetries) {
            $response = wp_remote_request($url, $args);

            if (is_wp_error($response)) {
                $lastError = $response->get_error_message();
                if ($attempt < $this->maxRetries - 1 && $this->shouldRetry($response)) {
                    $attempt++;
                    usleep($this->retryDelay * 1000);
                    continue;
                }
                break;
            }

            $statusCode = wp_remote_retrieve_response_code($response);

            if ($statusCode >= 200 && $statusCode < 300) {
                $body   = wp_remote_retrieve_body($response);
                $parsed = json_decode($body, true);
                return [
                    'status' => $statusCode,
                    'body'   => (json_last_error() === JSON_ERROR_NONE ? ($parsed['data'] ?? $parsed) : []),
                ];
            }

            $body   = wp_remote_retrieve_body($response);
            $parsed = json_decode($body, true);
            $errorMsg = $this->extractErrorMessage($parsed, $statusCode, $body);

            if ($statusCode >= 400 && $statusCode < 500) {
                return ['status' => $statusCode, 'body' => ['error' => $errorMsg]];
            }

            // For 5xx errors, try to extract a meaningful error from the response
            // rather than retrying and losing the body
            if ($statusCode >= 500) {
                // If we got a JSON response with an error, return it immediately
                if ($parsed !== null && isset($parsed['message'])) {
                    return ['status' => $statusCode, 'body' => ['error' => $errorMsg]];
                }
                // If HTML or unparseable, retry once
                $lastError = $errorMsg;
                $attempt++;
                usleep($this->retryDelay * 1000);
                continue;
            }

            $lastError = $errorMsg;
            $attempt++;
            usleep($this->retryDelay * 1000);
        }

        return ['status' => 0, 'body' => ['error' => $lastError ?? 'Request failed']];
    }

    /**
     * Emit NFe invoice (v1 - DEPRECATED)
     *
     * @deprecated Use emitInvoiceV2() instead
     * @param array $orderData Order data
     * @return array
     */
    public function emitInvoice(array $orderData): array
    {
        return $this->post('invoices', $orderData);
    }

    /**
     * Emit NFe invoice (v2 API)
     *
     * @param IssueInvoiceRequest $request Invoice request DTO
     * @return InvoiceResource
     * @throws \InvalidArgumentException if validation fails
     * @throws \RuntimeException if API call fails
     */
    public function emitInvoiceV2(IssueInvoiceRequest $request): InvoiceResource
    {
        // Validate request before sending
        $errors = $request->validate();
        if (!empty($errors)) {
            $this->log()->error('nfe.api.validation_failed', [
                'operation' => 'emit_invoice',
                'errors' => $errors
            ]);
            throw new \InvalidArgumentException('Invoice request validation failed: ' . json_encode($errors));
        }

        $this->log()->info('nfe.api.request_sent', [
            'method'       => 'POST',
            'endpoint'     => '/v1/invoices',
            'order_number' => $request->order_number,
        ]);

        // Use postRaw so we can distinguish 200 (sync) from 202 (async/scheduled)
        $raw = $this->postRaw('/v1/invoices', $request->jsonSerialize());

        if (!empty($raw['body']['error']) || $raw['status'] === 0) {
            $this->log()->error('nfe.api.request_failed', [
                'operation' => 'emit_invoice',
                'error'     => $raw['body']['error'] ?? 'unknown',
            ]);
            throw new \RuntimeException('Failed to emit invoice: ' . ($raw['body']['error'] ?? 'unknown'));
        }

        $this->log()->info('nfe.api.response_received', [
            'operation'   => 'emit_invoice',
            'http_status' => $raw['status'],
            'status'      => $raw['body']['status'] ?? 'unknown',
            'dps_number'  => $raw['body']['dps_number'] ?? null,
            'has_data_key' => isset($raw['body']['data']),
        ]);

        // 202 = async/scheduled accepted — server will call back via webhook
        if ($raw['status'] === 202) {
            $body = $raw['body'];
            // Build a minimal InvoiceResource so callers can store the invoice ID
            return InvoiceResource::fromArray(array_merge(
                ['status' => 'async_pending'],
                $body
            ));
        }

        // Parse 200/201 response into DTO
        try {
            return InvoiceResource::fromArray($raw['body']['data'] ?? $raw['body']);
        } catch (\Exception $e) {
            $this->log()->error('nfe.api.parsing_failed', [
                'operation' => 'emit_invoice',
                'error'     => $e->getMessage(),
                'response'  => $raw['body'],
            ]);
            throw new \RuntimeException('Failed to parse invoice response: ' . $e->getMessage());
        }
    }

    /**
     * Cancel NFe invoice (v1 - DEPRECATED)
     *
     * @deprecated Use cancelInvoiceV2() instead
     * @param string $invoiceId Invoice ID
     * @param string $reason Cancellation reason
     * @return array
     */
    public function cancelInvoice(string $invoiceId, string $reason): array
    {
        return $this->post("invoices/{$invoiceId}/cancel", [
            'reason' => $reason,
        ]);
    }

    /**
     * Cancel NFe invoice (v2 API)
     *
     * @param string $invoiceId Invoice ID
     * @param CancelInvoiceRequest $request Cancel request DTO
     * @return InvoiceResource
     * @throws \InvalidArgumentException if validation fails
     * @throws \RuntimeException if API call fails
     */
    public function cancelInvoiceV2(string $invoiceId, CancelInvoiceRequest $request): InvoiceResource
    {
        // Validate request before sending
        $errors = $request->validate();
        if (!empty($errors)) {
            $this->log()->error('nfe.api.validation_failed', [
                'operation' => 'cancel_invoice',
                'invoice_id' => $invoiceId,
                'errors' => $errors
            ]);
            throw new \InvalidArgumentException('Cancel request validation failed: ' . json_encode($errors));
        }

        $this->log()->info('nfe.api.request_sent', [
            'method' => 'POST',
            'endpoint' => "/v1/invoices/{$invoiceId}/cancel",
            'invoice_id' => $invoiceId
        ]);

        // Send request to v2 endpoint
        $response = $this->post("/v1/invoices/{$invoiceId}/cancel", $request->jsonSerialize());

        if (!empty($response['error'])) {
            $this->log()->error('nfe.api.request_failed', [
                'operation' => 'cancel_invoice',
                'invoice_id' => $invoiceId,
                'error' => $response['error']
            ]);
            throw new \RuntimeException('Failed to cancel invoice: ' . $response['error']);
        }

        $this->log()->info('nfe.api.response_received', [
            'operation' => 'cancel_invoice',
            'invoice_id' => $invoiceId,
            'status' => $response['status'] ?? 'unknown'
        ]);

        // Parse response into DTO
        try {
            return InvoiceResource::fromArray($response['data'] ?? $response);
        } catch (\Exception $e) {
            $this->log()->error('nfe.api.parsing_failed', [
                'operation' => 'cancel_invoice',
                'invoice_id' => $invoiceId,
                'error' => $e->getMessage(),
                'response' => $response
            ]);
            throw new \RuntimeException('Failed to parse cancel response: ' . $e->getMessage());
        }
    }

    /**
     * Get invoice status (v1 - DEPRECATED)
     *
     * @deprecated Use getInvoiceV2() instead
     * @param string $invoiceId Invoice ID
     * @return array
     */
    public function getInvoiceStatus(string $invoiceId): array
    {
        return $this->get("invoices/{$invoiceId}");
    }

    /**
     * Get invoice (v2 API)
     *
     * @param string $invoiceId Invoice ID
     * @return InvoiceResource
     * @throws \RuntimeException if API call fails
     */
    public function getInvoiceV2(string $invoiceId): InvoiceResource
    {
        $this->log()->info('nfe.api.request_sent', [
            'method' => 'GET',
            'endpoint' => "/v1/invoices/{$invoiceId}",
            'invoice_id' => $invoiceId
        ]);

        // Send request to v2 endpoint
        $response = $this->get("/v1/invoices/{$invoiceId}");

        if (!empty($response['error'])) {
            $this->log()->error('nfe.api.request_failed', [
                'operation' => 'get_invoice',
                'invoice_id' => $invoiceId,
                'error' => $response['error']
            ]);
            throw new \RuntimeException('Failed to get invoice: ' . $response['error']);
        }

        $this->log()->info('nfe.api.response_received', [
            'operation' => 'get_invoice',
            'invoice_id' => $invoiceId,
            'status' => $response['status'] ?? 'unknown'
        ]);

        // Parse response into DTO
        try {
            return InvoiceResource::fromArray($response['data'] ?? $response);
        } catch (\Exception $e) {
            $this->log()->error('nfe.api.parsing_failed', [
                'operation' => 'get_invoice',
                'invoice_id' => $invoiceId,
                'error' => $e->getMessage(),
                'response' => $response
            ]);
            throw new \RuntimeException('Failed to parse invoice response: ' . $e->getMessage());
        }
    }

    /**
     * Issue scheduled invoice immediately
     *
     * @param string $invoiceId Invoice ID
     * @return array
     */
    public function issueScheduledInvoice(string $invoiceId): array
    {
        $this->log()->info('nfe.api.request_sent', [
            'method' => 'POST',
            'endpoint' => "/invoices/{$invoiceId}/issue",
            'invoice_id' => $invoiceId
        ]);

        return $this->post("invoices/{$invoiceId}/issue", []);
    }

    /**
     * Reschedule invoice
     *
     * @param string $invoiceId Invoice ID
     * @param string $scheduledAt Scheduled datetime (ISO 8601)
     * @param string $timezone Timezone identifier
     * @param string|null $reason Optional reason for rescheduling
     * @return array
     */
    public function rescheduleInvoice(string $invoiceId, string $scheduledAt, string $timezone = 'America/Sao_Paulo', ?string $reason = null): array
    {
        $this->log()->info('nfe.api.request_sent', [
            'method' => 'PUT',
            'endpoint' => "/invoices/{$invoiceId}/schedule",
            'invoice_id' => $invoiceId,
            'scheduled_at' => $scheduledAt,
            'timezone' => $timezone
        ]);

        $data = [
            'scheduled_at' => $scheduledAt,
            'timezone' => $timezone,
        ];

        if ($reason) {
            $data['scheduling_reason'] = $reason;
        }

        return $this->request('PUT', "invoices/{$invoiceId}/schedule", $data);
    }

    /**
     * Recover NFSe from SEFIN by DPS number or access key
     *
     * @param array $params Recovery parameters: series (string), dps_number (int),
     *                      environment (string), force (bool)
     * @return array API response
     */
    public function recoverInvoice(array $params): array
    {
        $this->log()->info('nfe.api.request_sent', [
            'method' => 'POST',
            'endpoint' => '/v1/invoices/recover',
            'series' => $params['series'] ?? null,
            'dps_number' => $params['dps_number'] ?? null,
        ]);

        return $this->post('/v1/invoices/recover', $params);
    }

    /**
     * Download invoice file (PDF or XML) - v1 (DEPRECATED)
     *
     * @deprecated Use downloadPdfV2() or downloadXmlV2() instead
     * @param string $invoiceId Invoice ID
     * @param string $type File type (pdf|xml)
     * @return string File content or empty string on failure
     */
    public function downloadInvoiceFile(string $invoiceId, string $type): string
    {
        $baseUrl = $this->getApiBaseUrl();
        $token = $this->getApiToken();
        
        // Build the download URL
        $endpoint = $type === 'xml' ? "invoices/{$invoiceId}/xml" : "invoices/{$invoiceId}/pdf";
        $url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
        
        // Make direct HTTP request (not JSON)
        $response = wp_remote_get($url, [
            'timeout' => 30,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Accept' => $type === 'xml' ? 'application/xml' : 'application/pdf',
            ],
        ]);
        
        if (is_wp_error($response)) {
            $this->log()->warning('nfe.api.download_failed', [
                'invoice_id' => $invoiceId,
                'type' => $type,
                'error' => $response->get_error_message(),
            ]);
            return '';
        }
        
        $statusCode = wp_remote_retrieve_response_code($response);
        
        if ($statusCode !== 200) {
            $this->log()->warning('nfe.api.download_failed', [
                'invoice_id' => $invoiceId,
                'type' => $type,
                'status_code' => $statusCode,
                'response' => wp_remote_retrieve_body($response),
            ]);
            return '';
        }
        
        // Get the file content
        $content = wp_remote_retrieve_body($response);
        
        if (empty($content)) {
            $this->log()->warning('nfe.api.download_empty', [
                'invoice_id' => $invoiceId,
                'type' => $type,
            ]);
            return '';
        }
        
        $this->log()->info('nfe.api.download_success', [
            'invoice_id' => $invoiceId,
            'type' => $type,
            'size' => strlen($content),
        ]);
        
        return $content;
    }

    /**
     * Download PDF (v2 API)
     *
     * @param string $invoiceId Invoice ID
     * @return string PDF content
     * @throws \RuntimeException if download fails
     */
    public function downloadPdfV2(string $invoiceId): string
    {
        return $this->downloadFileV2($invoiceId, 'pdf')['content'];
    }

    /**
     * Download XML (v2 API)
     *
     * @param string $invoiceId Invoice ID
     * @return string XML (or ZIP for cancelled invoices) content
     * @throws \RuntimeException if download fails
     */
    public function downloadXmlV2(string $invoiceId): string
    {
        return $this->downloadFileV2($invoiceId, 'xml')['content'];
    }

    /**
     * Download XML with metadata (v2 API)
     *
     * Returns both content and the actual Content-Type from the server.
     * For cancelled invoices the server returns a ZIP archive instead of plain XML.
     *
     * @param string $invoiceId Invoice ID
     * @return array{content: string, content_type: string}
     * @throws \RuntimeException if download fails
     */
    public function downloadXmlV2WithMeta(string $invoiceId): array
    {
        return $this->downloadFileV2($invoiceId, 'xml');
    }

    /**
     * Download file from v2 API with retry logic
     *
     * @param string $invoiceId Invoice ID
     * @param string $type File type (pdf|xml)
     * @param int $maxRetries Maximum number of retries for transient failures
     * @return array{content: string, content_type: string}
     * @throws \RuntimeException if download fails after all retries
     */
    private function downloadFileV2(string $invoiceId, string $type, int $maxRetries = 2): array
    {
        $baseUrl = $this->getApiBaseUrl();
        $token = $this->getApiToken();

        // Build v2 endpoint URL
        $endpoint = "/v1/invoices/{$invoiceId}/{$type}";
        $url = rtrim($baseUrl, '/') . $endpoint;

        $lastException = null;
        $lastStatusCode = null;

        for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
            if ($attempt > 0) {
                // Exponential backoff: 1s, 2s, 4s...
                $backoffMs = (int) pow(2, $attempt - 1) * 1000000;
                usleep($backoffMs);

                $this->log()->info('nfe.api.download_retry', [
                    'invoice_id' => $invoiceId,
                    'type' => $type,
                    'attempt' => $attempt + 1,
                    'max_retries' => $maxRetries + 1,
                ]);
            }

            $this->log()->debug('nfe.api.request_sent', [
                'method' => 'GET',
                'endpoint' => $endpoint,
                'invoice_id' => $invoiceId,
                'type' => $type,
                'attempt' => $attempt + 1,
            ]);

            // Make direct HTTP request (not JSON)
            $response = wp_remote_get($url, [
                'timeout' => 30,
                'headers' => [
                    'Authorization' => 'Bearer ' . $token,
                    'Accept' => $type === 'xml' ? 'application/xml' : 'application/pdf',
                ],
            ]);

            if (is_wp_error($response)) {
                $error = $response->get_error_message();
                $lastException = new \RuntimeException("Failed to download {$type}: {$error}");

                // Log as warning for transient network errors
                $this->log()->warning('nfe.api.download_network_error', [
                    'invoice_id' => $invoiceId,
                    'type' => $type,
                    'error' => $error,
                    'attempt' => $attempt + 1,
                ]);

                // Retry on network errors
                continue;
            }

            $statusCode = wp_remote_retrieve_response_code($response);
            $lastStatusCode = $statusCode;

            // Success
            if ($statusCode === 200) {
                $content = wp_remote_retrieve_body($response);

                if (empty($content)) {
                    $this->log()->warning('nfe.api.download_empty', [
                        'invoice_id' => $invoiceId,
                        'type' => $type,
                    ]);
                    throw new \RuntimeException("Downloaded {$type} is empty");
                }

                $contentType = wp_remote_retrieve_header($response, 'content-type');
                // Normalise: strip charset/boundary suffixes (e.g. "application/xml; charset=utf-8")
                $contentType = strtolower(trim(explode(';', $contentType)[0]));

                $this->log()->info('nfe.api.download_success', [
                    'invoice_id' => $invoiceId,
                    'type' => $type,
                    'size' => strlen($content),
                    'content_type' => $contentType,
                ]);

                return ['content' => $content, 'content_type' => $contentType];
            }

            // Don't retry client errors (4xx) - these are permanent failures
            if ($statusCode >= 400 && $statusCode < 500) {
                $this->log()->warning('nfe.api.download_client_error', [
                    'invoice_id' => $invoiceId,
                    'type' => $type,
                    'status_code' => $statusCode,
                    'response' => wp_remote_retrieve_body($response),
                ]);
                throw new \RuntimeException("Failed to download {$type}: HTTP {$statusCode}");
            }

            // Retry server errors (5xx) - might be temporary
            if ($statusCode >= 500) {
                $lastException = new \RuntimeException("Failed to download {$type}: HTTP {$statusCode}");

                $this->log()->warning('nfe.api.download_server_error', [
                    'invoice_id' => $invoiceId,
                    'type' => $type,
                    'status_code' => $statusCode,
                    'attempt' => $attempt + 1,
                ]);

                // Continue to next retry
                continue;
            }

            // Unknown status code - don't retry
            $this->log()->error('nfe.api.download_failed', [
                'invoice_id' => $invoiceId,
                'type' => $type,
                'status_code' => $statusCode,
                'response' => wp_remote_retrieve_body($response),
            ]);
            throw new \RuntimeException("Failed to download {$type}: HTTP {$statusCode}");
        }

        // All retries exhausted
        $this->log()->error('nfe.api.download_failed_after_retries', [
            'invoice_id' => $invoiceId,
            'type' => $type,
            'attempts' => $maxRetries + 1,
            'last_status_code' => $lastStatusCode,
        ]);

        throw $lastException ?? new \RuntimeException("Failed to download {$type} after {$maxRetries} retries");
    }

    /**
     * Get tenant defaults from NFE server (v1 - DEPRECATED)
     *
     * @deprecated Use getTenantConfigV2() instead
     * @return array
     */
    public function getTenantDefaults(): array
    {
        try {
            $candidates = [
                'v1/tenant/config',
                '/v1/tenant/config',
                'tenant/defaults',
            ];

            foreach ($candidates as $endpoint) {
                $response = $this->get($endpoint);
                if (!empty($response['error'])) {
                    continue;
                }

                $normalized = $this->normalizeTenantDefaultsPayload($response);
                if (!empty($normalized)) {
                    return $normalized;
                }
            }

            $this->log()->warning('nfe.api.get_tenant_defaults_failed', [
                'error' => 'No compatible tenant defaults response',
            ]);

            return [];
        } catch (\Exception $e) {
            $this->log()->error('nfe.api.get_tenant_defaults_exception', [
                'error' => $e->getMessage()
            ]);
            return [];
        }
    }

    /**
     * Get tenant config (v2 API)
     *
     * @return TenantConfigResource
     * @throws \RuntimeException if API call fails
     */
    public function getTenantConfigV2(): TenantConfigResource
    {
        $this->log()->info('nfe.api.request_sent', [
            'method' => 'GET',
            'endpoint' => '/v1/tenant/config'
        ]);

        // Send request to v2 endpoint
        $response = $this->get('/v1/tenant/config');

        if (!empty($response['error'])) {
            $this->log()->error('nfe.api.request_failed', [
                'operation' => 'get_tenant_config',
                'error' => $response['error']
            ]);
            throw new \RuntimeException('Failed to get tenant config: ' . $response['error']);
        }

        $this->log()->info('nfe.api.response_received', [
            'operation' => 'get_tenant_config',
            'tenant_id' => $response['tenant_id'] ?? 'unknown'
        ]);

        // Parse response into DTO
        try {
            return TenantConfigResource::fromArray($response);
        } catch (\Exception $e) {
            $this->log()->error('nfe.api.parsing_failed', [
                'operation' => 'get_tenant_config',
                'error' => $e->getMessage(),
                'response' => $response
            ]);
            throw new \RuntimeException('Failed to parse tenant config response: ' . $e->getMessage());
        }
    }

    /**
     * Get municipal services from fiscal-info endpoint
     *
     * @param string $municipalityCode IBGE municipality code (7 digits)
     * @param bool $includeInactive Include inactive services
     * @return array
     * @throws \RuntimeException if API call fails
     */
    public function getMunicipalServices(string $municipalityCode, bool $includeInactive = false): array
    {
        $this->log()->info('nfe.api.request_sent', [
            'method' => 'GET',
            'endpoint' => '/v1/fiscal-info/municipal-services',
            'municipality_code' => $municipalityCode,
            'include_inactive' => $includeInactive
        ]);

        $response = $this->get('/v1/fiscal-info/municipal-services', [
            'municipality_code' => $municipalityCode,
            'include_inactive' => $includeInactive ? 'true' : 'false'
        ]);

        if (!empty($response['error'])) {
            $this->log()->error('nfe.api.request_failed', [
                'operation' => 'get_municipal_services',
                'municipality_code' => $municipalityCode,
                'error' => $response['error']
            ]);
            throw new \RuntimeException('Failed to get municipal services: ' . $response['error']);
        }

        $this->log()->info('nfe.api.response_received', [
            'operation' => 'get_municipal_services',
            'municipality_code' => $municipalityCode,
            'total' => $response['total'] ?? 'unknown'
        ]);

        return $response;
    }

    /**
     * Get grace period from NFE server
     *
     * @return array
     */
    public function getGracePeriod(): array
    {
        try {
            $response = $this->get('tenant/grace-period');
            $data = $response['data'] ?? $response;

            if (!empty($response['error'])) {
                $this->log()->warning('nfe.api.get_grace_period_failed', [
                    'error' => $response['message'] ?? 'Unknown error'
                ]);
                return ['grace_period' => 5]; // Default to 5 days
            }

            if (isset($data['grace_period']) && is_numeric($data['grace_period'])) {
                return ['grace_period' => (int) $data['grace_period']];
            }

            return ['grace_period' => 5];
        } catch (\Exception $e) {
            $this->log()->error('nfe.api.get_grace_period_exception', [
                'error' => $e->getMessage()
            ]);
            return ['grace_period' => 5]; // Default to 5 days on error
        }
    }

    /**
     * Normalize tenant defaults payload from multiple server formats.
     *
     * Server returns: nfse.service_code, nfse.nbs_code, nfse.municipality_code
     * Plugin supports both: service_code (root) and nfse.service_code (nested)
     * Legacy support: nfse.default_service_code (deprecated naming)
     */
    private function normalizeTenantDefaultsPayload(array $response): array
    {
        $data = $response['data'] ?? $response;
        $nfse = $data['nfse'] ?? [];

        // Check root level first, then nested nfse, then legacy default_* prefix
        $serviceCode = $data['service_code'] ?? $nfse['service_code'] ?? $nfse['default_service_code'] ?? $nfse['municipal_service_code'] ?? '';
        $nationalServiceCode = $data['national_service_code'] ?? $nfse['national_service_code'] ?? '';
        $nbsCode = $data['nbs_code'] ?? $nfse['nbs_code'] ?? $nfse['default_nbs_code'] ?? '';
        $issRate = $data['iss_rate'] ?? $nfse['iss_rate'] ?? $nfse['default_iss_rate'] ?? '';
        $municipalityCode = $data['municipality_code'] ?? $nfse['municipality_code'] ?? '';
        $gracePeriod = $data['grace_period'] ?? $nfse['grace_period'] ?? null;

        if ($serviceCode === '' && $nationalServiceCode === '' && $nbsCode === '' && $issRate === '' && $municipalityCode === '') {
            return [];
        }

        return [
            'service_code' => (string) $serviceCode,
            'national_service_code' => (string) $nationalServiceCode,
            'nbs_code' => (string) $nbsCode,
            'iss_rate' => is_numeric($issRate) ? (float) $issRate : '',
            'municipality_code' => (string) $municipalityCode,
            'grace_period' => is_numeric($gracePeriod) ? (int) $gracePeriod : null,
        ];
    }

    /**
     * Get list of companies for the authenticated tenant.
     *
     * @return array List of company arrays from the server
     */
    public function getCompanies(): array
    {
        $this->log()->info('nfe.api.request_sent', [
            'method' => 'GET',
            'endpoint' => '/v1/companies',
        ]);

        $response = $this->get('/v1/companies');

        if (!empty($response['error'])) {
            $this->log()->error('nfe.api.request_failed', [
                'operation' => 'get_companies',
                'error' => $response['error'],
            ]);
            throw new \RuntimeException('Failed to get companies: ' . $response['error']);
        }

        $companies = $response['data'] ?? $response;

        $this->log()->info('nfe.api.response_received', [
            'operation' => 'get_companies',
            'total' => is_array($companies) ? count($companies) : 0,
        ]);

        return is_array($companies) ? $companies : [];
    }

    /**
     * Get company configuration including federal taxes
     *
     * Endpoint: GET /v1/companies/{companyId}
     *
     * The response includes federal_taxes with the following structure:
     * - irrf_enabled, irrf_rate, irrf_min_value, irrf_apply_pf, irrf_apply_pj
     * - pis_cofins_cst, pis_rate, cofins_rate
     * - inss_enabled, inss_rate, inss_min_value
     * - csll_enabled, csll_rate, csll_min_value
     *
     * @param string $companyId Company UUID
     * @return array Company configuration with federal taxes
     * @throws \RuntimeException if API call fails
     */
    public function getCompanyConfig(string $companyId): array
    {
        $endpoint = '/v1/companies/' . $companyId;

        $this->log()->info('nfe.api.request_sent', [
            'method' => 'GET',
            'endpoint' => $endpoint,
            'company_id' => $companyId,
        ]);

        $response = $this->get($endpoint);

        if (!empty($response['error'])) {
            $this->log()->error('nfe.api.request_failed', [
                'operation' => 'get_company_config',
                'endpoint' => $endpoint,
                'company_id' => $companyId,
                'error' => $response['error'] ?? 'Unknown error',
            ]);

            throw new \RuntimeException('Failed to get company config: ' . ($response['error'] ?? 'Unknown error'));
        }

        $company = $response['data'] ?? $response;

        $this->log()->info('nfe.api.response_received', [
            'operation' => 'get_company_config',
            'endpoint' => $endpoint,
            'company_id' => $companyId,
            'has_federal_taxes' => isset($company['federal_taxes']),
        ]);

        return is_array($company) ? $company : [];
    }
}
