<?php
/**
 * IntegracaoService.php
 *
 * Módulo genérico de integração REST/Webhook com provedores externos
 * parceiros (ex.: painéis IPTV/Sigma e similares) via Bearer Token.
 *
 * Fluxo:
 *   1. Lookup: GET  {dominio}/api/customers?page=1&username={USUARIO}
 *   2. Renew:  POST {dominio}/api/customers/{CUSTOMER_ID}/renew
 *
 * Cada "servidor" cadastrado em db/servidores.json pode opcionalmente ter
 * dominio + bearer_token + package_id_padrao. Quando esses campos existem,
 * o cliente vinculado a esse servidor pode ser sincronizado automaticamente
 * a cada renovação (manual ou via webhook PIX).
 *
 * Nada aqui lança exceção para fora — falhas de integração NUNCA devem
 * quebrar o fluxo principal de renovação/pagamento do Cobranex. Tudo é
 * reportado via array de retorno + log em db/integracoes_log.json.
 */

/**
 * VERSÃO DE DEBUG: 2026-07-30-v2 (com token mascarado no diagnóstico).
 * Se o alerta de erro não mostrar a linha "Token usado: ...", o arquivo
 * rodando no servidor ainda é uma versão antiga — reenviar este arquivo.
 */
define('INTEGRACAO_SERVICE_VERSAO', '2026-07-30-v2');

require_once __DIR__ . '/db_helper.php';

/**
 * Timeout (segundos) para as chamadas cURL ao provedor externo.
 * Curto o suficiente para não travar a resposta do webhook/ação manual.
 */
define('INTEGRACAO_HTTP_TIMEOUT', 12);

/**
 * Executa uma requisição HTTP via cURL e devolve um array padronizado.
 *
 * @return array{ok:bool, http_code:int, body:?array, erro:?string}
 */
function integracao_http_request(string $metodo, string $url, string $bearerToken, ?array $payload = null, ?string $dominio = null): array
{
    if (!function_exists('curl_init')) {
        return ['ok' => false, 'http_code' => 0, 'body' => null, 'erro' => 'Extensão cURL não está habilitada neste servidor PHP.'];
    }

    $ch = curl_init();
    // Alguns provedores (atrás de Cloudflare/WAF) recusam requisições que
    // "parecem" script/cURL puro sem os headers completos de navegador.
    $headers = [
        'Authorization: Bearer ' . $bearerToken,
        'Accept: application/json',
        'Content-Type: application/json',
        'User-Agent: Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36',
        'Sec-Fetch-Dest: empty',
        'Sec-Fetch-Mode: cors',
        'Sec-Fetch-Site: same-origin',
        'Accept-Language: pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
        // Headers específicos do painel Sigma (não são "enfeite" de
        // navegador — vieram do DevTools de uma chamada real que retornou
        // 200 OK, então o backend pode validar isso).
        'X-App-Version: 3.86',
        'Locale: pt',
    ];
    if ($dominio) {
        $headers[] = 'Referer: https://' . $dominio . '/';
        $headers[] = 'Origin: https://' . $dominio;
    }

    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 6,
        CURLOPT_TIMEOUT => 15,
        // Verificação de SSL desativada a pedido — ver observação de
        // segurança no topo do arquivo.
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => 0,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_CUSTOMREQUEST => strtoupper($metodo),
        CURLOPT_HTTPHEADER => $headers,
        // Habilita o motor de cookies em memória do cURL (mantém cookies
        // entre a chamada de lookup e a de renovação, caso o provedor
        // dependa de sessão) e aceita respostas comprimidas (gzip/deflate).
        CURLOPT_COOKIEFILE => '',
        CURLOPT_ENCODING => '',
    ]);

    if ($payload !== null) {
        $json = json_encode($payload, JSON_UNESCAPED_UNICODE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
    }

    $respostaBruta = curl_exec($ch);
    $erroCurl = curl_error($ch);
    $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($respostaBruta === false) {
        return ['ok' => false, 'http_code' => $httpCode, 'body' => null, 'erro' => 'Falha de conexão: ' . $erroCurl];
    }

    $decodificado = json_decode($respostaBruta, true);
    $corpoValido = (json_last_error() === JSON_ERROR_NONE) ? $decodificado : null;

    if ($httpCode < 200 || $httpCode >= 300) {
        return [
            'ok' => false,
            'http_code' => $httpCode,
            'body' => $corpoValido,
            'erro' => 'O provedor respondeu HTTP ' . $httpCode . '.',
        ];
    }

    return ['ok' => true, 'http_code' => $httpCode, 'body' => $corpoValido, 'erro' => null];
}

/**
 * Normaliza o domínio digitado pelo usuário para uso em URLs HTTPS:
 * remove "http://"/"https://" (com ou sem "www." mantido como está),
 * remove barras finais e qualquer espaço em volta. Também remove
 * qualquer caminho que tenha sido colado junto (fica só host[:porta]).
 *
 * Exemplos:
 *   "https://goldplay.sigma.vin/"   -> "goldplay.sigma.vin"
 *   "http://goldplay.sigma.vin"     -> "goldplay.sigma.vin"
 *   "goldplay.sigma.vin/algo/"      -> "goldplay.sigma.vin"
 *   "  goldplay.sigma.vin  "        -> "goldplay.sigma.vin"
 */
function integracao_normalizar_dominio(string $dominioBruto): string
{
    $dominio = trim($dominioBruto);
    // Remove o esquema (http:// ou https://), se presente.
    $dominio = preg_replace('#^https?://#i', '', $dominio);
    // Remove qualquer caminho/query que tenha vindo junto, ficando só host[:porta].
    $dominio = explode('/', $dominio, 2)[0];
    $dominio = explode('?', $dominio, 2)[0];
    return trim($dominio, "/ \t\n\r\0\x0B");
}

/**
 * Mascara um token para exibir em mensagens de diagnóstico sem expor o
 * valor completo — mostra só os primeiros/últimos 4 caracteres e o
 * tamanho total, o suficiente para comparar com o token real.
 */
function integracao_mascarar_token(string $token): string
{
    $tamanho = strlen($token);
    if ($tamanho <= 8) {
        return str_repeat('*', $tamanho);
    }
    return substr($token, 0, 4) . str_repeat('*', $tamanho - 8) . substr($token, -4) . " (tamanho: {$tamanho})";
}

/**
 * Etapa 1: busca o ID interno do cliente no provedor externo pelo username.
 *
 * @return array{ok:bool, customer_id:?string, erro:?string}
 */
function integracao_buscar_id_cliente(array $servidor, string $usernameExterno): array
{
    $dominio = integracao_normalizar_dominio($servidor['dominio'] ?? '');
    $token = trim($servidor['bearer_token'] ?? '');

    if ($dominio === '' || $token === '') {
        return ['ok' => false, 'customer_id' => null, 'erro' => 'Servidor sem domínio/token de integração configurado.'];
    }
    if ($usernameExterno === '') {
        return ['ok' => false, 'customer_id' => null, 'erro' => 'Cliente sem "usuário externo" configurado para integração.'];
    }

    // O painel Sigma exige a query string completa (mesmos parâmetros que o
    // próprio painel web envia) — mesmo vazios, os parâmetros extras
    // precisam estar presentes ou a API responde 404.
    $url = 'https://' . $dominio . '/api/customers'
        . '?page=1'
        . '&username=' . rawurlencode($usernameExterno)
        . '&serverId='
        . '&packageId='
        . '&expiryFrom='
        . '&expiryTo='
        . '&createdFrom='
        . '&createdTo='
        . '&status='
        . '&isTrial='
        . '&connections='
        . '&perPage=10';
    $resposta = integracao_http_request('GET', $url, $token, null, $dominio);

    // Corpo bruto (truncado) da resposta, para diagnóstico — usado tanto no
    // caso de erro quanto de sucesso quando o retorno não bate o esperado.
    $bodyDebug = $resposta['body'] !== null
        ? json_encode($resposta['body'], JSON_UNESCAPED_UNICODE)
        : '(resposta não é um JSON válido ou veio vazia)';
    if (strlen($bodyDebug) > 500) {
        $bodyDebug = substr($bodyDebug, 0, 500) . '... (truncado)';
    }

    if (!$resposta['ok']) {
        return [
            'ok' => false,
            'customer_id' => null,
            'erro' => 'Falha na integração (HTTP ' . $resposta['http_code'] . ") [core v" . INTEGRACAO_SERVICE_VERSAO . "].\nURL chamada: {$url}\nToken usado: " . integracao_mascarar_token($token) . "\nID Encontrado: (vazio)\nResposta API: {$bodyDebug}",
            'debug' => ['url' => $url, 'http_code' => $resposta['http_code'], 'body' => $resposta['body']],
        ];
    }

    // A API pode devolver o cliente em formatos ligeiramente diferentes
    // conforme o provedor (lista direta, ou dentro de "data"/"customers").
    $corpo = $resposta['body'];
    $lista = null;
    if (is_array($corpo)) {
        if (isset($corpo['data']) && is_array($corpo['data'])) {
            $lista = $corpo['data'];
        } elseif (isset($corpo['customers']) && is_array($corpo['customers'])) {
            $lista = $corpo['customers'];
        } elseif (isset($corpo[0])) {
            // Lista simples (array indexado numericamente a partir de 0),
            // sem usar array_is_list() para manter compatibilidade com PHP 8.0.
            $lista = $corpo;
        }
    }

    if (!$lista || count($lista) === 0) {
        return [
            'ok' => false,
            'customer_id' => null,
            'erro' => "Falha na integração (HTTP {$resposta['http_code']}) [core v" . INTEGRACAO_SERVICE_VERSAO . "].\nURL chamada: {$url}\nToken usado: " . integracao_mascarar_token($token) . "\nID Encontrado: (vazio)\nResposta API: {$bodyDebug}\nMotivo: cliente \"{$usernameExterno}\" não encontrado no provedor.",
            'debug' => ['url' => $url, 'http_code' => $resposta['http_code'], 'body' => $resposta['body']],
        ];
    }

    $primeiro = $lista[0];
    $customerId = $primeiro['id'] ?? $primeiro['customer_id'] ?? null;

    if (!$customerId) {
        return [
            'ok' => false,
            'customer_id' => null,
            'erro' => "Falha na integração (HTTP {$resposta['http_code']}) [core v" . INTEGRACAO_SERVICE_VERSAO . "].\nURL chamada: {$url}\nToken usado: " . integracao_mascarar_token($token) . "\nID Encontrado: (vazio)\nResposta API: {$bodyDebug}\nMotivo: resposta do provedor não trouxe um ID de cliente válido.",
            'debug' => ['url' => $url, 'http_code' => $resposta['http_code'], 'body' => $resposta['body']],
        ];
    }

    return ['ok' => true, 'customer_id' => (string) $customerId, 'erro' => null, 'debug' => ['url' => $url, 'http_code' => $resposta['http_code'], 'body' => $resposta['body']]];
}

/**
 * Etapa 2: dispara o gatilho de renovação no provedor externo.
 *
 * @return array{ok:bool, erro:?string, resposta:?array}
 */
function integracao_renovar_no_provedor(array $servidor, string $customerId, ?string $packageId = null, int $conexoes = 1): array
{
    $dominio = integracao_normalizar_dominio($servidor['dominio'] ?? '');
    $token = trim($servidor['bearer_token'] ?? '');

    if ($dominio === '' || $token === '') {
        return ['ok' => false, 'erro' => 'Servidor sem domínio/token de integração configurado.', 'resposta' => null];
    }

    $pacote = $packageId ?: ($servidor['package_id_padrao'] ?? '');

    $url = 'https://' . $dominio . '/api/customers/' . rawurlencode($customerId) . '/renew';
    $payload = [
        'package_id' => $pacote,
        'connections' => max(1, $conexoes),
    ];

    $resposta = integracao_http_request('POST', $url, $token, $payload, $dominio);

    $bodyDebug = $resposta['body'] !== null
        ? json_encode($resposta['body'], JSON_UNESCAPED_UNICODE)
        : '(resposta não é um JSON válido ou veio vazia)';
    if (strlen($bodyDebug) > 500) {
        $bodyDebug = substr($bodyDebug, 0, 500) . '... (truncado)';
    }

    if (!$resposta['ok']) {
        return [
            'ok' => false,
            'erro' => "Falha na integração (HTTP {$resposta['http_code']}).\nURL chamada (renew): {$url}\nID Encontrado: {$customerId}\nResposta API: {$bodyDebug}",
            'resposta' => $resposta['body'],
            'debug' => ['url' => $url, 'http_code' => $resposta['http_code'], 'body' => $resposta['body']],
        ];
    }

    return ['ok' => true, 'erro' => null, 'resposta' => $resposta['body'], 'debug' => ['url' => $url, 'http_code' => $resposta['http_code'], 'body' => $resposta['body']]];
}

/**
 * Ponto de entrada único: recebe o cliente (array do Cobranex) e o servidor
 * vinculado a ele, executa lookup + renovação, e registra o resultado em
 * log. Retorna sempre um array (nunca lança exceção) para não interromper
 * o fluxo de pagamento/renovação que a chamou.
 *
 * @return array{ok:bool, motivo:string, detalhes:?array}
 */
function integracao_sincronizar_renovacao_cliente(array $cliente, ?string $origem = 'manual'): array
{
    $nomeServidor = $cliente['servidor'] ?? '';
    $usernameExterno = trim($cliente['usuario_externo'] ?? '');
    $conexoes = (int) ($cliente['conexoes'] ?? 1);
    if ($conexoes <= 0) {
        $conexoes = 1;
    }

    // Sem servidor vinculado: não é erro, apenas não há o que sincronizar.
    if ($nomeServidor === '') {
        return ['ok' => true, 'motivo' => 'Cliente sem servidor vinculado — nada a sincronizar.', 'detalhes' => null];
    }

    $servidores = db_read('servidores');
    $servidor = null;
    foreach ($servidores as $s) {
        if (($s['nome'] ?? '') === $nomeServidor) {
            $servidor = $s;
            break;
        }
    }

    if (!$servidor) {
        return ['ok' => false, 'motivo' => 'Servidor "' . $nomeServidor . '" não encontrado no cadastro.', 'detalhes' => null];
    }

    // Servidor sem configuração de integração: silenciosamente ignora (é
    // um servidor "só de estoque/hospedagem", sem parceiro de API).
    if (empty($servidor['dominio']) || empty($servidor['bearer_token'])) {
        return ['ok' => true, 'motivo' => 'Servidor sem integração externa configurada — nada a sincronizar.', 'detalhes' => null];
    }

    if ($usernameExterno === '') {
        $resultado = ['ok' => false, 'motivo' => 'Cliente sem "usuário externo" configurado — não é possível sincronizar.', 'detalhes' => null];
        integracao_registrar_log($cliente, $servidor, $origem, $resultado);
        return $resultado;
    }

    $lookup = integracao_buscar_id_cliente($servidor, $usernameExterno);
    if (!$lookup['ok'] || empty($lookup['customer_id'])) {
        // Mensagem clara e específica quando o lookup não retorna um ID
        // válido — evita chamar o POST /renew com um customerId vazio.
        $motivoClaro = "Usuário \"{$usernameExterno}\" não foi encontrado no servidor via API.";
        $detalhesTecnicos = $lookup['erro'] ?? '';
        $resultado = [
            'ok' => false,
            'motivo' => $motivoClaro . ($detalhesTecnicos ? "\n\n[Detalhes técnicos]\n" . $detalhesTecnicos : ''),
            'detalhes' => ['etapa' => 'lookup', 'debug' => $lookup['debug'] ?? null],
        ];
        integracao_registrar_log($cliente, $servidor, $origem, $resultado);
        return $resultado;
    }

    $renovacao = integracao_renovar_no_provedor($servidor, $lookup['customer_id'], $cliente['package_id'] ?? null, $conexoes);
    if (!$renovacao['ok']) {
        $resultado = ['ok' => false, 'motivo' => $renovacao['erro'], 'detalhes' => ['etapa' => 'renew', 'customer_id' => $lookup['customer_id'], 'debug' => $renovacao['debug'] ?? null]];
        integracao_registrar_log($cliente, $servidor, $origem, $resultado);
        return $resultado;
    }

    $resultado = [
        'ok' => true,
        'motivo' => 'Renovação sincronizada com sucesso no provedor externo.',
        'detalhes' => ['customer_id' => $lookup['customer_id'], 'conexoes' => $conexoes],
    ];
    integracao_registrar_log($cliente, $servidor, $origem, $resultado);
    return $resultado;
}

/**
 * Registra o resultado da tentativa de sincronização em
 * db/integracoes_log.json (histórico independente da auditoria geral, com
 * os detalhes técnicos da chamada) e também na auditoria padrão.
 */
function integracao_registrar_log(array $cliente, array $servidor, ?string $origem, array $resultado): void
{
    db_update('integracoes_log', function (&$log) use ($cliente, $servidor, $origem, $resultado) {
        $log[] = [
            'id' => gerar_id('INT-'),
            'cliente_id' => $cliente['id'] ?? null,
            'cliente_nome' => $cliente['nome'] ?? null,
            'servidor' => $servidor['nome'] ?? null,
            'origem' => $origem ?? 'manual', // manual | pix_automatico
            'ok' => $resultado['ok'],
            'motivo' => $resultado['motivo'],
            'detalhes' => $resultado['detalhes'],
            'data' => date('Y-m-d H:i:s'),
        ];
        // mantém só os últimos 2000 registros
        if (count($log) > 2000) {
            $log = array_slice($log, -2000);
        }
    }, []);

    $usuarioAuditoria = $resultado['ok'] ? 'INTEGRACAO_EXTERNA_OK' : 'INTEGRACAO_EXTERNA_FALHOU';
    auditoria_log('SISTEMA', $usuarioAuditoria, ($cliente['id'] ?? '') . ' | ' . ($servidor['nome'] ?? '') . ' | ' . $resultado['motivo']);
}
