<?php

declare(strict_types=1);

namespace App\Integrations;

use Exception;

/**
 * Cliente para integração com Shipay (Boleto Híbrido / Pix / etc.).
 *
 * Documentação: https://shipay.readme.io/reference/postpdvauth
 *
 * ENDPOINTS DISPONÍVEIS:
 * - Autenticação: POST /pdvauth
 * - Criar ordem/cobrança: POST /order
 * - Boleto Híbrido: POST /boletos-hibridos (verificar disponibilidade)
 *
 * AMBIENTE DE HOMOLOGAÇÃO (Sandbox):
 * Use suas credenciais reais para autenticação.
 * A wallet "shipay-pagador" é usada automaticamente para testes.
 * Após gerar o QR Code, escaneie em: apptest-staging.shipay.com.br
 *
 * Referência: https://docs.shipay.com.br/shipay-pagador.html
 *
 * URL Sandbox: https://api-staging.shipay.com.br
 */
class ShipayClient
{
    private const ENDPOINT_AUTH = '/pdvauth';
    // Endpoint principal para criar cobranças (Pix, Boleto, etc)
    private const ENDPOINT_ORDER = '/order';
    // Endpoint para consultar pedidos com expiration_date (boletos híbridos)
    private const ENDPOINT_ORDER_V = '/orderv';
    // Tempo mínimo de vida do token em segundos (3 horas conforme solicitado pela Shipay)
    private const TOKEN_MIN_LIFETIME = 10800;
    // Endpoint legado Bacen (deprecated)
    private const ENDPOINT_BACEN_ORDER_DUE_DATE = '/v1/bacen/order-due-date';
    // Endpoint para criar e consultar PIX com vencimento (boleto híbrido)
    private const ENDPOINT_V2_ORDER_DUE_DATE = '/v2/order-due-date';
    // Endpoint legado para boleto híbrido (deprecated)
    private const ENDPOINT_HYBRID_BOLETO = '/v1/hybrid-boleto';
    // Endpoint legado para boleto híbrido
    private const ENDPOINT_BOLETO = '/boletos-hibridos';

    private string $accessKey;
    private string $secretKey;
    private string $clientId;
    private string $environment;
    private string $baseUrl;

    private ?string $accessToken = null;
    private ?int $tokenExpiresAt = null;

    public function __construct(array $config)
    {
        $this->accessKey = $config['access_key'] ?? '';
        $this->secretKey = $config['secret_key'] ?? '';
        $this->clientId = $config['client_id'] ?? '';
        $this->environment = $config['environment'] ?? 'sandbox';
        $this->baseUrl = rtrim($config['base_url'][$this->environment] ?? '', '/');

        error_log("[ShipayClient] Inicializando - Ambiente: {$this->environment}, URL: {$this->baseUrl}");

        if (empty($this->accessKey) || empty($this->secretKey) || empty($this->clientId)) {
            throw new Exception('Credenciais da Shipay não configuradas.');
        }

        if (empty($this->baseUrl)) {
            throw new Exception(sprintf('Base URL da Shipay não encontrada para o ambiente "%s".', $this->environment));
        }
    }

    /**
     * Registrar uma cobrança (Pix, Boleto Híbrido, etc).
     *
     * Usa o endpoint /order da Shipay para PIX.
     * Para Boleto Híbrido, usa /v1/hybrid-boleto.
     *
     * @param array $payload Estrutura esperada pela Shipay.
     * @param string $tipo 'pix' ou 'boleto'
     */
    public function createHybridBoleto(array $payload, string $tipo = 'pix'): array
    {
        // Se for boleto híbrido, usar endpoint específico
        if ($tipo === 'boleto') {
            $this->logShipay("Gerando BOLETO HÍBRIDO - Ambiente: {$this->environment}");
            return $this->createBoletoHibrido($payload);
        }

        // Converter payload do formato interno para o formato da Shipay
        $shipayPayload = $this->convertPayloadToOrder($payload);

        $this->logShipay("Payload convertido para /order (PIX): " . json_encode($shipayPayload, JSON_UNESCAPED_UNICODE));

        // Usar o endpoint /order para PIX
        return $this->request('POST', self::ENDPOINT_ORDER, $shipayPayload);
    }

    /**
     * Registrar um Boleto Híbrido usando endpoint /v2/order-due-date
     *
     * Conforme documentação Shipay:
     * https://api-staging.shipay.com.br/v2/order-due-date
     *
     * Este endpoint gera PIX com vencimento (Pix Cobrança com due_date)
     *
     * @param array $payload Dados do boleto
     */
    public function createBoletoHibrido(array $payload): array
    {
        $shipayPayload = $this->convertPayloadToBacenOrderDueDate($payload);

        $this->logShipay("Payload para /v2/order-due-date: " . json_encode($shipayPayload, JSON_UNESCAPED_UNICODE));

        // Usar somente o endpoint oficial /v2/order-due-date.
        // Se falhar, deixamos a exceção subir para o chamador decidir (sem fallback automático).
        return $this->request('POST', self::ENDPOINT_V2_ORDER_DUE_DATE, $shipayPayload);
    }

    /**
     * Converte payload para formato do endpoint /v1/bacen/order-due-date
     *
     * PIX Cobrança com vencimento (Bacen)
     * Estrutura conforme documentação Shipay:
     * - calendar.due_date: Data de vencimento (YYYY-MM-DD)
     */
    private function convertPayloadToBacenOrderDueDate(array $payload): array
    {
        $valor = (float) ($payload['valor'] ?? $payload['total'] ?? 0);

        // Formatar vencimento no formato YYYY-MM-DD
        $vencimento = $payload['vencimento'] ?? date('Y-m-d', strtotime('+30 days'));
        if (strtotime($vencimento)) {
            $vencimento = date('Y-m-d', strtotime($vencimento));
        }

        // Garantir que a data de vencimento seja >= hoje (requisito Shipay)
        $hoje = date('Y-m-d');
        if ($vencimento < $hoje) {
            $this->logShipay("Data de vencimento ({$vencimento}) anterior a hoje. Ajustando para hoje.");
            $vencimento = $hoje;
        }

        $shipayPayload = [
            'order_ref' => $payload['order_ref'] ?? ('Systhema_' . time() . '_' . rand(1000, 9999)),
            'total' => $valor,
            'wallet' => $payload['wallet'] ?? 'shipay-pagador',
            'calendar' => [
                'due_date' => $vencimento,
            ],
            'items' => [
                [
                    'item_title' => $payload['descricao'] ?? $payload['description'] ?? 'Cobrança',
                    'quantity' => 1,
                    'unit_price' => $valor,
                ]
            ],
        ];

        // Dados do pagador (buyer) - obrigatório
        $pagador = $payload['pagador'] ?? $payload['buyer'] ?? [];
        if (!empty($pagador)) {
            $buyer = [];

            $nome = $pagador['nome'] ?? $pagador['name'] ?? '';
            if (!empty($nome)) {
                $buyer['name'] = $nome;
            }

            $cpfCnpj = preg_replace('/\D/', '', $pagador['documento'] ?? $pagador['cpf_cnpj'] ?? '');
            if (!empty($cpfCnpj)) {
                $buyer['cpf_cnpj'] = $cpfCnpj;
            }

            $email = $pagador['email'] ?? '';
            if (!empty($email)) {
                $buyer['email'] = $email;
            }

            if (!empty($buyer)) {
                $shipayPayload['buyer'] = $buyer;
            }
        }

        $this->logShipay("Payload convertido para /v1/bacen/order-due-date: " . json_encode($shipayPayload, JSON_UNESCAPED_UNICODE));

        return $shipayPayload;
    }

    /**
     * Converte payload para formato do endpoint de Boleto Híbrido (legado)
     *
     * Conforme documentação Shipay para /v1/hybrid-boleto
     */
    private function convertPayloadToBoletoHibrido(array $payload): array
    {
        $valor = (float) ($payload['valor'] ?? $payload['total'] ?? 0);

        // Formatar vencimento
        $vencimento = $payload['vencimento'] ?? date('Y-m-d', strtotime('+30 days'));
        if (strtotime($vencimento)) {
            $vencimento = date('Y-m-d', strtotime($vencimento));
        }

        $shipayPayload = [
            'order_ref' => $payload['order_ref'] ?? ('Systhema_' . time() . '_' . rand(1000, 9999)),
            'total' => $valor,
            'due_date' => $vencimento,
            'items' => [
                [
                    'item_title' => $payload['descricao'] ?? $payload['description'] ?? 'Cobrança',
                    'quantity' => 1,
                    'unit_price' => $valor,
                ]
            ],
        ];

        // Dados do pagador (payer) - obrigatório para boleto
        $pagador = $payload['pagador'] ?? $payload['buyer'] ?? [];
        if (!empty($pagador)) {
            $payer = [];

            $nome = $pagador['nome'] ?? $pagador['name'] ?? '';
            if (!empty($nome)) {
                $payer['name'] = $nome;
            }

            $cpfCnpj = preg_replace('/\D/', '', $pagador['documento'] ?? $pagador['cpf_cnpj'] ?? '');
            if (!empty($cpfCnpj)) {
                $payer['cpf_cnpj'] = $cpfCnpj;
            }

            $email = $pagador['email'] ?? '';
            if (!empty($email)) {
                $payer['email'] = $email;
            }

            $phone = preg_replace('/\D/', '', $pagador['telefone'] ?? $pagador['phone'] ?? '');
            if (!empty($phone)) {
                $payer['phone'] = $phone;
            }

            // Endereço do pagador
            $endereco = $pagador['endereco'] ?? [];
            if (!empty($endereco)) {
                $address = [];
                if (!empty($endereco['logradouro']))
                    $address['street'] = $endereco['logradouro'];
                if (!empty($endereco['numero']))
                    $address['number'] = $endereco['numero'];
                if (!empty($endereco['complemento']))
                    $address['complement'] = $endereco['complemento'];
                if (!empty($endereco['bairro']))
                    $address['neighborhood'] = $endereco['bairro'];
                if (!empty($endereco['cidade']))
                    $address['city'] = $endereco['cidade'];
                if (!empty($endereco['uf']))
                    $address['state'] = $endereco['uf'];
                if (!empty($endereco['cep']))
                    $address['zip_code'] = preg_replace('/\D/', '', $endereco['cep']);

                if (!empty($address)) {
                    $payer['address'] = $address;
                }
            }

            if (!empty($payer)) {
                $shipayPayload['payer'] = $payer;
            }
        }

        // Instruções do boleto (opcional)
        if (!empty($payload['instrucoes'])) {
            $shipayPayload['instructions'] = $payload['instrucoes'];
        }

        // Callback URL (opcional)
        if (!empty($payload['callback_url'])) {
            $shipayPayload['callback_url'] = $payload['callback_url'];
        }

        return $shipayPayload;
    }

    /**
     * Converte o payload do formato interno para o formato esperado pelo endpoint /order
     *
     * O endpoint /order da Shipay é usado para todas as cobranças.
     * O tipo de cobrança (Pix, Boleto Híbrido, etc.) depende do wallet
     * configurado no painel da Shipay para a loja.
     *
     * Documentação: https://shipay.readme.io/reference/post_order
     */
    private function convertPayloadToOrder(array $payload): array
    {
        $valor = (float) ($payload['valor'] ?? $payload['total'] ?? 0);

        $shipayPayload = [
            'order_ref' => $payload['order_ref'] ?? ('Systhema_' . time() . '_' . rand(1000, 9999)),
            'total' => $valor,
            'items' => [
                [
                    'item_title' => $payload['descricao'] ?? $payload['description'] ?? 'Cobrança',
                    'quantity' => 1,
                    'unit_price' => $valor,
                ]
            ],
        ];

        // Dados do pagador (buyer)
        $pagador = $payload['pagador'] ?? $payload['buyer'] ?? [];
        if (!empty($pagador)) {
            $buyer = [];

            $nome = $pagador['nome'] ?? $pagador['name'] ?? '';
            if (!empty($nome)) {
                $buyer['name'] = $nome;
            }

            $cpfCnpj = preg_replace('/\D/', '', $pagador['documento'] ?? $pagador['cpf_cnpj'] ?? '');
            if (!empty($cpfCnpj)) {
                $buyer['cpf_cnpj'] = $cpfCnpj;
            }

            $email = $pagador['email'] ?? '';
            if (!empty($email)) {
                $buyer['email'] = $email;
            }

            $phone = preg_replace('/\D/', '', $pagador['telefone'] ?? $pagador['phone'] ?? '');
            if (!empty($phone)) {
                $buyer['phone'] = $phone;
            }

            if (!empty($buyer)) {
                $shipayPayload['buyer'] = $buyer;
            }
        }

        // Wallet (tipo de pagamento) - obrigatório na Shipay
        // Em ambiente sandbox, usa 'shipay-pagador' para testes (conforme docs.shipay.com.br/shipay-pagador.html)
        // Em produção, usa o wallet especificado
        if ($this->environment === 'sandbox') {
            // Carteira de testes - simula PIX e Boleto (sem diferença real no sandbox)
            $shipayPayload['wallet'] = 'shipay-pagador';

            // Log informativo sobre sandbox
            $tipoCobranca = $payload['wallet'] ?? 'não especificado';
            $this->logShipay("SANDBOX: Usando wallet 'shipay-pagador' (simula {$tipoCobranca})");
        } else {
            // Produção: usar wallet específico
            // PIX: 'pix', 'mercadopago', 'picpay', 'pagseguro', etc.
            // Boleto: precisa ter wallet de banco configurado no painel Shipay
            //         Ex: 'itau-boleto', 'bb-boleto', 'bradesco-boleto', 'sicoob-boleto'
            $wallet = $payload['wallet'] ?? 'pix';
            $shipayPayload['wallet'] = $wallet;

            $this->logShipay("PRODUÇÃO: Usando wallet '{$wallet}'");
        }

        // Data de expiração (se fornecida)
        if (!empty($payload['vencimento'])) {
            $vencimento = strtotime($payload['vencimento']);
            if ($vencimento) {
                $shipayPayload['expiration_date'] = date('Y-m-d\TH:i:s', strtotime($payload['vencimento'] . ' 23:59:59'));
            }
        }

        // Callback URL (opcional)
        if (!empty($payload['callback_url'])) {
            $shipayPayload['callback_url'] = $payload['callback_url'];
        }

        return $shipayPayload;
    }

    /**
     * Recupera os dados de uma cobrança.
     *
     * @param string $chargeId ID da cobrança
     * @param string $tipo 'pix' ou 'boleto'
     */
    public function getHybridBoleto(string $chargeId, string $tipo = 'pix'): array
    {
        $this->logShipay("Consultando boleto/PIX - ID: {$chargeId}, Tipo: {$tipo}");

        // Se for boleto/PIX com vencimento, tentar endpoint /orderv primeiro (para pedidos com expiration_date)
        if ($tipo === 'boleto') {
            try {
                $this->logShipay("Tentando endpoint /orderv para boleto com vencimento");
                return $this->request('GET', sprintf('%s/%s', self::ENDPOINT_ORDER_V, urlencode($chargeId)));
            } catch (Exception $e) {
                $this->logShipay("Endpoint /orderv falhou: " . $e->getMessage());
                // Continuar para tentar outros endpoints
            }
        }

        // Tentar primeiro o endpoint /order
        try {
            return $this->request('GET', sprintf('%s/%s', self::ENDPOINT_ORDER, urlencode($chargeId)));
        } catch (Exception $e) {
            $errorMessage = $e->getMessage();
            // Se o erro indicar que precisa usar /orderv (pedido com expiration_date)
            if (strpos($errorMessage, 'expiration_date') !== false || strpos($errorMessage, '/orderv') !== false || strpos($errorMessage, '400') !== false) {
                $this->logShipay("Erro 400 detectado - tentando endpoint /orderv para pedido com expiration_date");
                try {
                    return $this->request('GET', sprintf('%s/%s', self::ENDPOINT_ORDER_V, urlencode($chargeId)));
                } catch (Exception $eOrderv) {
                    $this->logShipay("Endpoint /orderv também falhou: " . $eOrderv->getMessage());
                    // Continuar para tentar outros endpoints
                }
            }

            // Se falhar, tentar o endpoint /v2/order-due-date
            if (strpos($errorMessage, '404') !== false) {
                try {
                    return $this->request('GET', sprintf('%s/%s', self::ENDPOINT_V2_ORDER_DUE_DATE, urlencode($chargeId)));
                } catch (Exception $e2) {
                    // Se falhar também, tentar endpoint de boletos híbridos legado
                    if (strpos($e2->getMessage(), '404') !== false) {
                        try {
                            return $this->request('GET', sprintf('%s/%s', self::ENDPOINT_HYBRID_BOLETO, urlencode($chargeId)));
                        } catch (Exception $e3) {
                            if (strpos($e3->getMessage(), '404') !== false) {
                                return $this->request('GET', sprintf('%s/%s', self::ENDPOINT_BOLETO, urlencode($chargeId)));
                            }
                            throw $e3;
                        }
                    }
                    throw $e2;
                }
            }
            throw $e;
        }
    }

    /**
     * Consulta o status de um Boleto Híbrido
     *
     * @param string $chargeId ID da cobrança
     */
    public function getStatusBoletoHibrido(string $chargeId): array
    {
        return $this->request('GET', sprintf('%s/%s/status', self::ENDPOINT_HYBRID_BOLETO, urlencode($chargeId)));
    }

    /**
     * Baixa (cancela) um Boleto Híbrido
     *
     * @param string $chargeId ID da cobrança
     * @param string $motivo Motivo da baixa
     */
    public function baixaBoletoHibrido(string $chargeId, string $motivo = ''): array
    {
        $payload = ['status' => 'cancelled'];
        if (!empty($motivo)) {
            $payload['reason'] = $motivo;
        }

        return $this->request('PATCH', sprintf('%s/%s', self::ENDPOINT_HYBRID_BOLETO, urlencode($chargeId)), $payload);
    }

    /**
     * Cancela uma cobrança Shipay (pedido pendente).
     */
    public function cancelOrder(string $orderId): array
    {
        return $this->request('DELETE', sprintf('%s/%s', self::ENDPOINT_ORDER, urlencode($orderId)));
    }

    /**
     * Estorna um pagamento Shipay (pedido já pago).
     *
     * A Shipay suporta estorno no sandbox usando a carteira shipay-pagador.
     * Para testar: criar pedido > R$ 1.000, pagar pelo apptest-staging, depois estornar.
     *
     * Endpoints possíveis (tentamos ambos):
     * - POST /order/{order_id}/refund (mais comum)
     * - DELETE /order/{order_id}/refund (alternativa)
     *
     * Ref: https://docs.shipay.com.br/flows-cancellation-reversal.html
     */
    public function refundOrder(string $orderId, ?float $amount = null, ?string $reason = null): array
    {
        $endpoint = sprintf('%s/%s/refund', self::ENDPOINT_ORDER, urlencode($orderId));

        $this->logShipay("Estorno - Order ID: {$orderId}, Amount: " . ($amount ?? 'total') . ", Reason: " . ($reason ?? 'N/A'));

        // Preparar payload para estorno
        $payload = [];
        if ($amount !== null && $amount > 0) {
            $payload['amount'] = $amount;
        }
        if ($reason !== null && !empty($reason)) {
            $payload['reason'] = $reason;
        }

        // Tentar primeiro com POST (mais comum em APIs de pagamento)
        try {
            $this->logShipay("Tentando estorno com POST...");
            return $this->request('POST', $endpoint, !empty($payload) ? $payload : null);
        } catch (Exception $e) {
            // Se POST falhar com 404 ou 405, tentar DELETE
            if (strpos($e->getMessage(), '404') !== false || strpos($e->getMessage(), '405') !== false) {
                $this->logShipay("POST falhou, tentando com DELETE...");
                return $this->request('DELETE', $endpoint);
            }
            throw $e;
        }
    }

    /**
     * Método legado - mantido para compatibilidade.
     * Use cancelOrder() para cancelar ou refundOrder() para estornar.
     */
    public function updateHybridBoleto(string $chargeId, array $payload): array
    {
        // Para cancelar, usa DELETE no endpoint /order
        if (($payload['status'] ?? '') === 'cancelled') {
            return $this->cancelOrder($chargeId);
        }

        // Para estorno, usa POST /order/{id}/refund
        if (($payload['status'] ?? '') === 'refunded') {
            $amount = $payload['refund_amount'] ?? null;
            $reason = $payload['refund_reason'] ?? null;
            return $this->refundOrder($chargeId, $amount, $reason);
        }

        // Para outras atualizações, tentar PATCH (pode não funcionar)
        try {
            return $this->request('PATCH', sprintf('%s/%s', self::ENDPOINT_ORDER, urlencode($chargeId)), $payload);
        } catch (Exception $e) {
            throw new Exception("Operação não suportada pela API Shipay: " . $e->getMessage());
        }
    }

    /**
     * Realiza autenticação e guarda o token em arquivo para reutilização.
     *
     * Conforme solicitado pela Shipay durante homologação:
     * O token deve ser reutilizado por pelo menos 3 horas.
     * Evita gerar novo token a cada operação.
     */
    private function authenticate(bool $force = false): void
    {
        // Tentar carregar token do cache (arquivo) se não estiver em memória
        if (!$this->accessToken || !$this->tokenExpiresAt) {
            $this->loadTokenFromCache();
        }

        // Verificar se o token em memória ainda é válido (com margem de 5 minutos)
        $isTokenValid = $this->accessToken
            && $this->tokenExpiresAt
            && $this->tokenExpiresAt > time() + 300;

        if ($isTokenValid && !$force) {
            $remainingTime = $this->tokenExpiresAt - time();
            $this->logShipay("Token reutilizado do cache. Expira em " . round($remainingTime / 60) . " minutos.");
            return;
        }

        $this->logShipay("Gerando novo token de autenticação...");

        $response = $this->rawRequest('POST', self::ENDPOINT_AUTH, [
            'access_key' => $this->accessKey,
            'secret_key' => $this->secretKey,
            'client_id' => $this->clientId,
        ], false);

        $token = $response['token'] ?? $response['access_token'] ?? null;
        $expiresIn = (int) ($response['expires_in'] ?? 0);

        if (empty($token)) {
            throw new Exception('Token de acesso da Shipay não retornado.');
        }

        $this->accessToken = $token;

        // Se a API não retornar expires_in, usar o tempo mínimo de 3 horas
        if ($expiresIn <= 0) {
            $expiresIn = self::TOKEN_MIN_LIFETIME;
        }

        $this->tokenExpiresAt = time() + $expiresIn;

        // Salvar token em cache (arquivo) para reutilização entre requisições
        $this->saveTokenToCache();

        $this->logShipay("Novo token gerado. Expira em " . round($expiresIn / 60) . " minutos.");
    }

    /**
     * Retorna o caminho do arquivo de cache do token.
     * Usa um hash do client_id para permitir múltiplas lojas/ambientes.
     */
    private function getTokenCacheFile(): string
    {
        $cacheDir = (defined('ROOT_PATH') ? ROOT_PATH : dirname(__DIR__, 2)) . '/storage/cache';
        $cacheKey = md5($this->clientId . '_' . $this->environment);
        return $cacheDir . '/shipay_token_' . $cacheKey . '.json';
    }

    /**
     * Carrega o token do cache (arquivo).
     */
    private function loadTokenFromCache(): void
    {
        $cacheFile = $this->getTokenCacheFile();

        if (!file_exists($cacheFile)) {
            return;
        }

        $content = @file_get_contents($cacheFile);
        if (empty($content)) {
            return;
        }

        $data = json_decode($content, true);
        if (json_last_error() !== JSON_ERROR_NONE || empty($data)) {
            return;
        }

        $token = $data['access_token'] ?? null;
        $expiresAt = (int) ($data['expires_at'] ?? 0);

        // Verificar se o token ainda é válido (com margem de 5 minutos)
        if (!empty($token) && $expiresAt > time() + 300) {
            $this->accessToken = $token;
            $this->tokenExpiresAt = $expiresAt;
            $this->logShipay("Token carregado do cache. Válido até " . date('Y-m-d H:i:s', $expiresAt));
        } else {
            // Token expirado, remover arquivo de cache
            @unlink($cacheFile);
            $this->logShipay("Token em cache expirado ou inválido. Será gerado novo token.");
        }
    }

    /**
     * Salva o token em cache (arquivo) para reutilização.
     */
    private function saveTokenToCache(): void
    {
        $cacheFile = $this->getTokenCacheFile();
        $cacheDir = dirname($cacheFile);

        // Criar diretório de cache se não existir
        if (!is_dir($cacheDir)) {
            @mkdir($cacheDir, 0775, true);
        }

        $data = [
            'access_token' => $this->accessToken,
            'expires_at' => $this->tokenExpiresAt,
            'created_at' => time(),
            'environment' => $this->environment,
            'client_id' => $this->clientId,
        ];

        $result = @file_put_contents(
            $cacheFile,
            json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
            LOCK_EX
        );

        if ($result !== false) {
            $this->logShipay("Token salvo em cache: {$cacheFile}");
        } else {
            $this->logShipay("AVISO: Não foi possível salvar token em cache.");
        }
    }

    /**
     * Executa uma requisição autenticada.
     */
    private function request(string $method, string $endpoint, ?array $body = null): array
    {
        $this->authenticate();

        $headers = [
            'Authorization: Bearer ' . $this->accessToken,
        ];

        return $this->rawRequest($method, $endpoint, $body, true, $headers);
    }

    /**
     * Escreve log específico da Shipay em arquivo dedicado
     */
    private function logShipay(string $message): void
    {
        $logFile = (defined('ROOT_PATH') ? ROOT_PATH : dirname(__DIR__, 2)) . '/storage/logs/shipay-' . date('Y-m-d') . '.log';
        $logDir = dirname($logFile);

        if (!is_dir($logDir)) {
            @mkdir($logDir, 0775, true);
        }

        $timestamp = date('Y-m-d H:i:s');
        $logMessage = "[{$timestamp}] {$message}" . PHP_EOL;

        @file_put_contents($logFile, $logMessage, FILE_APPEND | LOCK_EX);
        error_log("[ShipayClient] {$message}");
    }

    /**
     * Requisição HTTP simples usando cURL.
     *
     * @param bool $authenticated Quando true, adiciona cabeçalhos extras além dos informados.
     * @param array<string> $extraHeaders Cabeçalhos adicionais passados pelo caller.
     * @param int $retryCount Contador interno de retentativas (para erros 5xx).
     */
    private function rawRequest(string $method, string $endpoint, ?array $body = null, bool $authenticated = false, array $extraHeaders = [], int $retryCount = 0): array
    {
        $maxRetries = 3;
        $url = $this->baseUrl . $endpoint;

        $this->logShipay("========== NOVA REQUISIÇÃO ==========");
        $this->logShipay("Método: {$method}");
        $this->logShipay("URL: {$url}");
        $this->logShipay("Ambiente: {$this->environment}");

        $ch = curl_init();

        $headers = array_merge([
            'Content-Type: application/json',
            'Accept: application/json',
        ], $extraHeaders);

        $this->logShipay("Headers: " . json_encode($headers));

        if (!empty($body)) {
            $bodyJson = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            $this->logShipay("Payload: {$bodyJson}");
        }

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => strtoupper($method),
            CURLOPT_TIMEOUT => 30,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 3,
        ]);

        if (!empty($body)) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        $curlErrno = curl_errno($ch);
        $effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
        curl_close($ch);

        $this->logShipay("HTTP Code: {$httpCode}");
        $this->logShipay("Content-Type: {$contentType}");
        $this->logShipay("URL Efetiva: {$effectiveUrl}");
        $this->logShipay("Resposta bruta (completa): " . (string) $response);

        if ($curlError) {
            $this->logShipay("ERRO cURL ({$curlErrno}): {$curlError}");
            throw new Exception("Erro cURL Shipay: {$curlError}");
        }

        if ($response === false || $response === '') {
            $this->logShipay("ERRO: Resposta vazia");
            throw new Exception(sprintf('Shipay retornou resposta vazia (HTTP %s).', $httpCode));
        }

        // Retry automático para erros 5xx (502, 503, 504 - erros temporários do servidor)
        if ($httpCode >= 500 && $httpCode < 600 && $retryCount < $maxRetries) {
            $waitSeconds = ($retryCount + 1) * 2; // 2s, 4s, 6s
            $this->logShipay("Erro {$httpCode} - Tentativa " . ($retryCount + 1) . "/{$maxRetries}. Aguardando {$waitSeconds}s...");
            sleep($waitSeconds);
            return $this->rawRequest($method, $endpoint, $body, $authenticated, $extraHeaders, $retryCount + 1);
        }

        // Verificar se a resposta parece ser HTML (erro de servidor/página não encontrada)
        $trimmedResponse = trim((string) $response);
        $isHtml = (
            stripos($trimmedResponse, '<!DOCTYPE') === 0 ||
            stripos($trimmedResponse, '<html') === 0 ||
            stripos($trimmedResponse, '<!') === 0
        );

        if ($isHtml) {
            $this->logShipay("ERRO: Resposta é HTML, não JSON");
            $this->logShipay("HTML recebido: " . substr($trimmedResponse, 0, 1000));
            throw new Exception(sprintf(
                'Shipay retornou HTML ao invés de JSON (HTTP %s). Endpoint: %s. Verifique se a URL e credenciais estão corretas.',
                $httpCode,
                $endpoint
            ));
        }

        $decoded = json_decode($response, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            $jsonError = json_last_error_msg();
            $this->logShipay("ERRO ao decodificar JSON: {$jsonError}");
            $this->logShipay("Resposta que causou erro: {$response}");

            // Tentar identificar o problema
            $errorDetails = "Erro: {$jsonError}. ";
            if (empty($response)) {
                $errorDetails .= "Resposta vazia.";
            } elseif (strlen($response) < 10) {
                $errorDetails .= "Resposta muito curta: '{$response}'.";
            } else {
                $errorDetails .= "Primeiros 200 chars: " . substr($response, 0, 200);
            }

            throw new Exception("Não foi possível decodificar resposta da Shipay: {$errorDetails}");
        }

        $this->logShipay("Resposta decodificada com sucesso: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));

        if ($httpCode < 200 || $httpCode >= 300) {
            $message = $decoded['message'] ?? $decoded['detail'] ?? $decoded['error'] ?? $decoded['msg'] ?? 'Erro desconhecido';
            $this->logShipay("Erro HTTP {$httpCode}: {$message}");

            // Token expirado -> tenta reautenticar automaticamente.
            if ($httpCode === 401 && $authenticated) {
                $this->logShipay("Token expirado, reautenticando...");
                $this->authenticate(true);
                return $this->request($method, $endpoint, $body);
            }

            throw new Exception(sprintf('Shipay HTTP %s: %s', $httpCode, $message));
        }

        $this->logShipay("========== REQUISIÇÃO CONCLUÍDA COM SUCESSO ==========");
        return $decoded;
    }

    /**
     * Criar uma ordem/cobrança Pix usando o endpoint padrão /order
     * Este é o endpoint mais comum e disponível para todos os clientes Shipay
     */
    public function createPixOrder(array $payload): array
    {
        return $this->request('POST', self::ENDPOINT_ORDER, $payload);
    }

    /**
     * Consultar status de uma ordem
     */
    public function getOrder(string $orderId): array
    {
        // Tentar primeiro o endpoint /order
        try {
            return $this->request('GET', sprintf('%s/%s', self::ENDPOINT_ORDER, urlencode($orderId)));
        } catch (Exception $e) {
            $errorMessage = $e->getMessage();
            // Se o erro indicar que precisa usar /orderv (pedido com expiration_date)
            if (strpos($errorMessage, 'expiration_date') !== false || strpos($errorMessage, '/orderv') !== false || strpos($errorMessage, '400') !== false) {
                $this->logShipay("Erro 400 detectado em getOrder - tentando endpoint /orderv para pedido com expiration_date");
                return $this->request('GET', sprintf('%s/%s', self::ENDPOINT_ORDER_V, urlencode($orderId)));
            }
            throw $e;
        }
    }

    /**
     * Testar conectividade com a API
     */
    public function testConnection(): array
    {
        try {
            $this->authenticate(true);
            return [
                'success' => true,
                'message' => 'Conexão com Shipay estabelecida com sucesso',
                'environment' => $this->environment,
                'base_url' => $this->baseUrl
            ];
        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage(),
                'environment' => $this->environment,
                'base_url' => $this->baseUrl
            ];
        }
    }

    /**
     * Lista as wallets (contas/carteiras) disponíveis para a loja
     *
     * Conforme documentação: GET /v1/wallets
     * https://shipay.readme.io/reference/get_v1-wallets
     */
    public function listWallets(): array
    {
        $this->logShipay("Consultando wallets disponíveis...");
        return $this->request('GET', '/v1/wallets');
    }

    /**
     * Retorna o ambiente atual
     */
    public function getEnvironment(): string
    {
        return $this->environment;
    }
}

