<?php
/**
 * CLASSE DE SINCRONIZAÇÃO ERP
 * 
 * Responsável pela sincronização incremental de dados do ERP para HELM
 * Com suporte a alto volume (1M+ registros/mês)
 */

class SincronizacaoERP {
    private $pdo;
    private $cdintegracao;
    private $cdempresa;
    private $conexao_erp;
    private $cdintegracao_log;
    
    const TAMANHO_LOTE = 1000;
    const MAX_TENTATIVAS = 5;
    const TIMEOUT_CONEXAO = 30;
    
    public function __construct($pdo, $cdintegracao, $cdempresa) {
        $this->pdo = $pdo;
        $this->cdintegracao = $cdintegracao;
        $this->cdempresa = $cdempresa;
    }
    
    /**
     * INICIAR SINCRONIZAÇÃO
     * Retorna: array com status da sincronização
     */
    public function iniciar() {
        try {
            // 1. VALIDAR CONFIGURAÇÃO
            $config = $this->obterConfiguracao();
            if (!$config) {
                return ['ok' => false, 'msg' => 'Configuração de sincronização não encontrada'];
            }
            
            // 2. REGISTRAR INÍCIO
            $this->cdintegracao_log = $this->criarRegistroLog('processando');
            
            // 3. CONECTAR AO ERP
            $this->conexao_erp = $this->conectarERP();
            if (!$this->conexao_erp) {
                throw new Exception('Falha ao conectar ao ERP');
            }
            
            // 4. BUSCAR DADOS DO ERP
            $registros = $this->buscarDadosERP($config);
            
            // 5. PROCESSAR REGISTROS
            $resultado = $this->processarRegistros($registros, $config);
            
            // 6. SINCRONIZAR DELETIONS (detectar por ausência - read-only)
            if ($config['fl_sincronizar_delecoes']) {
                $resultado['deletados'] = $this->sincronizarDeletadosReadOnly($config);
            }
            
            // 7. FINALIZAR
            return $this->finalizarSincronizacao($resultado);
            
        } catch (Exception $e) {
            $this->registrarErro($e->getMessage());
            return ['ok' => false, 'msg' => $e->getMessage()];
        }
    }
    
    /**
     * OBTER CONFIGURAÇÃO DE SINCRONIZAÇÃO
     */
    private function obterConfiguracao() {
        $stmt = $this->pdo->prepare("
            SELECT * FROM back.integracao_config_sinc
            WHERE cdintegracao = :integ AND cdempresa = :emp
        ");
        $stmt->execute([
            ':integ' => $this->cdintegracao,
            ':emp' => $this->cdempresa
        ]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    /**
     * CRIAR REGISTRO LOG
     */
    private function criarRegistroLog($status) {
        $stmt = $this->pdo->prepare("
            INSERT INTO back.integracao_log 
            (cdintegracao, flstatus, dthinicio, cdusername_criacao)
            VALUES (:integ, :status, NOW(), :user)
            RETURNING cdintegracao_log
        ");
        $stmt->execute([
            ':integ' => $this->cdintegracao,
            ':status' => $status,
            ':user' => $_SESSION['username'] ?? 'sistema'
        ]);
        return $stmt->fetchColumn();
    }
    
    /**
     * CONECTAR AO ERP
     */
    private function conectarERP() {
        // Buscar credenciais da integração
        $stmt = $this->pdo->prepare("
            SELECT * FROM back.integracao
            WHERE cdintegracao = :id AND fltipo = 'conexao'
        ");
        $stmt->execute([':id' => $this->cdintegracao]);
        $config = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$config) {
            throw new Exception('Integração ou origem não encontrada');
        }
        
        try {
            // Descobrir origem vinculada à conexão
            $cdorigem = $config['cdorigem'] ?? null;

            // Fallback: se ainda não houver cdorigem na conexão, tentar obter via modelo vinculado
            if (!$cdorigem && !empty($config['cdmodelo'])) {
                $stmtModelo = $this->pdo->prepare("
                    SELECT cdorigem FROM back.integracao WHERE cdintegracao = :id AND fltipo = 'modelo'
                ");
                $stmtModelo->execute([':id' => $config['cdmodelo']]);
                $cdorigem = $stmtModelo->fetchColumn();
            }

            if (!$cdorigem) {
                throw new Exception('Origem não vinculada à conexão');
            }

            // Buscar origem para obter SQL
            $stmtOrigem = $this->pdo->prepare("
                SELECT sql_origem FROM back.integracao WHERE cdintegracao = :id AND fltipo = 'origem'
            ");
            $stmtOrigem->execute([':id' => $cdorigem]);
            $origem = $stmtOrigem->fetch(PDO::FETCH_ASSOC);
            if (!$origem || empty($origem['sql_origem'])) {
                throw new Exception('SQL da origem não encontrada para a conexão');
            }
            
            // Montar string de conexão
            $dsn = $this->montarDSN($config);
            
            // Criar conexão com timeout
            $conexao = new PDO(
                $dsn,
                $config['usuario'],
                base64_decode($config['senha_encrypted']),
                [
                    PDO::ATTR_TIMEOUT => self::TIMEOUT_CONEXAO,
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
                ]
            );
            
            // Armazenar SQL da origem (usado na busca de dados)
            $conexao->setAttribute('sql_origem', $origem['sql_origem']);
            
            return $conexao;
            
        } catch (Exception $e) {
            throw new Exception("Erro ao conectar ao ERP: " . $e->getMessage());
        }
    }
    
    /**
     * MONTAR DSN POR TIPO DE BANCO
     */
    private function montarDSN($config) {
        switch ($config['tipo_banco']) {
            case 'pgsql':
                return "pgsql:host={$config['host']};port={$config['porta']};" .
                       "dbname={$config['dbname']};";
            case 'mysql':
                return "mysql:host={$config['host']};port={$config['porta']};" .
                       "dbname={$config['dbname']};charset=utf8mb4";
            case 'mssql':
                return "sqlsrv:Server={$config['host']},{$config['porta']};" .
                       "Database={$config['dbname']}";
            default:
                throw new Exception("Tipo de banco não suportado: {$config['tipo_banco']}");
        }
    }
    
    /**
     * BUSCAR DADOS DO ERP
     * Modelo read-only: busca apenas registros novos/modificados
     * NÃO procura por data_delecao (não existe em ERP read-only)
     */
    private function buscarDadosERP($config) {
        $sql_origem = $this->conexao_erp->getAttribute('sql_origem');
        
        // Blindagem: garantir que sql_origem continua sendo apenas SELECT
        if (!function_exists('validarSQLOrigem')) {
            require_once dirname(__DIR__) . '/modules/integracoes_actions.php';
        }
        if (!validarSQLOrigem($sql_origem)) {
            throw new Exception('SQL da origem inválido para execução (apenas SELECT simples é permitido)');
        }
        
        // Se for primeira sincronização, buscar tudo
        if (!$config['ultima_sinc_sucesso']) {
            $sql = $sql_origem;
        } else {
            // Sincronização incremental - apenas registros novos/modificados
            // IMPORTANTE: SQL da origem deve ter colunas de data
            $data_ultima = $config['ultima_sinc_sucesso'];
            
            // Adicionar cláusula WHERE para filtrar por data
            // NOTA: Em ERP read-only, NÃO procuramos por data_delecao
            // Deleções são detectadas na Fase 2 por ausência
            $sql = $sql_origem . " 
                WHERE (data_criacao > '$data_ultima' 
                       OR data_modificacao > '$data_ultima')
                ORDER BY data_modificacao DESC";
        }
        
        try {
            $stmt = $this->conexao_erp->prepare($sql);
            $stmt->execute();
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (Exception $e) {
            throw new Exception("Erro ao executar SQL do ERP: " . $e->getMessage());
        }
    }
    
    /**
     * PROCESSAR REGISTROS (Detectar INSERT/UPDATE)
     * Modelo read-only: Apenas INSERT e UPDATE
     * DELETE é detectado na Fase 2 por ausência
     */
    private function processarRegistros($registros, $config) {
        $qtprocessados = 0;
        $qterro = 0;
        
        // Processar em lotes
        foreach (array_chunk($registros, self::TAMANHO_LOTE) as $lote) {
            foreach ($lote as $registro) {
                try {
                    // Calcular chave única do registro (necessário customizar por caso)
                    $chave_origem = $this->extrairChaveOrigem($registro);
                    
                    // Calcular hash do registro
                    $hash_novo = hash('sha256', json_encode($registro));
                    
                    // Verificar se registro já existe no rastreamento
                    $rast = $this->obterRastreamento($chave_origem);
                    
                    if (!$rast) {
                        // NOVO: Adicionar à fila
                        $this->adicionarFila('INSERT', $registro);
                        $qtprocessados++;
                    } elseif ($hash_novo !== $rast['hash_dados']) {
                        // MODIFICADO: Adicionar UPDATE à fila
                        $this->adicionarFila('UPDATE', $registro, $rast['cdregistro_destino']);
                        $qtprocessados++;
                    }
                    // Senão: nenhuma mudança, skip
                    
                } catch (Exception $e) {
                    $this->registrarErroRastreamento($chave_origem, $e->getMessage());
                    $qterro++;
                }
            }
        }
        
        // Atualizar log
        $this->atualizarRegistroLog($qtprocessados, $qterro);
        
        return ['processados' => $qtprocessados, 'erros' => $qterro];
    }
    
    /**
     * EXTRAIR CHAVE ÚNICA DO REGISTRO
     * CUSTOMIZAÇÃO NECESSÁRIA: Adaptар conforme seu modelo de dados
     */
    private function extrairChaveOrigem($registro) {
        // Exemplo: usar 'id' ou 'codigo' do ERP como chave
        return $registro['id'] ?? $registro['codigo'] ?? md5(json_encode($registro));
    }
    
    /**
     * OBTER RASTREAMENTO DE REGISTRO
     */
    private function obterRastreamento($chave_origem) {
        $stmt = $this->pdo->prepare("
            SELECT * FROM back.integracao_rastreamento
            WHERE cdintegracao = :integ AND chave_origem = :chave AND flstatus != 'deletado'
        ");
        $stmt->execute([
            ':integ' => $this->cdintegracao,
            ':chave' => $chave_origem
        ]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    /**
     * SINCRONIZAR DELEÇÕES (Read-Only)
     * Modelo: Detecta registros deletados no ERP por AUSÊNCIA
     * Em ERP read-only, quando usuário deleta um registro, ele desaparece do banco
     */
    private function sincronizarDeletadosReadOnly($config) {
        error_log("[SincronizacaoERP] Fase 2: Detectar deleções (read-only)");
        
        // Buscar TODAS as chaves que ainda existem no ERP
        try {
            $stmt = $this->conexao_erp->query("SELECT id as chave_origem FROM lancamentos ORDER BY id");
            $chaves_erp = [];
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $chaves_erp[] = $row['chave_origem'];
            }
        } catch (Exception $e) {
            error_log("[SincronizacaoERP] Erro ao buscar chaves do ERP: " . $e->getMessage());
            return 0;
        }
        
        // Buscar chaves que estão ativas no rastreamento
        $stmt = $this->pdo->prepare("
            SELECT chave_origem, cdregistro_destino
            FROM back.integracao_rastreamento
            WHERE cdintegracao = :integ AND flstatus = 'ativo'
        ");
        $stmt->execute([':integ' => $this->cdintegracao]);
        
        $map_rastreamento = [];
        $chaves_rastreadas = [];
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $chaves_rastreadas[] = $row['chave_origem'];
            $map_rastreamento[$row['chave_origem']] = $row['cdregistro_destino'];
        }
        
        // Encontrar diferença: quais estavam em rastreamento mas NÃO estão mais no ERP?
        $chaves_deletadas = array_diff($chaves_rastreadas, $chaves_erp);
        
        // Para cada chave deletada, adicionar DELETE à fila
        $qt_deletados = 0;
        foreach ($chaves_deletadas as $chave_deletada) {
            try {
                $cdregistro_destino = $map_rastreamento[$chave_deletada];
                
                // Adicionar DELETE à fila
                $this->adicionarFila(
                    'DELETE',
                    [
                        'cdregistro_destino' => $cdregistro_destino,
                        'chave_origem' => $chave_deletada,
                        'motivo' => 'Deletado no ERP'
                    ]
                );
                
                // Marcar como deletado no rastreamento
                $stmt_upd = $this->pdo->prepare("
                    UPDATE back.integracao_rastreamento
                    SET flstatus = 'deletado', dthdelecao = NOW()
                    WHERE cdintegracao = :integ AND chave_origem = :chave
                ");
                $stmt_upd->execute([
                    ':integ' => $this->cdintegracao,
                    ':chave' => $chave_deletada
                ]);
                
                $qt_deletados++;
            } catch (Exception $e) {
                error_log("Erro ao processar deleção de $chave_deletada: " . $e->getMessage());
            }
        }
        
        return $qt_deletados;
    }
    
    /**
     * ADICIONAR À FILA DE PROCESSAMENTO
     */
    private function adicionarFila($tipo_op, $dados, $cdregistro_destino = null) {
        $stmt = $this->pdo->prepare("
            INSERT INTO back.integracao_fila 
            (cdintegracao, tipo_operacao, dados_json, flprioridade, flstatus)
            VALUES (:integ, :tipo, :dados, 5, 'pendente')
        ");
        
        return $stmt->execute([
            ':integ' => $this->cdintegracao,
            ':tipo' => $tipo_op,
            ':dados' => json_encode($dados)
        ]);
    }
    

    
    /**
     * REGISTRAR RASTREAMENTO
     */
    private function registrarRastreamento($chave, $hash, $tabela, $cdregistro, $status = 'ativo') {
        $stmt = $this->pdo->prepare("
            INSERT INTO back.integracao_rastreamento 
            (cdintegracao, chave_origem, hash_dados, cdtabela_destino, cdregistro_destino, flstatus)
            VALUES (:integ, :chave, :hash, :tab, :cdreg, :status)
            ON CONFLICT (cdintegracao, chave_origem, cdtabela_destino) 
            DO UPDATE SET hash_dados = :hash, flstatus = :status, dthmodificacao = NOW()
        ");
        
        return $stmt->execute([
            ':integ' => $this->cdintegracao,
            ':chave' => $chave,
            ':hash' => $hash,
            ':tab' => $tabela,
            ':cdreg' => $cdregistro,
            ':status' => $status
        ]);
    }
    
    /**
     * REGISTRAR ERRO
     */
    private function registrarErro($mensagem) {
        if ($this->cdintegracao_log) {
            $stmt = $this->pdo->prepare("
                UPDATE back.integracao_log
                SET flstatus = 'erro', dthfim = NOW(), dsmensagem_erro = :msg
                WHERE cdintegracao_log = :id
            ");
            $stmt->execute([
                ':id' => $this->cdintegracao_log,
                ':msg' => substr($mensagem, 0, 500)
            ]);
        }
    }
    
    /**
     * REGISTRAR ERRO NO RASTREAMENTO
     */
    private function registrarErroRastreamento($chave, $mensagem) {
        $stmt = $this->pdo->prepare("
            INSERT INTO back.integracao_rastreamento 
            (cdintegracao, chave_origem, flstatus, dsmensagem_erro)
            VALUES (:integ, :chave, 'erro', :msg)
            ON CONFLICT (cdintegracao, chave_origem, cdtabela_destino) 
            DO UPDATE SET flstatus = 'erro', dsmensagem_erro = :msg
        ");
        
        $stmt->execute([
            ':integ' => $this->cdintegracao,
            ':chave' => $chave,
            ':msg' => substr($mensagem, 0, 500)
        ]);
    }
    
    /**
     * ATUALIZAR REGISTRO LOG
     */
    private function atualizarRegistroLog($qt_processados, $qt_erros) {
        $stmt = $this->pdo->prepare("
            UPDATE back.integracao_log
            SET qtregistros_processados = :qt, qtregistros_erro = :err
            WHERE cdintegracao_log = :id
        ");
        $stmt->execute([
            ':qt' => $qt_processados,
            ':err' => $qt_erros,
            ':id' => $this->cdintegracao_log
        ]);
    }
    
    /**
     * FINALIZAR SINCRONIZAÇÃO
     */
    private function finalizarSincronizacao($resultado) {
        $status = ($resultado['erros'] ?? 0) > 0 ? 'erro' : 'sucesso';

        // ✨ ETAPA 2: Consolidação dos dados (Refresh das Materialized Views)
        // Só considerar sucesso se o refresh retornar OK.
        if ($status === 'sucesso') {
            try {
                $consolidacao = $this->refreshMaterializedViews();
                $resultado['consolidacao'] = $consolidacao;

                if (empty($consolidacao['ok'])) {
                    $status = 'erro';
                }
            } catch (Exception $e) {
                error_log("Erro ao consolidar materialized views: " . $e->getMessage());
                $resultado['consolidacao_erro'] = $e->getMessage();
                $status = 'erro';
            }
        }

        // Atualiza log com o status final (já considerando consolidação)
        $stmt = $this->pdo->prepare("
            UPDATE back.integracao_log
            SET flstatus = :status, dthfim = NOW()
            WHERE cdintegracao_log = :id
        ");
        $stmt->execute([
            ':status' => $status,
            ':id' => $this->cdintegracao_log
        ]);

        // Atualizar data da última sincronização bem-sucedida apenas se tudo concluiu
        if ($status === 'sucesso') {
            $stmtConfig = $this->pdo->prepare("
                UPDATE back.integracao_config_sinc
                SET ultima_sinc_sucesso = NOW(), proxima_sinc_agendada = NOW() + INTERVAL '1 hour'
                WHERE cdintegracao = :integ AND cdempresa = :emp
            ");
            $stmtConfig->execute([
                ':integ' => $this->cdintegracao,
                ':emp' => $this->cdempresa
            ]);
        }
        
        return [
            'ok' => $status === 'sucesso',
            'msg' => "Sincronização {$status}",
            'cdintegracao_log' => $this->cdintegracao_log,
            'resultado' => $resultado
        ];
    }
    
    /**
     * Refresh das Materialized Views para consolidação de dados
     * Executado após sincronização bem-sucedida
     */
    private function refreshMaterializedViews(): array {
        $views = [
            'back.v_realizado_conta_mes_por_dia',
            'back.v_realizado_conta_mes',
            'back.v_orcado_conta_mes'
        ];
        
        $resultado = [
            'ok' => true,
            'views_atualizadas' => 0,
            'mensagens' => []
        ];
        
        foreach ($views as $view) {
            try {
                try {
                    $sql = "REFRESH MATERIALIZED VIEW CONCURRENTLY {$view}";
                    $this->pdo->exec($sql);
                    $resultado['views_atualizadas']++;
                    $resultado['mensagens'][] = "✓ {$view} consolidada";
                } catch (Exception $eConcurrent) {
                    $sql = "REFRESH MATERIALIZED VIEW {$view}";
                    $this->pdo->exec($sql);
                    $resultado['views_atualizadas']++;
                    $resultado['mensagens'][] = "⚠ {$view} consolidada (fallback sem CONCURRENTLY)";
                }
            } catch (Exception $e) {
                $resultado['ok'] = false;
                $resultado['mensagens'][] = "✗ Erro ao consolidar {$view}: " . $e->getMessage();
            }
        }
        
        return $resultado;
    }
}
?>
