<?php

namespace Modules\Integracoes\OpenBanking\Services;

use PDO;
use Exception;

/**
 * Serviço para integração com Open Banking
 */
class OpenBankingAPIService {
    private PDO $pdo;
    private string $tenantId;
    private array $bancos = [
        '001' => ['nome' => 'Banco do Brasil', 'api_url' => 'https://api.bb.com.br'],
        '033' => ['nome' => 'Santander', 'api_url' => 'https://api.santander.com.br'],
        '104' => ['nome' => 'Caixa', 'api_url' => 'https://api.caixa.gov.br'],
        '237' => ['nome' => 'Bradesco', 'api_url' => 'https://api.bradesco.com.br'],
        '341' => ['nome' => 'Itaú', 'api_url' => 'https://api.itau.com.br'],
    ];

    public function __construct(string $tenantId) {
        $this->tenantId = $tenantId;
        $this->pdo = \Gestoreasy\Core\Tenant\ConnectionManager::forTenant($tenantId);
    }

    /**
     * Obtém configuração
     */
    public function getConfig(?string $bancoCodigo = null, bool $incluirSecrets = false): array {
        if ($bancoCodigo) {
            $stmt = $this->pdo->prepare('SELECT * FROM openbanking_config WHERE tenant_id = ? AND banco_codigo = ?');
            $stmt->execute([$this->tenantId, $bancoCodigo]);
            $config = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if (!$config) {
                return [];
            }
            
            // Remover dados sensíveis se não solicitado
            if (!$incluirSecrets) {
                unset($config['access_token'], $config['refresh_token'], $config['client_secret']);
            }
            
            return $config;
        } else {
            $stmt = $this->pdo->prepare('SELECT * FROM openbanking_config WHERE tenant_id = ?');
            $stmt->execute([$this->tenantId]);
            $configs = $stmt->fetchAll(PDO::FETCH_ASSOC);
            
            // Remover dados sensíveis
            foreach ($configs as &$config) {
                if (!$incluirSecrets) {
                    unset($config['access_token'], $config['refresh_token'], $config['client_secret']);
                }
            }
            
            return $configs;
        }
    }

    /**
     * Atualiza configuração
     */
    public function updateConfig(string $bancoCodigo, array $dados): void {
        $banco = $this->bancos[$bancoCodigo] ?? null;
        if (!$banco) {
            throw new Exception('Banco não suportado');
        }

        $stmt = $this->pdo->prepare('SELECT id FROM openbanking_config WHERE tenant_id = ? AND banco_codigo = ?');
        $stmt->execute([$this->tenantId, $bancoCodigo]);
        $exists = $stmt->fetch();

        $allowedFields = [
            'client_id', 'client_secret', 'access_token', 'refresh_token', 'token_expires_at',
            'ambiente', 'conciliacao_automatica', 'intervalo_conciliacao_minutos', 'ativo'
        ];

        if ($exists) {
            $updates = [];
            $params = [];
            foreach ($allowedFields as $field) {
                if (isset($dados[$field])) {
                    $updates[] = "{$field} = ?";
                    $params[] = $dados[$field];
                }
            }
            if (!empty($updates)) {
                $params[] = $this->tenantId;
                $params[] = $bancoCodigo;
                $sql = 'UPDATE openbanking_config SET ' . implode(', ', $updates) . ' WHERE tenant_id = ? AND banco_codigo = ?';
                $stmt = $this->pdo->prepare($sql);
                $stmt->execute($params);
            }
        } else {
            $fields = ['banco_codigo', 'banco_nome'];
            $values = ['?', '?'];
            $params = [$bancoCodigo, $banco['nome']];
            
            foreach ($allowedFields as $field) {
                if (isset($dados[$field])) {
                    $fields[] = $field;
                    $values[] = '?';
                    $params[] = $dados[$field];
                }
            }
            
            $params[] = $this->tenantId;
            $sql = 'INSERT INTO openbanking_config (tenant_id, ' . implode(', ', $fields) . ') VALUES (?, ' . implode(', ', $values) . ')';
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
        }
    }

    /**
     * Lista contas bancárias
     */
    public function listContas(?string $bancoCodigo = null): array {
        $where = 'oc.tenant_id = ?';
        $params = [$this->tenantId];

        if ($bancoCodigo) {
            $where .= ' AND oc.banco_codigo = ?';
            $params[] = $bancoCodigo;
        }

        $sql = "
            SELECT oc.*, obc.id as conta_id, obc.agencia, obc.numero_conta,
                   obc.saldo_atual, obc.saldo_disponivel
            FROM openbanking_config oc
            LEFT JOIN openbanking_contas obc ON oc.id = obc.config_id
            WHERE {$where}
            ORDER BY oc.banco_nome ASC
        ";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Obtém saldo da conta
     */
    public function getSaldo(int $contaId): array {
        // Buscar dados da conta para obter banco_codigo
        $stmt = $this->pdo->prepare('SELECT * FROM openbanking_contas WHERE id = ? AND tenant_id = ?');
        $stmt->execute([$contaId, $this->tenantId]);
        $conta = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$conta) {
            throw new Exception('Conta não encontrada');
        }

        $config = $this->getConfig($conta['banco_codigo'], true); // Incluir secrets para API
        
        // Buscar saldo via API do banco
        $saldo = $this->buscarSaldoAPI($config, $contaId);
        
        // Atualizar no banco de dados
        $stmt = $this->pdo->prepare('
            UPDATE openbanking_contas
            SET saldo_atual = ?,
                saldo_disponivel = ?,
                ultima_atualizacao = NOW()
            WHERE id = ?
        ');
        $stmt->execute([
            $saldo['saldo_atual'],
            $saldo['saldo_disponivel'],
            $contaId
        ]);

        return $saldo;
    }

    /**
     * Obtém extrato
     */
    public function getExtrato(int $contaId, ?string $dataInicio = null, ?string $dataFim = null): array {
        $dataInicio = $dataInicio ?: date('Y-m-01');
        $dataFim = $dataFim ?: date('Y-m-t');
        
        // Buscar dados da conta para obter banco_codigo
        $stmt = $this->pdo->prepare('SELECT * FROM openbanking_contas WHERE id = ? AND tenant_id = ?');
        $stmt->execute([$contaId, $this->tenantId]);
        $conta = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$conta) {
            throw new Exception('Conta não encontrada');
        }

        $config = $this->getConfig($conta['banco_codigo'], true); // Incluir secrets para API
        
        // Buscar extrato via API
        $transacoes = $this->buscarExtratoAPI($config, $contaId, $dataInicio, $dataFim);
        
        // Transações já são salvas dentro de buscarExtratoAPI
        return $transacoes;
    }

    /**
     * Conciliação automática
     */
    public function conciliacaoAutomatica(?string $bancoCodigo = null, ?string $dataInicio = null, ?string $dataFim = null): array {
        $dataInicio = $dataInicio ?: date('Y-m-01');
        $dataFim = $dataFim ?: date('Y-m-t');
        
        $contas = $this->listContas($bancoCodigo);
        $totalConciliadas = 0;
        $totalNaoConciliadas = 0;

        foreach ($contas as $conta) {
            if (empty($conta['conta_id'])) {
                continue;
            }

            // Buscar transações não conciliadas
            $stmt = $this->pdo->prepare('
                SELECT * FROM openbanking_transacoes
                WHERE conta_id = ? AND conciliado = FALSE
                AND data_transacao BETWEEN ? AND ?
            ');
            $stmt->execute([$conta['conta_id'], $dataInicio, $dataFim]);
            $transacoes = $stmt->fetchAll(PDO::FETCH_ASSOC);

            foreach ($transacoes as $transacao) {
                $conciliada = $this->tentarConciliar($transacao);
                if ($conciliada) {
                    $totalConciliadas++;
                } else {
                    $totalNaoConciliadas++;
                }
            }
        }

        // Registrar log
        $stmt = $this->pdo->prepare('
            INSERT INTO openbanking_conciliacao_logs (
                tenant_id, data_execucao, tipo, total_transacoes,
                total_conciliadas, total_nao_conciliadas
            ) VALUES (?, NOW(), "automatica", ?, ?, ?)
        ');
        $stmt->execute([
            $this->tenantId,
            $totalConciliadas + $totalNaoConciliadas,
            $totalConciliadas,
            $totalNaoConciliadas
        ]);

        return [
            'total_transacoes' => $totalConciliadas + $totalNaoConciliadas,
            'total_conciliadas' => $totalConciliadas,
            'total_nao_conciliadas' => $totalNaoConciliadas
        ];
    }

    /**
     * Tenta conciliar transação
     */
    private function tentarConciliar(array $transacao): bool {
        $valor = abs((float)$transacao['valor']);
        $data = $transacao['data_transacao'];
        $tipo = $transacao['tipo'];

        // Buscar contas a receber/pagar com valor e data similares
        $tabela = $tipo === 'credito' ? 'contas_receber' : 'contas_pagar';
        $campoValor = $tipo === 'credito' ? 'valor' : 'valor';
        
        $stmt = $this->pdo->prepare("
            SELECT id FROM {$tabela}
            WHERE ABS({$campoValor} - ?) < 0.01
            AND DATE(vencimento) = ?
            AND status = 'pendente'
            LIMIT 1
        ");
        $stmt->execute([$valor, $data]);
        $conta = $stmt->fetch();

        if ($conta) {
            // Conciliar
            $stmt = $this->pdo->prepare('
                UPDATE openbanking_transacoes
                SET conciliado = TRUE,
                    conta_financeiro_id = ?,
                    tipo_financeiro = ?
                WHERE id = ?
            ');
            $stmt->execute([
                $conta['id'],
                $tipo === 'credito' ? 'receber' : 'pagar',
                $transacao['id']
            ]);
            return true;
        }

        return false;
    }

    /**
     * Troca code por access_token (OAuth 2.0)
     */
    public function trocarCodePorToken(string $bancoCodigo, string $code, ?string $state = null): array {
        $config = $this->getConfig($bancoCodigo, true); // Incluir secrets para OAuth
        if (empty($config)) {
            throw new Exception('Configuração do banco não encontrada');
        }

        $banco = $this->bancos[$bancoCodigo] ?? null;
        if (!$banco) {
            throw new Exception('Banco não suportado');
        }

        // URL do token endpoint (varia por banco)
        $tokenUrl = $this->getTokenUrl($bancoCodigo);

        // Dados para troca do token
        $data = [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'client_id' => $config['client_id'],
            'client_secret' => $config['client_secret'],
            'redirect_uri' => $this->getRedirectUri($bancoCodigo)
        ];

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

        // Fazer requisição
        $ch = curl_init($tokenUrl);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($data),
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/x-www-form-urlencoded',
                'Accept: application/json'
            ],
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_TIMEOUT => 30
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($error) {
            throw new Exception("Erro na requisição: {$error}");
        }

        if ($httpCode >= 400) {
            $errorData = json_decode($response, true);
            $message = $errorData['error_description'] ?? $errorData['error'] ?? "Erro HTTP {$httpCode}";
            throw new Exception($message);
        }

        $tokenData = json_decode($response, true);
        
        if (empty($tokenData['access_token'])) {
            throw new Exception('Access token não recebido');
        }

        // Salvar tokens
        $expiresIn = (int)($tokenData['expires_in'] ?? 3600);
        $expiresAt = date('Y-m-d H:i:s', time() + $expiresIn);

        $stmt = $this->pdo->prepare('
            UPDATE openbanking_config 
            SET access_token = ?, refresh_token = ?, token_expires_at = ?
            WHERE tenant_id = ? AND banco_codigo = ?
        ');
        $stmt->execute([
            $tokenData['access_token'],
            $tokenData['refresh_token'] ?? null,
            $expiresAt,
            $this->tenantId,
            $bancoCodigo
        ]);

        return [
            'success' => true,
            'expires_at' => $expiresAt
        ];
    }

    /**
     * Busca saldo via API
     */
    private function buscarSaldoAPI(array $config, int $contaId): array {
        // Verificar se token está válido
        if (empty($config['access_token'])) {
            throw new Exception('Access token não configurado');
        }

        // Verificar expiração (renovar com 5 minutos de antecedência)
        if (!empty($config['token_expires_at'])) {
            $expiresAt = strtotime($config['token_expires_at']);
            $now = time();
            // Renovar se expirar em menos de 5 minutos
            if ($expiresAt < ($now + 300)) {
                try {
                    $this->renovarToken($config['banco_codigo']);
                    $config = $this->getConfig($config['banco_codigo'], true);
                } catch (Exception $e) {
                    error_log("Erro ao renovar token Open Banking: " . $e->getMessage());
                    // Se falhar, tentar usar token atual mesmo
                }
            }
        }

        // Verificar cache de saldo (cache de 1 minuto)
        $cacheKey = "saldo_{$contaId}";
        $cached = $this->getCachedSaldo($contaId);
        if ($cached !== null) {
            return $cached;
        }

        // Buscar dados da conta
        $stmt = $this->pdo->prepare('SELECT * FROM openbanking_contas WHERE id = ? AND tenant_id = ?');
        $stmt->execute([$contaId, $this->tenantId]);
        $conta = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$conta) {
            throw new Exception('Conta não encontrada');
        }

        $banco = $this->bancos[$conta['banco_codigo']] ?? null;
        if (!$banco) {
            throw new Exception('Banco não suportado');
        }

        // URL da API de saldo (varia por banco)
        $contaNumero = $conta['numero_conta'] ?? $conta['conta_id'] ?? '';
        $saldoUrl = $this->getSaldoUrl($conta['banco_codigo'], $contaNumero);

        // Fazer requisição com retry
        $maxRetries = 3;
        $retryDelay = 1; // segundos
        $lastError = null;

        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            try {
                $ch = curl_init($saldoUrl);
                curl_setopt_array($ch, [
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_HTTPHEADER => [
                        'Authorization: Bearer ' . $config['access_token'],
                        'Content-Type: application/json',
                        'Accept: application/json'
                    ],
                    CURLOPT_SSL_VERIFYPEER => true,
                    CURLOPT_TIMEOUT => 30
                ]);

                $response = curl_exec($ch);
                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                $error = curl_error($ch);
                curl_close($ch);

                if ($error) {
                    throw new Exception("Erro na requisição: {$error}");
                }

                if ($httpCode === 401) {
                    // Token inválido, tentar renovar
                    if ($attempt < $maxRetries) {
                        try {
                            $this->renovarToken($config['banco_codigo']);
                            $config = $this->getConfig($config['banco_codigo'], true);
                            sleep($retryDelay);
                            continue;
                        } catch (Exception $e) {
                            throw new Exception("Token inválido e não foi possível renovar: " . $e->getMessage());
                        }
                    }
                }

                if ($httpCode >= 400) {
                    $errorData = json_decode($response, true);
                    $message = $errorData['message'] ?? $errorData['error'] ?? "Erro HTTP {$httpCode}";
                    
                    // Retry em erros temporários (500, 502, 503, 504)
                    if (in_array($httpCode, [500, 502, 503, 504]) && $attempt < $maxRetries) {
                        $lastError = new Exception($message);
                        sleep($retryDelay * $attempt); // Backoff exponencial
                        continue;
                    }
                    
                    throw new Exception($message);
                }

                // Sucesso, sair do loop
                break;
            } catch (Exception $e) {
                $lastError = $e;
                if ($attempt < $maxRetries) {
                    sleep($retryDelay * $attempt);
                    continue;
                }
                throw $e;
            }
        }

        if ($lastError && !isset($response)) {
            throw $lastError;
        }

        $saldoData = json_decode($response, true);

        // Normalizar resposta (varia por banco)
        $saldoAtual = $this->extrairSaldoAtual($saldoData, $conta['banco_codigo']);
        $saldoDisponivel = $this->extrairSaldoDisponivel($saldoData, $conta['banco_codigo']);

        // Atualizar saldo na conta
        $stmt = $this->pdo->prepare('
            UPDATE openbanking_contas 
            SET saldo_atual = ?, saldo_disponivel = ?, ultima_atualizacao = NOW()
            WHERE id = ?
        ');
        $stmt->execute([$saldoAtual, $saldoDisponivel, $contaId]);

        $saldo = [
            'saldo_atual' => $saldoAtual,
            'saldo_disponivel' => $saldoDisponivel
        ];

        // Cachear saldo por 1 minuto
        $this->cacheSaldo($contaId, $saldo);

        return $saldo;
    }

    /**
     * Cacheia saldo por 1 minuto
     */
    private function cacheSaldo(int $contaId, array $saldo): void {
        try {
            $stmt = $this->pdo->prepare('
                INSERT INTO openbanking_saldo_cache (conta_id, saldo_atual, saldo_disponivel, cached_at)
                VALUES (?, ?, ?, NOW())
                ON DUPLICATE KEY UPDATE
                    saldo_atual = VALUES(saldo_atual),
                    saldo_disponivel = VALUES(saldo_disponivel),
                    cached_at = NOW()
            ');
            $stmt->execute([
                $contaId,
                $saldo['saldo_atual'],
                $saldo['saldo_disponivel']
            ]);
        } catch (Exception $e) {
            // Ignorar erro de cache
            error_log("Erro ao cachear saldo: " . $e->getMessage());
        }
    }

    /**
     * Obtém saldo do cache se ainda válido (1 minuto)
     */
    private function getCachedSaldo(int $contaId): ?array {
        try {
            $stmt = $this->pdo->prepare('
                SELECT saldo_atual, saldo_disponivel, cached_at
                FROM openbanking_saldo_cache
                WHERE conta_id = ?
                AND cached_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
                LIMIT 1
            ');
            $stmt->execute([$contaId]);
            $cached = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if ($cached) {
                return [
                    'saldo_atual' => (float)$cached['saldo_atual'],
                    'saldo_disponivel' => (float)$cached['saldo_disponivel']
                ];
            }
        } catch (Exception $e) {
            // Se tabela não existir, retornar null
            if (strpos($e->getMessage(), "doesn't exist") === false) {
                error_log("Erro ao buscar saldo do cache: " . $e->getMessage());
            }
        }

        return null;
    }

    /**
     * Busca extrato via API
     */
    private function buscarExtratoAPI(array $config, int $contaId, string $dataInicio, string $dataFim): array {
        // Verificar se token está válido
        if (empty($config['access_token'])) {
            throw new Exception('Access token não configurado');
        }

        // Verificar expiração (renovar com 5 minutos de antecedência)
        if (!empty($config['token_expires_at'])) {
            $expiresAt = strtotime($config['token_expires_at']);
            $now = time();
            if ($expiresAt < ($now + 300)) {
                try {
                    $this->renovarToken($config['banco_codigo']);
                    $config = $this->getConfig($config['banco_codigo'], true);
                } catch (Exception $e) {
                    error_log("Erro ao renovar token Open Banking: " . $e->getMessage());
                }
            }
        }

        // Buscar dados da conta
        $stmt = $this->pdo->prepare('SELECT * FROM openbanking_contas WHERE id = ? AND tenant_id = ?');
        $stmt->execute([$contaId, $this->tenantId]);
        $conta = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$conta) {
            throw new Exception('Conta não encontrada');
        }

        $banco = $this->bancos[$conta['banco_codigo']] ?? null;
        if (!$banco) {
            throw new Exception('Banco não suportado');
        }

        // URL da API de extrato
        $contaNumero = $conta['numero_conta'] ?? $conta['conta_id'] ?? '';
        $extratoUrl = $this->getExtratoUrl($conta['banco_codigo'], $contaNumero, $dataInicio, $dataFim);

        // Fazer requisição com retry
        $maxRetries = 3;
        $retryDelay = 1;
        $lastError = null;
        $response = null;

        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            try {
                $ch = curl_init($extratoUrl);
                curl_setopt_array($ch, [
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_HTTPHEADER => [
                        'Authorization: Bearer ' . $config['access_token'],
                        'Content-Type: application/json',
                        'Accept: application/json'
                    ],
                    CURLOPT_SSL_VERIFYPEER => true,
                    CURLOPT_TIMEOUT => 60 // Timeout maior para extrato
                ]);

                $response = curl_exec($ch);
                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                $error = curl_error($ch);
                curl_close($ch);

                if ($error) {
                    throw new Exception("Erro na requisição: {$error}");
                }

                if ($httpCode === 401) {
                    // Token inválido, tentar renovar
                    if ($attempt < $maxRetries) {
                        try {
                            $this->renovarToken($config['banco_codigo']);
                            $config = $this->getConfig($config['banco_codigo'], true);
                            sleep($retryDelay);
                            continue;
                        } catch (Exception $e) {
                            throw new Exception("Token inválido e não foi possível renovar: " . $e->getMessage());
                        }
                    }
                }

                if ($httpCode >= 400) {
                    $errorData = json_decode($response, true);
                    $message = $errorData['message'] ?? $errorData['error'] ?? "Erro HTTP {$httpCode}";
                    
                    // Retry em erros temporários
                    if (in_array($httpCode, [500, 502, 503, 504]) && $attempt < $maxRetries) {
                        $lastError = new Exception($message);
                        sleep($retryDelay * $attempt);
                        continue;
                    }
                    
                    throw new Exception($message);
                }

                // Sucesso
                break;
            } catch (Exception $e) {
                $lastError = $e;
                if ($attempt < $maxRetries) {
                    sleep($retryDelay * $attempt);
                    continue;
                }
                throw $e;
            }
        }

        if ($lastError && !$response) {
            throw $lastError;
        }

        if (!$response) {
            throw new Exception('Nenhuma resposta recebida da API');
        }

        $extratoData = json_decode($response, true);

        // Normalizar e salvar transações
        $transacoes = $this->normalizarExtrato($extratoData, $conta['banco_codigo']);
        
        foreach ($transacoes as $transacao) {
            $this->salvarTransacao($contaId, $transacao);
        }

        return $transacoes;
    }

    /**
     * Renova access token usando refresh token
     */
    private function renovarToken(string $bancoCodigo): void {
        $config = $this->getConfig($bancoCodigo, true); // Incluir secrets para renovação
        
        if (empty($config['refresh_token'])) {
            throw new Exception('Refresh token não disponível');
        }

        $tokenUrl = $this->getTokenUrl($bancoCodigo);

        $data = [
            'grant_type' => 'refresh_token',
            'refresh_token' => $config['refresh_token'],
            'client_id' => $config['client_id'],
            'client_secret' => $config['client_secret']
        ];

        $ch = curl_init($tokenUrl);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($data),
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/x-www-form-urlencoded',
                'Accept: application/json'
            ],
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_TIMEOUT => 30
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode >= 400) {
            throw new Exception('Erro ao renovar token');
        }

        $tokenData = json_decode($response, true);
        
        $expiresIn = (int)($tokenData['expires_in'] ?? 3600);
        $expiresAt = date('Y-m-d H:i:s', time() + $expiresIn);

        $stmt = $this->pdo->prepare('
            UPDATE openbanking_config 
            SET access_token = ?, refresh_token = ?, token_expires_at = ?
            WHERE tenant_id = ? AND banco_codigo = ?
        ');
        $stmt->execute([
            $tokenData['access_token'],
            $tokenData['refresh_token'] ?? $config['refresh_token'],
            $expiresAt,
            $this->tenantId,
            $bancoCodigo
        ]);
    }

    /**
     * Obtém URL do token endpoint (varia por banco)
     */
    private function getTokenUrl(string $bancoCodigo): string {
        $banco = $this->bancos[$bancoCodigo] ?? null;
        if (!$banco) {
            throw new Exception('Banco não suportado');
        }

        // URLs padrão do Open Banking Brasil (podem variar por banco)
        $urls = [
            '001' => $banco['api_url'] . '/oauth/token', // Banco do Brasil
            '033' => $banco['api_url'] . '/oauth/v2/token', // Santander
            '104' => $banco['api_url'] . '/token', // Caixa
            '237' => $banco['api_url'] . '/oauth/token', // Bradesco
            '341' => $banco['api_url'] . '/oauth/token', // Itaú
        ];

        return $urls[$bancoCodigo] ?? $banco['api_url'] . '/oauth/token';
    }

    /**
     * Obtém URL de saldo (varia por banco)
     */
    private function getSaldoUrl(string $bancoCodigo, string $contaNumero): string {
        $banco = $this->bancos[$bancoCodigo] ?? null;
        if (!$banco) {
            throw new Exception('Banco não suportado');
        }

        // URLs padrão (podem variar por banco)
        $urls = [
            '001' => $banco['api_url'] . '/open-banking/accounts/v1/accounts/' . $contaNumero . '/balances',
            '033' => $banco['api_url'] . '/open-banking/v1/accounts/' . $contaNumero . '/balances',
            '104' => $banco['api_url'] . '/open-banking/accounts/v1/accounts/' . $contaNumero . '/balances',
            '237' => $banco['api_url'] . '/open-banking/accounts/v1/accounts/' . $contaNumero . '/balances',
            '341' => $banco['api_url'] . '/open-banking/accounts/v1/accounts/' . $contaNumero . '/balances',
        ];

        return $urls[$bancoCodigo] ?? $banco['api_url'] . '/open-banking/accounts/v1/accounts/' . $contaNumero . '/balances';
    }

    /**
     * Obtém URL de extrato (varia por banco)
     */
    private function getExtratoUrl(string $bancoCodigo, string $contaNumero, string $dataInicio, string $dataFim): string {
        $banco = $this->bancos[$bancoCodigo] ?? null;
        if (!$banco) {
            throw new Exception('Banco não suportado');
        }

        $baseUrl = $banco['api_url'] . '/open-banking/transactions/v1/accounts/' . $contaNumero . '/transactions';
        return $baseUrl . '?fromBookingDate=' . $dataInicio . '&toBookingDate=' . $dataFim;
    }

    /**
     * Obtém redirect URI para OAuth
     */
    private function getRedirectUri(string $bancoCodigo): string {
        // URL base do sistema
        $baseUrl = $_SERVER['HTTP_HOST'] ?? 'localhost';
        $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        return $protocol . '://' . $baseUrl . '/integracoes/open-banking/callback?banco_codigo=' . $bancoCodigo;
    }

    /**
     * Extrai saldo atual da resposta (normaliza formato de diferentes bancos)
     */
    private function extrairSaldoAtual(array $data, string $bancoCodigo): float {
        // Formato pode variar por banco
        // Exemplo: data['data']['balance']['amount'] ou data['balance']['availableAmount']
        
        if (isset($data['data']['balance']['amount'])) {
            return (float)$data['data']['balance']['amount'];
        }
        
        if (isset($data['balance']['availableAmount'])) {
            return (float)$data['balance']['availableAmount'];
        }
        
        if (isset($data['data']['availableAmount'])) {
            return (float)$data['data']['availableAmount'];
        }

        return 0.0;
    }

    /**
     * Extrai saldo disponível da resposta
     */
    private function extrairSaldoDisponivel(array $data, string $bancoCodigo): float {
        if (isset($data['data']['balance']['availableAmount'])) {
            return (float)$data['data']['balance']['availableAmount'];
        }
        
        if (isset($data['balance']['availableAmount'])) {
            return (float)$data['balance']['availableAmount'];
        }

        // Se não tiver disponível, usar saldo atual
        return $this->extrairSaldoAtual($data, $bancoCodigo);
    }

    /**
     * Normaliza extrato de diferentes bancos para formato padrão
     */
    private function normalizarExtrato(array $data, string $bancoCodigo): array {
        $transacoes = [];

        // Formato pode variar: data['data']['transactions'] ou data['transactions']
        $items = $data['data']['transactions'] ?? $data['transactions'] ?? [];

        foreach ($items as $item) {
            $transacoes[] = [
                'id' => $item['transactionId'] ?? $item['id'] ?? uniqid(),
                'data' => $item['bookingDate'] ?? $item['date'] ?? date('Y-m-d'),
                'data_liquidacao' => $item['valueDate'] ?? $item['bookingDate'] ?? null,
                'tipo' => $this->normalizarTipo($item['creditDebitIndicator'] ?? $item['type'] ?? 'DEBIT'),
                'valor' => (float)($item['amount']['amount'] ?? $item['amount'] ?? 0),
                'descricao' => $item['transactionInformation'] ?? $item['description'] ?? 'Transação',
            ];
        }

        return $transacoes;
    }

    /**
     * Normaliza tipo de transação
     */
    private function normalizarTipo(string $tipo): string {
        $tipo = strtoupper($tipo);
        
        if (in_array($tipo, ['CREDIT', 'CREDITO', 'CR'])) {
            return 'credito';
        }
        
        if (in_array($tipo, ['DEBIT', 'DEBITO', 'DB'])) {
            return 'debito';
        }

        return 'debito';
    }

    /**
     * Salva transação
     */
    private function salvarTransacao(int $contaId, array $transacao): void {
        $stmt = $this->pdo->prepare('
            INSERT INTO openbanking_transacoes (
                tenant_id, conta_id, transacao_id, data_transacao,
                data_liquidacao, tipo, valor, descricao, dados_json
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE
                valor = VALUES(valor),
                descricao = VALUES(descricao),
                dados_json = VALUES(dados_json)
        ');
        $stmt->execute([
            $this->tenantId,
            $contaId,
            $transacao['id'],
            $transacao['data'],
            $transacao['data_liquidacao'] ?? null,
            $transacao['tipo'],
            $transacao['valor'],
            $transacao['descricao'] ?? null,
            json_encode($transacao)
        ]);
    }

    /**
     * Obtém config para conta
     */
    private function getConfigForConta(int $contaId): array {
        $stmt = $this->pdo->prepare('
            SELECT oc.* FROM openbanking_config oc
            INNER JOIN openbanking_contas obc ON oc.id = obc.config_id
            WHERE obc.id = ? AND oc.tenant_id = ?
        ');
        $stmt->execute([$contaId, $this->tenantId]);
        $config = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$config) {
            throw new Exception('Conta não encontrada');
        }

        return $config;
    }
}

