<?php
namespace Modules\Payments\Controllers;

use Gestoreasy\Core\Http\Request;
use Gestoreasy\Core\Http\Response;
use Gestoreasy\Core\Tenant\ConnectionManager;
use Gestoreasy\Core\Tenant\TenantContext;
// Ambiente não deve fornecer segredos de bancos; usar apenas configurações do sistema
use Gestoreasy\Core\Security\Crypto;

class WebhookController {
    /** Healthcheck simples para provedores que validam URL */
    public function ping(Request $req): Response {
        $provider = $this->providerFromReq($req);
        return Response::json(['ok' => true, 'provider' => $provider, 'status' => 'ready']);
    }

    /** Último evento recebido para exibir na UI */
    public function last(Request $req): Response {
        $provider = $this->providerFromReq($req);
        if ($provider === 'unknown') {
            return Response::json(['error' => 'provider inválido'], 422);
        }
        $dir = __DIR__ . '/../../../storage/tmp/webhooks/' . $provider;
        $last = null;
        if (is_dir($dir)) {
            $files = @glob($dir . '/event_*.json');
            if (is_array($files) && count($files) > 0) {
                usort($files, function($a,$b){ return filemtime($b) <=> filemtime($a); });
                $lastFile = $files[0];
                $last = date('c', @filemtime($lastFile) ?: time());
            }
        }
        return Response::json(['provider' => $provider, 'last_received_at' => $last]);
    }

    /** Simula um evento de webhook autenticado para testes internos */
    public function simulate(Request $req): Response {
        $provider = $this->providerFromReq($req);
        if ($provider === 'unknown') {
            return Response::json(['error' => 'provider inválido'], 422);
        }
        $tenant = (string)($req->query['tenant'] ?? '') ?: TenantContext::getTenantId();
        $payload = $req->getJsonBody();
        if (!$payload || !is_array($payload)) {
            // payload padrão por provedor
            $payload = match ($provider) {
                'pagarme' => ['event' => 'charge.paid', 'type' => 'charge', 'charge' => ['id' => 'sim_pg_' . time(), 'status' => 'paid', 'amount' => 12345]],
                'mercadopago' => ['type' => 'payment', 'data' => ['id' => 'sim_mp_' . time(), 'status' => 'approved', 'amount' => 123.45]],
                'pagbank' => ['event' => 'payment', 'payment' => ['id' => 'sim_pb_' . time(), 'status' => 'paid', 'amount' => ['value' => 123.45]]],
                'asaas' => ['event' => ['id' => 'sim_as_' . time()], 'payment' => ['id' => 'sim_as_' . time(), 'status' => 'RECEIVED', 'value' => 123.45]],
                'itau' => ['pix' => [['endToEndId' => 'sim_itau_' . time(), 'txid' => 'simtx_' . substr(uniqid(),-6), 'valor' => '123.45', 'horario' => date('c')]], 'status' => 'received'],
                default => ['type' => 'payment', 'status' => 'received', 'amount' => 0],
            };
        }

        // Reutiliza o mesmo fluxo de persistência e conciliação da handle(), sem exigir assinatura
        $dir = __DIR__ . '/../../../storage/tmp/webhooks/' . $provider;
        if (!is_dir($dir)) { @mkdir($dir, 0775, true); }
        $eventId = $this->extractEventId($provider, $payload, $req) ?: ('sim_' . substr(sha1(json_encode($payload)), 0, 10));
        $flagFile = $dir . '/processed_' . $eventId . '.flag';
        if (is_file($flagFile)) {
            @unlink($flagFile); // permitir reprocessar simulações
        }
        $rawPath = $dir . '/event_' . $eventId . '.json';
        @file_put_contents($rawPath, json_encode(['received_at' => date('c'),'tenant'=> $tenant,'headers' => ['simulate' => true],'body' => $payload], JSON_UNESCAPED_UNICODE));

        $pdo = ConnectionManager::forTenant($tenant);
        $this->ensurePaymentsTable($pdo);
        $mapped = $this->mapEvent($provider, $payload);
        $stmt = $pdo->prepare('INSERT INTO payments (tenant_id, amount_cents, status, occurred_at, provider, external_id, notes) VALUES (?,?,?,?,?,?,?)');
        $stmt->execute([$tenant,(int)($mapped['amount_cents'] ?? 0),(string)($mapped['status'] ?? 'received'),(string)($mapped['occurred_at'] ?? date('Y-m-d H:i:s')),(string)$provider,(string)($mapped['external_id'] ?? $eventId),json_encode(['event_type' => $mapped['event_type'] ?? 'simulate'], JSON_UNESCAPED_UNICODE)]);
        $paymentId = (int)$pdo->lastInsertId();

        // Conciliação
        try {
            $movementType = $this->movementTypeFromStatus($provider, (string)($mapped['status'] ?? ''));
            $amount = max(0.0, ((int)($mapped['amount_cents'] ?? 0)) / 100.0);
            if ($movementType && $amount > 0) {
                $targetId = $this->loadTargetAccountId($provider, $tenant);
                $acc = null;
                if ($targetId) {
                    $stAcc = $pdo->prepare("SELECT id, saldo, status FROM bank_accounts WHERE id = ? LIMIT 1");
                    $stAcc->execute([$targetId]);
                    $acc = $stAcc->fetch(\PDO::FETCH_ASSOC) ?: null;
                }
                if (!$acc) {
                    $stAcc = $pdo->query("SELECT id, saldo, status FROM bank_accounts WHERE status = 'ativa' ORDER BY id ASC LIMIT 1");
                    $acc = $stAcc ? $stAcc->fetch(\PDO::FETCH_ASSOC) : null;
                }
                if ($acc && isset($acc['id']) && $acc['status'] === 'ativa') {
                    $contaId = (int)$acc['id'];
                    $saldoAtual = (float)($acc['saldo'] ?? 0.0);
                    $novoSaldo = $movementType === 'entrada' ? ($saldoAtual + $amount) : ($saldoAtual - $amount);
                    $dataMov = (string)($mapped['occurred_at'] ?? date('Y-m-d H:i:s'));
                    $dataDia = substr($dataMov, 0, 10);
                    $descricao = sprintf('Simulado %s %s %s', $provider, (string)($mapped['event_type'] ?? 'payment'), (string)($mapped['external_id'] ?? $eventId));
                    $insMov = $pdo->prepare('INSERT INTO bank_transactions (conta_id, data, descricao, tipo, valor, saldo, source_type, source_id, created_at) VALUES (?,?,?,?,?,?,?,?, NOW())');
                    $insMov->execute([$contaId, $dataDia, $descricao, $movementType, $amount, $novoSaldo, 'payments', $paymentId]);
                    $upd = $pdo->prepare('UPDATE bank_accounts SET saldo = ?, updated_at = NOW() WHERE id = ?');
                    $upd->execute([$novoSaldo, $contaId]);
                }
            }
        } catch (\Throwable $e) { error_log('payments_webhook_simulate_conciliation_error: ' . $e->getMessage()); }

        @file_put_contents($flagFile, 'ok');
        return Response::json(['ok' => true, 'simulated' => true, 'provider' => $provider, 'event_id' => $eventId]);
    }

    /** Entrada unificada de webhooks: mercadopago, pagbank, asaas */
    public function handle(Request $req): Response {
        $provider = $this->providerFromReq($req);
        $tenant = (string)($req->query['tenant'] ?? '') ?: TenantContext::getTenantId();
        $payload = $req->getJsonBody();

        // Verificação obrigatória de segredo via configurações do tenant no DB
        $expectedSecret = $this->loadWebhookSecret($provider, $tenant);
        if ($expectedSecret === '') {
            return Response::json(['error' => 'missing_secret', 'provider' => $provider], 401);
        }
        $sigHeader = $this->signatureHeader($provider, $req);
        $rawBody = (string)($req->get('raw_body') ?? '');
        if ($expectedSecret !== '' && !$this->verifySignature($provider, $expectedSecret, $sigHeader, $payload, $rawBody)) {
            return Response::json(['error' => 'invalid_signature', 'provider' => $provider], 401);
        }

        // Garantir diretório de armazenamento do raw event
        $dir = __DIR__ . '/../../../storage/tmp/webhooks/' . $provider;
        if (!is_dir($dir)) { @mkdir($dir, 0775, true); }

        // Idempotência: usar chave do provedor ou fallback com hash do corpo
        $eventId = $this->extractEventId($provider, $payload, $req);
        $idemKey = $eventId ?: 'hash_' . substr(sha1(json_encode($payload)), 0, 24);
        $flagFile = $dir . '/processed_' . $idemKey . '.flag';
        if (is_file($flagFile)) {
            // Já processado: responder 200 para evitar reentrega contínua
            return Response::json(['ok' => true, 'duplicate' => true]);
        }

        // Persistir evento bruto para auditoria
        $rawPath = $dir . '/event_' . $idemKey . '.json';
        @file_put_contents($rawPath, json_encode([
            'received_at' => date('c'),
            'tenant' => $tenant,
            'headers' => $req->headers,
            'body' => $payload,
            'raw' => $rawBody ?? null,
            'signature_header' => $sigHeader
        ], JSON_UNESCAPED_UNICODE));

        // Mapear para estrutura interna mínima e registrar em tabela payments do tenant
        $pdo = ConnectionManager::forTenant($tenant);
        $this->ensurePaymentsTable($pdo);

        $mapped = $this->mapEvent($provider, $payload);
        $externalId = (string)($mapped['external_id'] ?? $idemKey);
        $stmt = $pdo->prepare('INSERT INTO payments (tenant_id, amount_cents, status, occurred_at, provider, external_id, notes) VALUES (?,?,?,?,?,?,?)');
        try {
            $stmt->execute([
                $tenant,
                (int)($mapped['amount_cents'] ?? 0),
                (string)($mapped['status'] ?? 'received'),
                (string)($mapped['occurred_at'] ?? date('Y-m-d H:i:s')),
                (string)$provider,
                $externalId,
                json_encode(['event_type' => $mapped['event_type'] ?? null], JSON_UNESCAPED_UNICODE)
            ]);
            $paymentId = (int)$pdo->lastInsertId();
        } catch (\PDOException $e) {
            // Se já existir (único por provedor+external_id), reaproveitar o ID
            if ($e->getCode() === '23000') {
                $st = $pdo->prepare('SELECT id FROM payments WHERE provider = ? AND external_id = ? ORDER BY id DESC LIMIT 1');
                $st->execute([(string)$provider, $externalId]);
                $row = $st->fetch(\PDO::FETCH_ASSOC);
                $paymentId = (int)($row['id'] ?? 0);
            } else { throw $e; }
        }

        // Conciliação automática básica em Bancos: refletir entrada/saída conforme status
        try {
            $movementType = $this->movementTypeFromStatus($provider, (string)($mapped['status'] ?? ''));
            $amount = max(0.0, ((int)($mapped['amount_cents'] ?? 0)) / 100.0);
            if ($movementType && $amount > 0) {
                // Preferir conta destino configurada para o provedor; fallback para primeira ativa
                $targetId = $this->loadTargetAccountId($provider, $tenant);
                $acc = null;
                if ($targetId) {
                    $stAcc = $pdo->prepare("SELECT id, saldo, status FROM bank_accounts WHERE id = ? LIMIT 1");
                    $stAcc->execute([$targetId]);
                    $acc = $stAcc->fetch(\PDO::FETCH_ASSOC) ?: null;
                }
                if (!$acc) {
                    $stAcc = $pdo->query("SELECT id, saldo, status FROM bank_accounts WHERE status = 'ativa' ORDER BY id ASC LIMIT 1");
                    $acc = $stAcc ? $stAcc->fetch(\PDO::FETCH_ASSOC) : null;
                }
                if ($acc && isset($acc['id']) && $acc['status'] === 'ativa') {
                    $contaId = (int)$acc['id'];
                    $saldoAtual = (float)($acc['saldo'] ?? 0.0);
                    $novoSaldo = $movementType === 'entrada' ? ($saldoAtual + $amount) : ($saldoAtual - $amount);
                    $dataMov = (string)($mapped['occurred_at'] ?? date('Y-m-d H:i:s'));
                    $dataDia = substr($dataMov, 0, 10);
                    $descricao = sprintf('Webhook %s %s %s', $provider, (string)($mapped['event_type'] ?? 'payment'), (string)($mapped['external_id'] ?? $idemKey));
                    $insMov = $pdo->prepare('INSERT INTO bank_transactions (conta_id, data, descricao, tipo, valor, saldo, source_type, source_id, created_at) VALUES (?,?,?,?,?,?,?,?, NOW())');
                    $insMov->execute([$contaId, $dataDia, $descricao, $movementType, $amount, $novoSaldo, 'payments', $paymentId]);
                    $upd = $pdo->prepare('UPDATE bank_accounts SET saldo = ?, updated_at = NOW() WHERE id = ?');
                    $upd->execute([$novoSaldo, $contaId]);
                }
            }
        } catch (\Throwable $e) {
            // Não interromper o fluxo do webhook por falha de conciliação
            error_log('payments_webhook_conciliation_error: ' . $e->getMessage());
        }

        // Marcar como processado (idempotência)
        @file_put_contents($flagFile, 'ok');

        return Response::json(['ok' => true]);
    }

    /** Determina tipo de movimentação a partir do status do provedor */
    private function movementTypeFromStatus(string $provider, string $status): ?string {
        $s = strtolower($status);
        if ($provider === 'pagarme') {
            if (in_array($s, ['paid','paid_with_boleto','paid_with_pix','authorized','captured'], true)) return 'entrada';
            if (in_array($s, ['refunded','canceled','charge_refunded'], true)) return 'saida';
        }
        if ($provider === 'mercadopago') {
            if (in_array($s, ['approved','authorized'], true)) return 'entrada';
            if (in_array($s, ['refunded','cancelled','charged_back'], true)) return 'saida';
        }
        if ($provider === 'pagbank') {
            if (in_array($s, ['paid','authorized','available'], true)) return 'entrada';
            if (in_array($s, ['refunded','canceled','charge_refunded'], true)) return 'saida';
        }
        if ($provider === 'asaas') {
            if (in_array($s, ['RECEIVED','CONFIRMED'], true)) return 'entrada';
            if (in_array($s, ['REFUNDED','CANCELLED'], true)) return 'saida';
        }
        if ($provider === 'itau') {
            if (in_array($s, ['received','confirmed','liquidado','creditado'], true)) return 'entrada';
            if (in_array($s, ['refunded','devolvido','estornado','canceled'], true)) return 'saida';
        }
        // Status não mapeado: não movimenta
        return null;
    }

    private function providerFromReq(Request $req): string {
        $params = (array)$req->get('params');
        $p = strtolower((string)($params[0] ?? ''));
        // aceitar alias 'pg' como 'pagarme'
        if ($p === 'pg') { $p = 'pagarme'; }
        return in_array($p, ['mercadopago','pagbank','asaas','pagarme','itau'], true) ? $p : 'unknown';
    }

    // Removido: resolução de segredos via variáveis de ambiente

    /**
     * Carrega o segredo de webhook criptografado por tenant.
     * - Asaas: reaproveita tabela asaas_settings
     * - Demais provedores: usa tabela genérica payment_provider_settings
     */
    private function loadWebhookSecret(string $provider, ?string $tenantId = null): string {
        try {
            $pdo = ConnectionManager::forTenant($tenantId ?: null);
            if ($provider === 'asaas') {
                // Usa a configuração do addon Asaas
                $stmt = $pdo->query('SELECT webhook_secret_enc, webhook_secret_iv, webhook_secret_tag, webhook_secret_salt FROM asaas_settings ORDER BY id DESC LIMIT 1');
                $row = $stmt->fetch(\PDO::FETCH_ASSOC);
                if ($row && !empty($row['webhook_secret_enc'])) {
                    try {
                        $data = Crypto::decryptJson($row['webhook_secret_enc'], $row['webhook_secret_iv'], $row['webhook_secret_tag'], $row['webhook_secret_salt']);
                        return (string)($data['webhook_secret'] ?? '');
                    } catch (\Throwable $e) { /* ignore */ }
                }
                return '';
            }

            // Demais provedores: tabela genérica por tenant
            $this->ensureProviderSettingsTable($pdo);
            $stmt = $pdo->prepare('SELECT webhook_secret_enc, webhook_secret_iv, webhook_secret_tag, webhook_secret_salt FROM payment_provider_settings WHERE provider = ? ORDER BY id DESC LIMIT 1');
            $stmt->execute([$provider]);
            $row = $stmt->fetch(\PDO::FETCH_ASSOC);
            if ($row && !empty($row['webhook_secret_enc'])) {
                try {
                    $data = Crypto::decryptJson($row['webhook_secret_enc'], $row['webhook_secret_iv'], $row['webhook_secret_tag'], $row['webhook_secret_salt']);
                    return (string)($data['webhook_secret'] ?? '');
                } catch (\Throwable $e) { /* ignore */ }
            }
            return '';
        } catch (\Throwable $e) {
            return '';
        }
    }

    private function ensureProviderSettingsTable(\PDO $pdo): void {
        try {
            $pdo->exec('CREATE TABLE IF NOT EXISTS payment_provider_settings (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                provider VARCHAR(64) NOT NULL,
                api_key_enc BLOB NULL,
                api_key_iv BLOB NULL,
                api_key_tag BLOB NULL,
                api_key_salt BLOB NULL,
                webhook_secret_enc BLOB NULL,
                webhook_secret_iv BLOB NULL,
                webhook_secret_tag BLOB NULL,
                webhook_secret_salt BLOB NULL,
                target_account_id INT NULL,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                UNIQUE KEY uniq_provider (provider)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
            $pdo->exec('ALTER TABLE payment_provider_settings ADD COLUMN IF NOT EXISTS target_account_id INT NULL');
        } catch (\Throwable $e) { /* ignore */ }
    }

    private function loadTargetAccountId(string $provider, ?string $tenantId = null): ?int {
        try {
            $pdo = ConnectionManager::forTenant($tenantId ?: null);
            $this->ensureProviderSettingsTable($pdo);
            $stmt = $pdo->prepare('SELECT target_account_id FROM payment_provider_settings WHERE provider = ? ORDER BY id DESC LIMIT 1');
            $stmt->execute([$provider]);
            $row = $stmt->fetch(\PDO::FETCH_ASSOC);
            if ($row && isset($row['target_account_id'])) {
                $v = (int)$row['target_account_id'];
                return $v > 0 ? $v : null;
            }
        } catch (\Throwable $e) { /* ignore */ }
        return null;
    }

    // Removido: fallback de segredos via ambiente

    private function signatureHeader(string $provider, Request $req): ?string {
        $hdrs = $req->headers ?? [];
        return match ($provider) {
            'mercadopago' => ($hdrs['x-signature'] ?? null) ?: ($hdrs['x-mp-signature'] ?? null),
            'pagbank' => ($hdrs['x-signature'] ?? null) ?: ($hdrs['x-pagbank-signature'] ?? null),
            'asaas' => ($hdrs['x-asaas-signature'] ?? null) ?: ($hdrs['x-signature'] ?? null),
            'pagarme' => ($hdrs['x-hub-signature-256'] ?? null) ?: ($hdrs['x-hub-signature'] ?? null),
            'itau' => ($hdrs['x-itau-hmac'] ?? null) ?: ($hdrs['x-signature'] ?? null),
            default => null,
        };
    }

    private function verifySignature(string $provider, string $secret, ?string $signature, array $payload, ?string $rawBody = null): bool {
        // Implementação genérica: HMAC do corpo JSON com segredo compartilhado
        if (!$signature) return false;
        $body = ($rawBody !== null && $rawBody !== '')
            ? (string)$rawBody
            : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $sig = (string)$signature;
        // suportar formatos "sha256=<hex>" ou "sha1=<hex>" usados por alguns provedores
        $algo = 'sha256';
        if (str_starts_with($sig, 'sha256=')) { $algo = 'sha256'; $sig = substr($sig, 7); }
        elseif (str_starts_with($sig, 'sha1=')) { $algo = 'sha1'; $sig = substr($sig, 5); }
        $calc = hash_hmac($algo, (string)$body, $secret);
        return hash_equals($calc, $sig);
    }

    private function extractEventId(string $provider, array $payload, Request $req): ?string {
        return match ($provider) {
            'mercadopago' => (string)($payload['id'] ?? $payload['data']['id'] ?? ($req->headers['x-idempotency-key'] ?? '')),
            'pagbank' => (string)($payload['id'] ?? ($req->headers['x-idempotency-key'] ?? '')),
            'asaas' => (string)($payload['event']['id'] ?? ($req->headers['x-id'] ?? '')),
            'pagarme' => (string)($payload['charge']['id'] ?? $payload['order']['id'] ?? $payload['data']['id'] ?? ($req->headers['x-idempotency-key'] ?? '')),
            'itau' => (string)($payload['pix'][0]['endToEndId'] ?? $payload['pix'][0]['e2eid'] ?? $payload['pix'][0]['txid'] ?? ($req->headers['x-idempotency-key'] ?? '')),
            default => null,
        } ?: null;
    }

    private function mapEvent(string $provider, array $payload): array {
        if ($provider === 'mercadopago') {
            $type = (string)($payload['type'] ?? $payload['action'] ?? 'payment');
            $status = (string)($payload['data']['status'] ?? $payload['status'] ?? 'received');
            $amount = (float)($payload['data']['amount'] ?? $payload['amount'] ?? 0.0);
            $ext = (string)($payload['data']['id'] ?? $payload['id'] ?? '');
            return [
                'event_type' => $type,
                'status' => $status,
                'amount_cents' => (int)round($amount * 100),
                'external_id' => $ext,
            ];
        }
        if ($provider === 'pagbank') {
            $type = (string)($payload['event'] ?? 'payment');
            $status = (string)($payload['payment']['status'] ?? $payload['status'] ?? 'received');
            $amount = (float)($payload['payment']['amount']['value'] ?? $payload['amount'] ?? 0.0);
            $ext = (string)($payload['payment']['id'] ?? $payload['id'] ?? '');
            return [
                'event_type' => $type,
                'status' => $status,
                'amount_cents' => (int)round($amount * 100),
                'external_id' => $ext,
            ];
        }
        if ($provider === 'pagarme') {
            // Estrutura flexível cobrindo eventos de order/charge
            $type = (string)($payload['type'] ?? $payload['event'] ?? 'payment');
            // status pode estar em charge, order ou data
            $status = (string)($payload['charge']['status'] ?? $payload['order']['status'] ?? $payload['data']['status'] ?? $payload['status'] ?? 'received');
            // amount pode estar em centavos (amount) ou value
            $amount = 0.0;
            if (isset($payload['charge']['amount'])) {
                $amount = ((int)$payload['charge']['amount']) / 100.0;
            } elseif (isset($payload['order']['amount'])) {
                $amount = ((int)$payload['order']['amount']) / 100.0;
            } elseif (isset($payload['data']['amount'])) {
                $amount = (float)$payload['data']['amount'];
            } elseif (isset($payload['amount']['value'])) {
                $amount = (float)$payload['amount']['value'];
            }
            $ext = (string)($payload['charge']['id'] ?? $payload['order']['id'] ?? $payload['data']['id'] ?? $payload['id'] ?? '');
            return [
                'event_type' => $type,
                'status' => $status,
                'amount_cents' => (int)round($amount * 100),
                'external_id' => $ext,
            ];
        }
        if ($provider === 'asaas') {
            $type = (string)($payload['event'] ?? $payload['type'] ?? 'payment');
            $status = (string)($payload['payment']['status'] ?? $payload['status'] ?? 'received');
            $amount = (float)($payload['payment']['value'] ?? $payload['value'] ?? 0.0);
            $ext = (string)($payload['payment']['id'] ?? $payload['id'] ?? '');
            return [
                'event_type' => $type,
                'status' => $status,
                'amount_cents' => (int)round($amount * 100),
                'external_id' => $ext,
            ];
        }
        if ($provider === 'itau') {
            // Webhook Itaú Pix: payload típico contém array "pix" com itens (endToEndId/e2eid, txid, valor, horario)
            $item = isset($payload['pix'][0]) && is_array($payload['pix'][0]) ? $payload['pix'][0] : [];
            $type = 'pix';
            $status = (string)($payload['status'] ?? 'received');
            $valor = isset($item['valor']) ? (float)str_replace(',', '.', (string)$item['valor']) : (float)($payload['amount'] ?? 0.0);
            $ext = (string)($item['endToEndId'] ?? $item['e2eid'] ?? $item['txid'] ?? $payload['id'] ?? '');
            $occurred = (string)($item['horario'] ?? $payload['occurred_at'] ?? '');
            return [
                'event_type' => $type,
                'status' => $status,
                'amount_cents' => (int)round($valor * 100),
                'external_id' => $ext,
                'occurred_at' => $occurred ? date('Y-m-d H:i:s', strtotime($occurred)) : null,
            ];
        }
        // Desconhecido: retornar mapeamento mínimo
        return [ 'event_type' => 'unknown', 'status' => 'received', 'amount_cents' => 0 ];
    }

    private function ensurePaymentsTable(\PDO $pdo): void {
        try {
            $pdo->exec('CREATE TABLE IF NOT EXISTS payments (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                tenant_id VARCHAR(100) NULL,
                amount_cents INT NOT NULL,
                status VARCHAR(50) NULL,
                occurred_at DATETIME NULL,
                provider VARCHAR(50) NULL,
                external_id VARCHAR(190) NULL,
                notes TEXT NULL,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_provider (provider),
                INDEX idx_external (external_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
            // Refreforço de idempotência: tentar criar índice único (provider, external_id)
            try { $pdo->exec('ALTER TABLE payments ADD UNIQUE KEY uniq_provider_external (provider, external_id)'); } catch (\Throwable $e) { /* pode já existir ou haver duplicatas; tolerar */ }
        } catch (\Throwable $e) { /* tolerante */ }
    }
}