<?php
require_once(dirname(__DIR__) . '/config/db.php');
require_once(dirname(__DIR__) . '/lib/origem_sync_lib.php');

ini_set('display_errors', '0');
error_reporting(E_ALL);

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
$cdempresa = $_SESSION['cdempresa'] ?? 1;
$usuario   = $_SESSION['usuario']   ?? null;

/**
 * Refresh das Materialized Views para consolidação dos dados.
 * IMPORTANTE: REFRESH MATERIALIZED VIEW CONCURRENTLY não pode rodar dentro
 * de um transaction block no PostgreSQL.
 */
function refreshMaterializedViewsRealizado(PDO $pdo): 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}";
        $pdo->exec($sql);
        $resultado['views_atualizadas']++;
        $resultado['mensagens'][] = "✓ {$view} consolidada";
      } catch (Exception $eConcurrent) {
        $sql = "REFRESH MATERIALIZED VIEW {$view}";
        $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;
}

/**
 * Log de execução de integração (1 linha por execução),
 * com totais e resumo, ao invés de log por registro.
 */
function registrarIntegracaoLogExecucao(
  PDO $pdo,
  int $cdintegracao,
  ?int $cdempresa,
  ?int $cdusuario,
  string $tpintegracao,
  int $florigem,
  ?string $idlote,
  ?string $dtperiodoInicio,
  ?string $dtperiodoFim,
  string $dtexecInicio,
  ?string $dtexecFim,
  string $status,
  int $qtdInseridos,
  int $qtdIgnorados,
  int $qtdErros,
  array $ignoradosMotivos = [],
  array $cdorigensErro = [],
  ?string $mensagem = null
): void {
  try {
    static $stmt = null;
    if ($stmt === null) {
      $stmt = $pdo->prepare('
        INSERT INTO back.integracao_log (
          cdintegracao, cdempresa, cdusuario, tpintegracao, florigem,
          idlote, dtperiodo_inicio, dtperiodo_fim,
          dtexec_inicio, dtexec_fim,
          status, qtd_inseridos, qtd_ignorados, qtd_erros,
          motivos_ignorados, cdsorigem_erro, mensagem
        ) VALUES (
          :cdintegracao, :cdempresa, :cdusuario, :tpintegracao, :florigem,
          :idlote, :dtperiodo_inicio, :dtperiodo_fim,
          :dtexec_inicio, :dtexec_fim,
          :status, :qtd_inseridos, :qtd_ignorados, :qtd_erros,
          :motivos_ignorados, :cdsorigem_erro, :mensagem
        )
      ');
    }

    $motivosJson = !empty($ignoradosMotivos) ? json_encode($ignoradosMotivos, JSON_UNESCAPED_UNICODE) : null;
    $cdorigensStr = '';
    if (!empty($cdorigensErro)) {
      $uniq = array_values(array_unique(array_filter(array_map('strval', $cdorigensErro))));
      $cdorigensStr = implode(',', $uniq);
    }

    $stmt->execute([
      ':cdintegracao'      => $cdintegracao,
      ':cdempresa'         => $cdempresa,
      ':cdusuario'         => $cdusuario,
      ':tpintegracao'      => $tpintegracao,
      ':florigem'          => $florigem,
      ':idlote'            => $idlote,
      ':dtperiodo_inicio'  => $dtperiodoInicio,
      ':dtperiodo_fim'     => $dtperiodoFim,
      ':dtexec_inicio'     => $dtexecInicio,
      ':dtexec_fim'        => $dtexecFim,
      ':status'            => $status,
      ':qtd_inseridos'     => $qtdInseridos,
      ':qtd_ignorados'     => $qtdIgnorados,
      ':qtd_erros'         => $qtdErros,
      ':motivos_ignorados' => $motivosJson,
      ':cdsorigem_erro'    => $cdorigensStr !== '' ? $cdorigensStr : null,
      ':mensagem'          => $mensagem,
    ]);
  } catch (Throwable $e) {
    // Erro de log não pode quebrar sincronização
    error_log('Falha ao registrarIntegracaoLogExecucao: ' . $e->getMessage());
  }
}

/**
 * Inserção em lote de registros em back.realizado com RETURNING,
 * evitando 1 INSERT por linha. Mantém a transação ativa.
 *
 * Para performance, o log de sucesso detalhado é feito apenas
 * quando a integração é "manual" (disparada pela tela). Nas
 * integrações automáticas, recomenda-se usar um log consolidado
 * após a execução.
 */
function flushBatchInsertRealizado(PDO $pdo, array &$batch, int $cdintegracao, string $tpintegracao, int $appUserInt, int &$inseridos): void {
  if (empty($batch)) {
    return;
  }

  // Ordem das colunas deve ser idêntica em todos os registros do batch
  $cols = [
    'cdempresa', 'cdexercicio', 'cdcentrocusto', 'cdplanocontabil',
    'mesref', 'anoref', 'tpmovimento', 'valor', 'dscomplemento',
    'dtmovimento', 'cdintegracao', 'cdorigem', 'florigem', 'json_origem',
    'flcancelado', 'dtcriacao', 'cdpessoa', 'idlote'
  ];

  $colCount = count($cols);
  $valuesParts = [];
  $params = [];

  foreach ($batch as $row) {
    $valuesParts[] = '(' . implode(',', array_fill(0, $colCount, '?')) . ')';

    $params[] = $row['cdempresa'];
    $params[] = $row['cdexercicio'];
    $params[] = $row['cdcentrocusto'];
    $params[] = $row['cdplanocontabil'];
    $params[] = $row['mesref'];
    $params[] = $row['anoref'];
    $params[] = $row['tpmovimento'];
    $params[] = $row['valor'];
    $params[] = $row['dscomplemento'];
    $params[] = $row['dtmovimento'];
    $params[] = $row['cdintegracao'];
    $params[] = $row['cdorigem'];
    $params[] = 2; // florigem fixo para ERP
    $params[] = $row['json_origem'];
    $params[] = 0; // flcancelado
    $params[] = $row['dtcriacao'];
    $params[] = 0; // cdpessoa
    $params[] = $row['idlote'];
  }

  $sql = 'INSERT INTO back.realizado (' . implode(',', $cols) . ')
           VALUES ' . implode(',', $valuesParts) . '';

  $stmt = $pdo->prepare($sql);
  $stmt->execute($params);

  // Incrementa total de inseridos com base no tamanho do batch
  $inseridos += count($batch);

  // Limpa o batch após processar
  $batch = [];
}

function json_out($arr, $code = 200) {
  http_response_code($code);
  header('Content-Type: application/json; charset=utf-8');
  echo json_encode($arr, JSON_UNESCAPED_UNICODE);
  exit;
}

// Converte warnings em exception para não quebrar JSON
set_error_handler(function($severity, $message, $file, $line) {
  $loc = basename($file) . ':' . $line;
  throw new ErrorException("$message @ $loc", 0, $severity, $file, $line);
});

// Garante JSON em exceções não tratadas
set_exception_handler(function($e) {
  $loc = basename($e->getFile()) . ':' . $e->getLine();
  $msg = $e->getMessage();
  json_out(['ok'=>false,'msg'=>"Erro interno ($loc): $msg"], 500);
});

/**
 * Converte qualquer tipo (incluindo array) para string segura.
 */
function to_scalar($v): string {
  if ($v === null) return '';

  // Se vier array (inclusive aninhado), achata tudo com segurança
  if (is_array($v)) {
    $out = [];
    $stack = [$v];

    while ($stack) {
      $cur = array_pop($stack);
      foreach ($cur as $item) {
        if (is_array($item)) {
          $stack[] = $item;
        } elseif (is_object($item)) {
          $out[] = json_encode($item, JSON_UNESCAPED_UNICODE);
        } elseif ($item === null) {
          $out[] = '';
        } elseif (is_bool($item)) {
          $out[] = $item ? '1' : '0';
        } else {
          $out[] = (string)$item;
        }
      }
    }
    return trim(implode(' ', $out));
  }

  // Objetos
  if (is_object($v)) {
    return trim(json_encode($v, JSON_UNESCAPED_UNICODE));
  }

  // Escalares
  if (is_bool($v)) return $v ? '1' : '0';

  return trim((string)$v);
}


function csv_escape($v) {
  $v = to_scalar($v);
  $v = str_replace('"', '""', $v);
  return '"' . $v . '"';
}

function month_cols() {
  return [
    1=>'Jan',2=>'Fev',3=>'Mar',4=>'Abr',5=>'Mai',6=>'Jun',
    7=>'Jul',8=>'Ago',9=>'Set',10=>'Out',11=>'Nov',12=>'Dez'
  ];
}

function origem_label($o) {
  if ($o === null) return '-';
  $o = (int)$o;
  if ($o === 0) return 'Manual';
  if ($o === 1) return 'Excel';
  if ($o === 2) return 'ERP';
  return 'Outro';
}

function parse_number_br($value) {
  $v = trim(to_scalar($value));
  if ($v === '') return null;

  if (strpos($v, ',') !== false && strpos($v, '.') !== false) {
    $v = str_replace('.', '', $v);
    $v = str_replace(',', '.', $v);
  } else {
    if (strpos($v, ',') !== false) $v = str_replace(',', '.', $v);
  }
  return is_numeric($v) ? (float)$v : null;
}

function uuid_v4() {
  $data = random_bytes(16);
  $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
  $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
  return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

function tratarErroBD(Exception $e) {
  $msg = $e->getMessage();
  
  // Erros de constraint (chave estrangeira, unique, etc)
  if (strpos($msg, 'foreign key') !== false || strpos($msg, 'CONSTRAINT') !== false) {
    return 'Erro: Referência inválida no banco de dados. Verifique se o exercício, centro de custo ou conta contábil existe.';
  }
  
  // Erro de valor inválido
  if (strpos($msg, 'Invalid') !== false || strpos($msg, 'invalid') !== false) {
    return 'Erro: Valor inválido enviado. Verifique o formato dos dados.';
  }
  
  // Erro de tipo de dado
  if (strpos($msg, 'SQLSTATE[42') !== false) {
    return 'Erro: Campo ou tabela não encontrada no banco de dados.';
  }
  
  // Erro genérico de SQL
  if (strpos($msg, 'SQLSTATE') !== false) {
    // Extrai código do erro
    if (preg_match('/SQLSTATE\[(\w+)\]/', $msg, $m)) {
      $code = $m[1];
      if ($code === '23502') return 'Erro: Campo obrigatório não preenchido.';
      if ($code === '23505') return 'Erro: Valor duplicado não permitido.';
      if ($code === '23503') return 'Erro: Referência inválida (Verifique o Código de conta ou centro de custo).';
    }
  }
  
  // Fallback
  return 'Erro no banco de dados: ' . substr($msg, 0, 100) . '...';
}

// Se estiver sendo incluído como biblioteca (HOME_ACTIONS_LOADING), não execute o switch
if (defined('HOME_ACTIONS_LOADING')) {
  return; // Sair sem executar o switch abaixo
}

$ajax = $_GET['ajax'] ?? '';
if ($ajax === '') { http_response_code(204); exit; }

switch ($ajax) {

  // compatibilidade
  case 'listar':
  case 'listar_lotes':
    listarLotes();
    break;

  case 'listar_lancamentos':
  case 'listar_lancamentos_lote':
    listarLancamentosLote();
    break;
  case 'listar_lancamentos_lote_arvore':
    listarLancamentosLoteArvore();
    break;
  case 'listar_lancamentos_detalhe_conta':
    listarLancamentosDetalheConta();
    break;
  case 'confirmar_excel':
  case 'confirmar_importacao':
    try {
      confirmarImportacao();
    } catch (Exception $e) {
      error_log("Erro em confirmarImportacao: " . $e->getMessage());
      json_out(['ok'=>false,'msg'=>tratarErroBD($e)], 500);
    }
    break;

  // novos
  case 'export_modelo_csv':
    exportModeloCSVContasAnaliticas();
    break;

  case 'export_realizado_csv':
    exportRealizadoCSV();
    break;

  case 'importar_csv':
    try {
      importarCSV();
    } catch (Exception $e) {
      error_log("Erro em importarCSV: " . $e->getMessage());
      json_out(['ok'=>false,'msg'=>tratarErroBD($e)], 500);
    }
    break;

  case 'preview_lote':
    previewLote();
    break;

  case 'cancelar':
    cancelar();
    break;

  case 'cancelar_lote':
    cancelarLote();
    break;

  case 'sincronizar_periodo':
    sincronizarPeriodo();
    break;

  default:
    json_out(['ok' => false, 'msg' => 'Ação inválida'], 400);
}

/* ============================================================
 * LISTAR LOTES
 * ============================================================ */
function listarLotes() {
  global $pdo, $cdempresa;
  
  // Resumo leve por exercício: não varre toda a tabela de realizado.
  // Mostra todos os exercícios ativos da empresa logada, com a data
  // e o usuário da última sincronização registrada em integracao_log
  // e um flag indicando se já existem lançamentos no realizado.

  $params = [':emp' => $cdempresa];
  $whereEx = ['e.cdempresa = :emp', 'e.flativo = 0'];

  if (!empty($_GET['anoref'])) {
    $whereEx[] = 'e.anoref = :ano';
    $params[':ano'] = (int)$_GET['anoref'];
  }

  // Filtros opcionais para checar se existe realizado
  $cdcentro = !empty($_GET['cdcentrocusto']) ? (int)$_GET['cdcentrocusto'] : null;
  $florigem = (isset($_GET['florigem']) && $_GET['florigem'] !== '') ? (int)$_GET['florigem'] : null;
  $dtini    = !empty($_GET['dtinicio']) ? $_GET['dtinicio'] : null;
  $dtfim    = !empty($_GET['dtfim'])    ? $_GET['dtfim']    : null;

  $sql = '
    SELECT
      e.cdexercicio,
      e.anoref,
      e.descricao,
      lg.dtexec_fim    AS dtultima_sync,
      lg.tpintegracao  AS tpintegracao,
      lg.idlote        AS idlote,
      p.dspessoa       AS usuario_sync,
      (rx.tem_lanc IS NOT NULL) AS tem_lanc
    FROM back.exercicio e
    LEFT JOIN LATERAL (
      SELECT il.dtexec_fim, il.tpintegracao, il.cdusuario, il.idlote
        FROM back.integracao_log il
       WHERE il.cdempresa = e.cdempresa
         AND il.dtperiodo_inicio IS NOT NULL
         AND EXTRACT(YEAR FROM il.dtperiodo_inicio) = e.anoref
       ORDER BY il.dtexec_fim DESC, il.cdintegracao_log DESC
       LIMIT 1
    ) lg ON true
    LEFT JOIN back.pessoa p ON p.cdpessoa = lg.cdusuario
    LEFT JOIN LATERAL (
      SELECT 1 AS tem_lanc
        FROM back.realizado r
       WHERE r.cdempresa = e.cdempresa
         AND r.anoref   = e.anoref
         AND r.flcancelado = 0
    ';

  // Aplica filtros de forma segura na subconsulta de existência
  if ($cdcentro !== null) {
    $sql .= ' AND r.cdcentrocusto = :cc';
    $params[':cc'] = $cdcentro;
  }
  if ($florigem !== null) {
    $sql .= ' AND r.florigem = :origem';
    $params[':origem'] = $florigem;
  }
  if ($dtini !== null) {
    $sql .= ' AND r.dtmovimento >= :dtini';
    $params[':dtini'] = $dtini;
  }
  if ($dtfim !== null) {
    $sql .= ' AND r.dtmovimento <= :dtfim';
    $params[':dtfim'] = $dtfim;
  }

  $sql .= '
       LIMIT 1
    ) rx ON true
    WHERE ' . implode(' AND ', $whereEx) . '
    ORDER BY e.anoref DESC
  ';

  $st = $pdo->prepare($sql);
  $st->execute($params);
  $rows = $st->fetchAll(PDO::FETCH_ASSOC);

  // Ajustes de apresentação
  foreach ($rows as &$r) {
    $r['origem_label'] = 'ERP'; // resumo focado na sincronização ERP
    $r['situacao_label'] = !empty($r['tem_lanc']) ? 'Com lançamentos' : 'Sem lançamentos';
  }

  json_out($rows);
}

/* ============================================================
 * LISTAR LANÇAMENTOS DO LOTE
 * ============================================================ */
function listarLancamentosLote() {
  global $pdo, $cdempresa;

  $idlote = $_GET['idlote'] ?? null;
  if (!$idlote) json_out(['ok'=>false,'msg'=>'Lote não informado'], 400);
  $where  = ["r.cdempresa = :emp", "r.flcancelado = 0", "r.idlote = :lote"];
  $params = [':emp' => $cdempresa, ':lote' => $idlote];

  if (!empty($_GET['anoref'])) {
    $where[] = "r.anoref = :ano";
    $params[':ano'] = (int)$_GET['anoref'];
  }
  if (!empty($_GET['cdcentrocusto'])) {
    $where[] = "r.cdcentrocusto = :cc";
    $params[':cc'] = (int)$_GET['cdcentrocusto'];
  }
  if (isset($_GET['florigem']) && $_GET['florigem'] !== '') {
    $where[] = "r.florigem = :origem";
    $params[':origem'] = (int)$_GET['florigem'];
  }
  if (!empty($_GET['dtinicio'])) {
    $where[] = "r.dtmovimento >= :dtini";
    $params[':dtini'] = $_GET['dtinicio'];
  }
  if (!empty($_GET['dtfim'])) {
    $where[] = "r.dtmovimento <= :dtfim";
    $params[':dtfim'] = $_GET['dtfim'];
  }

  $sql = "
    SELECT
      r.cdrealizado,
      r.anoref,
      r.mesref,
      r.valor,
      r.dtcriacao,
      cc.dscentro,
      r.cdplanocontabil,
      pc.cdconta,
      pc.dsconta
    FROM back.realizado r
    JOIN back.centro_custo cc ON cc.cdcentrocusto = r.cdcentrocusto
    JOIN back.planocontabil pc ON pc.cdcontacontabil = r.cdplanocontabil
    WHERE " . implode(' AND ', $where) . "
    ORDER BY r.mesref, pc.cdconta
  ";

  $st = $pdo->prepare($sql);
  $st->execute($params);
  json_out($st->fetchAll(PDO::FETCH_ASSOC));
}

/* ============================================================
 * LISTAR LANÇAMENTOS DO LOTE EM FORMA DE ÁRVORE (PLANO CONTÁBIL)
 * ============================================================ */
function listarLancamentosLoteArvore() {
  global $pdo, $cdempresa;

  $idlote = $_GET['idlote'] ?? null;
  if (!$idlote) {
    json_out(['ok' => false, 'msg' => 'Lote não informado'], 400);
  }

  // Filtros básicos iguais ao listarLancamentosLote
  $where  = ["r.cdempresa = :emp", "r.flcancelado = 0", "r.idlote = :lote"];
  $params = [':emp' => $cdempresa, ':lote' => $idlote];

  if (!empty($_GET['anoref'])) {
    $where[] = "r.anoref = :ano";
    $params[':ano'] = (int)$_GET['anoref'];
  }
  if (!empty($_GET['cdcentrocusto'])) {
    $where[] = "r.cdcentrocusto = :cc";
    $params[':cc'] = (int)$_GET['cdcentrocusto'];
  }
  if (isset($_GET['florigem']) && $_GET['florigem'] !== '') {
    $where[] = "r.florigem = :origem";
    $params[':origem'] = (int)$_GET['florigem'];
  }
  if (!empty($_GET['dtinicio'])) {
    $where[]          = "r.dtmovimento >= :dtini";
    $params[':dtini'] = $_GET['dtinicio'];
  }
  if (!empty($_GET['dtfim'])) {
    $where[]          = "r.dtmovimento <= :dtfim";
    $params[':dtfim'] = $_GET['dtfim'];
  }

  // Agregar valores realizados por conta contábil e mês, usando
  // crédito - débito (tpmovimento) para formar o saldo do mês.
  $sqlReal = '
    SELECT
      r.cdplanocontabil,
      r.mesref,
      SUM(CASE WHEN r.tpmovimento = \'C\' THEN r.valor ELSE -r.valor END) AS valor
    FROM back.realizado r
    WHERE ' . implode(' AND ', $where) . '
    GROUP BY r.cdplanocontabil, r.mesref
  ';

  $stReal = $pdo->prepare($sqlReal);
  $stReal->execute($params);

  $mapValores = [];
  while ($row = $stReal->fetch(PDO::FETCH_ASSOC)) {
    $cdcc = (int)$row['cdplanocontabil'];
    $mes  = (int)$row['mesref'];
    $val  = (float)$row['valor'];

    if (!isset($mapValores[$cdcc])) {
      $mapValores[$cdcc] = array_fill(0, 12, 0.0);
    }
    if ($mes >= 1 && $mes <= 12) {
      $mapValores[$cdcc][$mes - 1] += $val;
    }
  }

  // Totais por mês e total geral calculados diretamente no banco
  // (garante que o rodapé da tela bata exatamente com o total do lote).
  $sqlTotais = '
    SELECT
      r.mesref,
      SUM(CASE WHEN r.tpmovimento = \'C\' THEN r.valor ELSE -r.valor END) AS valor
    FROM back.realizado r
    WHERE ' . implode(' AND ', $where) . '
    GROUP BY r.mesref
  ';

  $stTotais = $pdo->prepare($sqlTotais);
  $stTotais->execute($params);

  $totaisMes = array_fill(1, 12, 0.0);
  $totalGeral = 0.0;
  while ($row = $stTotais->fetch(PDO::FETCH_ASSOC)) {
    $mes = (int)$row['mesref'];
    $val = (float)$row['valor'];
    if ($mes >= 1 && $mes <= 12) {
      $totaisMes[$mes] += $val;
      $totalGeral += $val;
    }
  }

    // Carregar plano de contas somente ativo (melhor desempenho)
  $sqlPlano = '
    SELECT cdcontacontabil,
           cdconta,
           cdcontapai,
           dsconta,
           fltipo
      FROM back.planocontabil
      WHERE flativo = 0
     ORDER BY cdconta ASC
  ';

  $contas = $pdo->query($sqlPlano)->fetchAll(PDO::FETCH_ASSOC) ?: [];

  $dados = [];
  foreach ($contas as $c) {
    $cdcc = (int)$c['cdcontacontabil'];
    $valores = $mapValores[$cdcc] ?? array_fill(0, 12, 0.0);

    $dados[] = [
      'cdcontacontabil' => $cdcc,
      'cdconta'         => $c['cdconta'],
      'cdcontapai'      => $c['cdcontapai'] ?: null,
      'dsconta'         => $c['dsconta'],
      'fltipo'          => $c['fltipo'],
      'valores'         => $valores,
    ];
  }

  json_out([
    'ok'     => true,
    'dados'  => $dados,
    'totais' => [
      'meses'       => $totaisMes,
      'total_geral' => $totalGeral,
    ],
  ]);
}

/* ============================================================
 * LISTAR LANÇAMENTOS DETALHADOS POR CONTA (DENTRO DO LOTE)
 * ============================================================ */
function listarLancamentosDetalheConta() {
  global $pdo, $cdempresa;

  $idlote          = $_GET['idlote']          ?? null;
  $cdconta         = $_GET['cdconta']         ?? null;
  $cdplanocontabil = $_GET['cdplanocontabil'] ?? null;

  if (!$idlote || (!$cdconta && !$cdplanocontabil)) {
    json_out(['ok' => false, 'msg' => 'Lote ou conta não informados'], 400);
  }

  $where = [
    'r.cdempresa = :emp',
    'r.flcancelado = 0',
    'r.idlote = :lote'
  ];

  $params = [
    ':emp'     => $cdempresa,
    ':lote'    => $idlote,
  ];

  if ($cdplanocontabil) {
    // Conta analítica: filtra diretamente pelo cdplanocontabil informado
    $where[]                    = 'r.cdplanocontabil = :cdplanocontabil';
    $params[':cdplanocontabil'] = $cdplanocontabil;
  } else {
    // Conta sintética: traz lançamentos de todas as contas cuja cdconta
    // seja exatamente a informada OU descendente (prefixo "cdconta.%").
    $where[]             = '(pc.cdconta = :cdconta OR pc.cdconta LIKE :cdconta_prefix)';
    $params[':cdconta']        = $cdconta;
    $params[':cdconta_prefix'] = $cdconta . '.%';
  }

  if (!empty($_GET['dtinicio'])) {
    $where[]         = 'r.dtmovimento >= :dtini';
    $params[':dtini'] = $_GET['dtinicio'];
  }

  if (!empty($_GET['dtfim'])) {
    $where[]         = 'r.dtmovimento <= :dtfim';
    $params[':dtfim'] = $_GET['dtfim'];
  }

  $sql = '
    SELECT
      r.cdrealizado,
      r.cdplanocontabil,
      r.dtmovimento,
      r.dscomplemento,
      r.valor,
      r.tpmovimento,
      pc.cdconta,
      pc.dsconta
    FROM back.realizado r
    JOIN back.planocontabil pc ON pc.cdcontacontabil = r.cdplanocontabil
    WHERE ' . implode(' AND ', $where) . '
    ORDER BY pc.cdconta, r.dtmovimento, r.cdrealizado
  ';

  $st = $pdo->prepare($sql);
  $st->execute($params);
  json_out($st->fetchAll(PDO::FETCH_ASSOC));
}

/* ============================================================
 * EXPORT MODELO CSV (contas analíticas ativas + meses zerados)
 * ============================================================ */
function exportModeloCSVContasAnaliticas() {
  global $pdo, $cdempresa;

  $anoref = (int)($_GET['anoref'] ?? 0);
  $cdcentro = (int)($_GET['cdcentrocusto'] ?? 0);

  if (!$anoref || !$cdcentro) {
    json_out(['ok'=>false,'msg'=>'Informe anoref e cdcentrocusto para exportar o modelo.'], 400);
  }

  $st = $pdo->prepare("
    SELECT dscentro
      FROM back.centro_custo
     WHERE cdcentrocusto = :cc
       AND flativo = 0
     LIMIT 1
  ");
  $st->execute([':cc'=>$cdcentro]);
  $dscentro = (string)($st->fetchColumn() ?: '');

  if ($dscentro === '') {
    json_out(['ok'=>false,'msg'=>'Centro de custo inválido/inativo.'], 400);
  }

  $st = $pdo->prepare("
    SELECT cdcontacontabil, cdconta, dsconta
      FROM back.planocontabil
     WHERE flativo = 0
       AND fltipo = 'A'
     ORDER BY cdconta
  ");
  $st->execute();
  $contas = $st->fetchAll(PDO::FETCH_ASSOC);

  if (!$contas) {
    json_out(['ok'=>false,'msg'=>'Nenhuma conta analítica ativa encontrada no plano de contas.'], 400);
  }

  $months = month_cols();
  $sep = ';';

  // Try to include company apelido (short name) in filename
  $st = $pdo->prepare("SELECT dsapelido FROM back.empresa WHERE cdempresa = :emp LIMIT 1");
  $st->execute([':emp'=>$cdempresa]);
  $apelido = (string)($st->fetchColumn() ?: '');
  $safe_apelido = $apelido ? preg_replace('/[^A-Za-z0-9_-]/', '_', $apelido) : '';

  header_remove('Content-Type');
  header('Content-Type: text/csv; charset=utf-8');
  $fname = 'modelo_realizado_' . $anoref . '_CC' . $cdcentro;
  if ($safe_apelido !== '') $fname .= '_' . $safe_apelido;
  $fname .= '.csv';
  header('Content-Disposition: attachment; filename="' . $fname . '"');

  $headers = ['CdEmpresa','Ano','CdCentro','Centro','CdContaContabil','CdConta','Conta'];
  foreach ($months as $m) $headers[] = $m;
  echo implode($sep, array_map('csv_escape', $headers)) . "\n";

  foreach ($contas as $c) {
    $line = [
      $cdempresa,
      $anoref,
      $cdcentro,
      $dscentro,
      $c['cdcontacontabil'],
      $c['cdconta'],
      $c['dsconta']
    ];
    for ($i=1;$i<=12;$i++) $line[] = 0;
    echo implode($sep, array_map('csv_escape', $line)) . "\n";
  }
  exit;
}

/* ============================================================
 * EXPORT REALIZADO PIVOTADO CSV
 * ============================================================ */
function exportRealizadoCSV() {
  global $pdo, $cdempresa;

  $anoref = (int)($_GET['anoref'] ?? 0);
  if (!$anoref) json_out(['ok'=>false,'msg'=>'Informe o exercício (anoref).'], 400);

  $sql = "
    SELECT
      cc.dscentro,
      pc.cdconta,
      pc.dsconta,
      SUM(CASE WHEN r.mesref = 1  THEN r.valor ELSE 0 END) AS jan,
      SUM(CASE WHEN r.mesref = 2  THEN r.valor ELSE 0 END) AS fev,
      SUM(CASE WHEN r.mesref = 3  THEN r.valor ELSE 0 END) AS mar,
      SUM(CASE WHEN r.mesref = 4  THEN r.valor ELSE 0 END) AS abr,
      SUM(CASE WHEN r.mesref = 5  THEN r.valor ELSE 0 END) AS mai,
      SUM(CASE WHEN r.mesref = 6  THEN r.valor ELSE 0 END) AS jun,
      SUM(CASE WHEN r.mesref = 7  THEN r.valor ELSE 0 END) AS jul,
      SUM(CASE WHEN r.mesref = 8  THEN r.valor ELSE 0 END) AS ago,
      SUM(CASE WHEN r.mesref = 9  THEN r.valor ELSE 0 END) AS set,
      SUM(CASE WHEN r.mesref = 10 THEN r.valor ELSE 0 END) AS out,
      SUM(CASE WHEN r.mesref = 11 THEN r.valor ELSE 0 END) AS nov,
      SUM(CASE WHEN r.mesref = 12 THEN r.valor ELSE 0 END) AS dez
    FROM back.realizado r
    JOIN back.centro_custo cc ON cc.cdcentrocusto = r.cdcentrocusto
    JOIN back.planocontabil pc ON pc.cdcontacontabil = r.cdplanocontabil
    WHERE r.cdempresa = :emp
      AND r.flcancelado = 0
      AND r.anoref = :ano
    GROUP BY cc.dscentro, pc.cdconta, pc.dsconta
    ORDER BY cc.dscentro, pc.cdconta
  ";

  $st = $pdo->prepare($sql);
  $st->execute([':emp'=>$cdempresa, ':ano'=>$anoref]);
  $rows = $st->fetchAll(PDO::FETCH_ASSOC);

  // Include company apelido (short name) in filename when possible
  $st = $pdo->prepare("SELECT dsapelido FROM back.empresa WHERE cdempresa = :emp LIMIT 1");
  $st->execute([':emp'=>$cdempresa]);
  $apelido = (string)($st->fetchColumn() ?: '');
  $safe_apelido = $apelido ? preg_replace('/[^A-Za-z0-9_-]/', '_', $apelido) : '';

  header_remove('Content-Type');
  header('Content-Type: text/csv; charset=utf-8');
  $fname = 'realizado_' . $anoref;
  if ($safe_apelido !== '') $fname .= '_' . $safe_apelido;
  $fname .= '.csv';
  header('Content-Disposition: attachment; filename="' . $fname . '"');

  $sep = ';';
  $months = month_cols();

  $headers = ['Ano','Centro','CdConta','Conta'];
  foreach ($months as $m) $headers[] = $m;
  echo implode($sep, array_map('csv_escape', $headers)) . "\n";

  foreach ($rows as $r) {
    $line = [$anoref, $r['dscentro'], $r['cdconta'], $r['dsconta']];
    $line[] = $r['jan'] ?? 0;
    $line[] = $r['fev'] ?? 0;
    $line[] = $r['mar'] ?? 0;
    $line[] = $r['abr'] ?? 0;
    $line[] = $r['mai'] ?? 0;
    $line[] = $r['jun'] ?? 0;
    $line[] = $r['jul'] ?? 0;
    $line[] = $r['ago'] ?? 0;
    $line[] = $r['set'] ?? 0;
    $line[] = $r['out'] ?? 0;
    $line[] = $r['nov'] ?? 0;
    $line[] = $r['dez'] ?? 0;
    echo implode($sep, array_map('csv_escape', $line)) . "\n";
  }
  exit;
}

/* ============================================================
 * IMPORTAR CSV -> back.realizado_temp (robusto)
 * ============================================================ */
function importarCSV() {
  global $pdo, $cdempresa, $usuario;

  // Garantir que as variáveis globais são escalares
  if (is_array($cdempresa)) $cdempresa = (int)reset($cdempresa);
  else $cdempresa = (int)$cdempresa;
  
  if (is_array($usuario)) $usuario = (string)reset($usuario);
  else $usuario = (string)$usuario;

  if (!isset($_FILES['arquivo']) || $_FILES['arquivo']['error'] !== UPLOAD_ERR_OK) {
    json_out(['ok'=>false,'msg'=>'Arquivo não enviado.'], 400);
  }

  $path = $_FILES['arquivo']['tmp_name'];
  $fh = fopen($path, 'r');
  if (!$fh) json_out(['ok'=>false,'msg'=>'Não foi possível ler o arquivo.'], 400);

  // Detecta separador pela primeira linha
  $firstLine = fgets($fh);
  if ($firstLine === false || trim($firstLine) === '') {
    fclose($fh);
    json_out(['ok'=>false,'msg'=>'Arquivo vazio.'], 400);
  }
  $sep = (substr_count($firstLine, ';') >= substr_count($firstLine, ',')) ? ';' : ',';
  rewind($fh);

  // Lê header
  $header = fgetcsv($fh, 0, $sep);
  if (!$header || !is_array($header)) {
    fclose($fh);
    json_out(['ok'=>false,'msg'=>'Header inválido no CSV.'], 400);
  }

  $header = array_map(function($h){
    $h = trim(to_scalar($h));
    $h = ltrim($h, "\xEF\xBB\xBF");
    return $h;
  }, $header);

  $idx = array_flip($header);

  $required = ['CdEmpresa','Ano','CdCentro','Centro','CdContaContabil','CdConta','Conta','Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
  foreach ($required as $col) {
    if (!isset($idx[$col])) {
      fclose($fh);
      json_out(['ok'=>false,'msg'=>"Coluna obrigatória ausente: $col"], 400);
    }
  }

  $idlote = uuid_v4();
  $mesCols = month_cols();

  $totalLinhas = 0;
  $tempRows = 0;
  $validos = 0;
  $invalidos = 0;

  // Rastrear duplicatas na mesma importação
  $duplicatas = []; // { "anoCSV-ccId-contaId": count }

  $ctxAno = null;
  $ctxCentro = null;
  $ctxCentroNome = null;

  $pdo->beginTransaction();

  $ins = $pdo->prepare("
    INSERT INTO back.realizado_temp
      (cdempresa, anoref, mesref, cdcentrocusto, cdplanocontabil, valor, flvalido, erro, idlote, usuario)
    VALUES
      (:emp, :ano, :mes, :cc, :conta, :valor, :ok, :erro, :lote, :usuario)
  ");

  $get = function(array $row, string $name) use ($idx) {
    $i = $idx[$name] ?? null;
    if ($i === null) return '';
    $val = $row[$i] ?? '';
    return trim(to_scalar($val));
  };

  while (($row = fgetcsv($fh, 0, $sep)) !== false) {
    if (!is_array($row) || (count($row) === 1 && trim(to_scalar($row[0])) === '')) continue;

    $totalLinhas++;

    $empCSV    = (int)$get($row, 'CdEmpresa');
    $anoCSV    = (int)$get($row, 'Ano');
    $cdCentro  = $get($row, 'CdCentro');
    $dsCentro  = $get($row, 'Centro');
    $cdContaId = $get($row, 'CdContaContabil');

    if ($empCSV && $empCSV !== (int)$cdempresa) {
      $ins->execute([
        ':emp' => (int)$cdempresa,
        ':ano' => (int)($anoCSV ?: 0),
        ':mes' => 1,
        ':cc' => null,
        ':conta' => null,
        ':valor' => 0.0,
        ':ok' => 0,
        ':erro' => "Empresa do CSV Incorreta ($empCSV) Esperado Empresa:($cdempresa)",
        ':lote' => (string)$idlote,
        ':usuario' => (string)$usuario
      ]);
      $invalidos++;
      continue;
    }

    if (!$anoCSV || $anoCSV < 2000) {
      $ins->execute([
        ':emp' => (int)$cdempresa,
        ':ano' => 0,
        ':mes' => 1,
        ':cc' => null,
        ':conta' => null,
        ':valor' => 0.0,
        ':ok' => 0,
        ':erro' => "Ano inválido/vazio no CSV",
        ':lote' => (string)$idlote,
        ':usuario' => (string)$usuario
      ]);
      $invalidos++;
      continue;
    }

    // Validar ano consistente na importação
    if ($ctxAno === null) {
      $ctxAno = $anoCSV;
    } elseif ($ctxAno !== $anoCSV) {
      $ins->execute([
        ':emp' => (int)$cdempresa,
        ':ano' => (int)$anoCSV,
        ':mes' => 1,
        ':cc' => null,
        ':conta' => null,
        ':valor' => 0.0,
        ':ok' => 0,
        ':erro' => "Anos mistos na importação. Todos os registros devem ser do mesmo exercício ($ctxAno). Encontrado: $anoCSV",
        ':lote' => (string)$idlote,
        ':usuario' => (string)$usuario
      ]);
      $invalidos++;
      continue;
    }

    if ($cdCentro === '' || !ctype_digit($cdCentro)) {
      $ins->execute([
        ':emp' => (int)$cdempresa,
        ':ano' => (int)$anoCSV,
        ':mes' => 1,
        ':cc' => null,
        ':conta' => null,
        ':valor' => 0.0,
        ':ok' => 0,
        ':erro' => "CdCentro inválido/vazio",
        ':lote' => (string)$idlote,
        ':usuario' => (string)$usuario
      ]);
      $invalidos++;
      continue;
    }

    if ($cdContaId === '' || !ctype_digit($cdContaId)) {
      $ins->execute([
        ':emp' => (int)$cdempresa,
        ':ano' => (int)$anoCSV,
        ':mes' => 1,
        ':cc' => (int)$cdCentro,
        ':conta' => null,
        ':valor' => 0.0,
        ':ok' => 0,
        ':erro' => "CdContaContabil inválido/vazio",
        ':lote' => (string)$idlote,
        ':usuario' => (string)$usuario
      ]);
      $invalidos++;
      continue;
    }

    // Validar duplicação de conta na mesma importação
    $chaveUnica = $anoCSV . '-' . (int)$cdCentro . '-' . (int)$cdContaId;
    if (isset($duplicatas[$chaveUnica])) {
      $duplicatas[$chaveUnica]++;
      $ins->execute([
        ':emp' => (int)$cdempresa,
        ':ano' => (int)$anoCSV,
        ':mes' => 1,
        ':cc' => (int)$cdCentro,
        ':conta' => (int)$cdContaId,
        ':valor' => 0.0,
        ':ok' => 0,
        ':erro' => "Conta contábil duplicada nesta importação. A mesma conta não pode aparecer em mais de uma linha.",
        ':lote' => (string)$idlote,
        ':usuario' => (string)$usuario
      ]);
      $invalidos++;
      continue;
    }
    $duplicatas[$chaveUnica] = 1;

    // Contexto do centro (para reference apenas)
    if ($ctxCentro === null) {
      $ctxCentro = (int)$cdCentro;
      $ctxCentroNome = $dsCentro;
    }

    $ccId = (int)$cdCentro;
    $contaId = (int)$cdContaId;

    foreach ($mesCols as $mes => $nomeMes) {
      // Garante que $nomeMes é string pura
      if (is_array($nomeMes)) $nomeMes = implode('', $nomeMes);
      $nomeMes = (string)$nomeMes;
      
      $valorRaw = $get($row, $nomeMes);
      if (is_array($valorRaw)) $valorRaw = implode('', $valorRaw);
      
      $valor = parse_number_br($valorRaw);
      if ($valor === null || !is_numeric($valor)) $valor = 0.0;
      $valor = (float)$valor;

      // Construir params com valores já validados
      $params = [
        ':emp' => (int)$cdempresa,
        ':ano' => (int)$anoCSV,
        ':mes' => (int)$mes,
        ':cc' => (int)$ccId,
        ':conta' => (int)$contaId,
        ':valor' => $valor,
        ':ok' => 1,
        ':erro' => null,
        ':lote' => (string)$idlote,
        ':usuario' => (string)$usuario
      ];

      try {
        $ins->execute($params);
      } catch (Exception $e) {
        $errMsg = $e->getMessage();
        error_log("Erro em importarCSV linha " . (int)$totalLinhas . ": " . $errMsg);
        
        // Tenta interpretar o erro para registrar na temp
        if (strpos($errMsg, 'foreign key') !== false) {
          $errUser = 'Referência inválida (exercício, centro ou conta não existe)';
        } elseif (strpos($errMsg, 'CONSTRAINT') !== false) {
          $errUser = 'Violação de restrição de banco de dados';
        } else {
          $errUser = substr($errMsg, 0, 100);
        }
        
        // Registra o erro na temp
        try {
          $errStmt = $pdo->prepare("
            INSERT INTO back.realizado_temp
              (cdempresa, anoref, mesref, cdcentrocusto, cdplanocontabil, valor, flvalido, erro, idlote, usuario)
            VALUES
              (:emp, :ano, :mes, :cc, :conta, :valor, 0, :erro, :lote, :usuario)
          ");
          $errStmt->execute([
            ':emp' => (int)$cdempresa,
            ':ano' => (int)$anoCSV,
            ':mes' => (int)$mes,
            ':cc' => (int)$ccId,
            ':conta' => (int)$contaId,
            ':valor' => $valor,
            ':erro' => $errUser,
            ':lote' => (string)$idlote,
            ':usuario' => (string)$usuario
          ]);
        } catch (Exception $e2) {
          // Se não conseguir registrar na temp, continua
        }
        
        $invalidos++;
        continue;
      }
      $tempRows++;
      $validos++;
    }
  }

  fclose($fh);
  $pdo->commit();

  $dsCentroDb = null;
  if ($ctxCentro) {
    $st = $pdo->prepare("SELECT dscentro FROM back.centro_custo WHERE cdcentrocusto = :cc LIMIT 1");
    $st->execute([':cc'=>$ctxCentro]);
    $dsCentroDb = $st->fetchColumn() ?: null;
  }

  json_out([
    'ok'=>true,
    'msg'=>'CSV importado para prévia.',
    'idlote'=>$idlote,
    'contexto'=>[
      'anoref'=>$ctxAno,
      'cdcentrocusto'=>$ctxCentro,
      'dscentro'=>$dsCentroDb ?: $ctxCentroNome
    ],
    'resumo'=>[
      'linhas_lidas'=>$totalLinhas,
      'registros_temp'=>$tempRows,
      'validos'=>$validos,
      'invalidos'=>$invalidos
    ]
  ]);
}

/* ============================================================
    FROM back.realizado_temp
  $sql = "
    SELECT
      r.cdrealizado,
      r.anoref,
      r.mesref,
      r.valor,
      r.dtcriacao,
      cc.dscentro,
      pc.cdconta,
      pc.dsconta
    FROM back.realizado r
    JOIN back.centro_custo cc ON cc.cdcentrocusto = r.cdcentrocusto
    JOIN back.planocontabil pc ON pc.cdcontacontabil = r.cdplanocontabil
    WHERE " . implode(' AND ', $where) . "
    ORDER BY r.mesref, pc.cdconta
  ";
    WHERE cdempresa = :emp AND idlote = :lote
  ");
  $sum->execute([':emp'=>$cdempresa, ':lote'=>$idlote]);
  $resumo = $sum->fetch(PDO::FETCH_ASSOC);

  $inv = $pdo->prepare("
    SELECT anoref, mesref, cdcentrocusto, cdplanocontabil, valor, erro
      FROM back.realizado_temp
     WHERE cdempresa = :emp AND idlote = :lote AND flvalido = 0
     ORDER BY cdtemp ASC
     LIMIT 20
  ");
  $inv->execute([':emp'=>$cdempresa, ':lote'=>$idlote]);

  $val = $pdo->prepare("
    SELECT anoref, mesref, cdcentrocusto, cdplanocontabil, valor
      FROM back.realizado_temp
     WHERE cdempresa = :emp AND idlote = :lote AND flvalido = 1
     ORDER BY cdtemp ASC
     LIMIT 20
  ");
  $val->execute([':emp'=>$cdempresa, ':lote'=>$idlote]);

  json_out([
    'ok'=>true,
    'resumo'=>$resumo,
    'invalidos'=>$inv->fetchAll(PDO::FETCH_ASSOC),
    'validos'=>$val->fetchAll(PDO::FETCH_ASSOC),
  ]);
}

/* ============================================================
 * CONFIRMAR IMPORTAÇÃO TEMP -> REALIZADO
 * ============================================================ */
function confirmarImportacao() {
  global $pdo, $cdempresa, $usuario;

  $idlote = $_GET['idlote'] ?? null;
  if (!$idlote) json_out(['ok'=>false,'msg'=>'Lote não informado'], 400);

  // Verificar se existem registros inválidos (flvalido = 0)
  $stCheck = $pdo->prepare("
    SELECT COUNT(*) as qtd_invalidos
    FROM back.realizado_temp
    WHERE idlote = :lote AND flvalido = 0
  ");
  $stCheck->execute([':lote' => (string)$idlote]);
  $check = $stCheck->fetch(PDO::FETCH_ASSOC);
  
  if ($check['qtd_invalidos'] > 0) {
    json_out([
      'ok' => false,
      'msg' => "Não é possível confirmar. Existem " . $check['qtd_invalidos'] . " registro(s) com erro(s). Corrija os erros antes de confirmar a importação."
    ], 400);
  }

  $pdo->beginTransaction();

  // Garantir que variáveis são escalares
  if (is_array($cdempresa)) $cdempresa = (int)reset($cdempresa);
  else $cdempresa = (int)$cdempresa;
  
  if (is_array($usuario)) $usuario = (string)reset($usuario);
  else $usuario = (string)$usuario;
  
  if (is_array($idlote)) $idlote = (string)reset($idlote);
  else $idlote = (string)$idlote;

  // Exclusão lógica: cancelar dados antigos do mesmo exercício/centro/conta
  // Isso evita duplicação se o usuário importa a mesma planilha novamente
  $pdo->prepare("
    UPDATE back.realizado SET flcancelado = 1
     WHERE cdempresa = :emp
       AND anoref IN (SELECT anoref FROM back.realizado_temp WHERE idlote = :lote LIMIT 1)
       AND cdcentrocusto IN (SELECT cdcentrocusto FROM back.realizado_temp WHERE idlote = :lote)
       AND cdplanocontabil IN (SELECT cdplanocontabil FROM back.realizado_temp WHERE idlote = :lote)
       AND flcancelado = 0
  ")->execute([':emp'=>(int)$cdempresa, ':lote'=>(string)$idlote]);

  $sql = "
    INSERT INTO back.realizado
    (cdempresa, cdexercicio, cdcentrocusto, cdplanocontabil,
     anoref, mesref, valor, florigem, idlote, cdpessoa, flcancelado)
    SELECT
      :emp,
      e.cdexercicio,
      t.cdcentrocusto,
      t.cdplanocontabil,
      t.anoref,
      t.mesref,
      t.valor,
      1,
      t.idlote,
      :usuario,
      0
    FROM back.realizado_temp t
    JOIN back.exercicio e
      ON e.anoref = t.anoref
     AND e.cdempresa = :emp
    WHERE t.cdempresa = :emp
      AND t.idlote = :lote
      AND t.flvalido = 1
  ";
  $st = $pdo->prepare($sql);
  $st->execute([':emp'=>(int)$cdempresa, ':usuario'=>(string)$usuario, ':lote'=>(string)$idlote]);

  // Deletar dados da temp após confirmação
  $pdo->prepare("
    DELETE FROM back.realizado_temp
     WHERE cdempresa = :emp AND idlote = :lote
  ")->execute([':emp'=>(int)$cdempresa, ':lote'=>(string)$idlote]);

  $pdo->commit();

  json_out(['ok'=>true,'msg'=>'Importação confirmada.']);
}

/* ============================================================
 * CANCELAR LOTE
 * ============================================================ */
function cancelarLote() {
  global $pdo, $cdempresa;

  $idlote = $_GET['idlote'] ?? null;
  if (!$idlote) json_out(['ok'=>false,'msg'=>'Lote não informado'], 400);

  $st = $pdo->prepare("
    DELETE FROM back.realizado
     WHERE cdempresa = :emp
       AND idlote = :lote
  ");
  $st->execute([':emp'=>$cdempresa, ':lote'=>$idlote]);
  json_out(['ok'=>true,'msg'=>'Lote excluído com sucesso']);
}

/* ============================================================
 * CANCELAR 1 LANÇAMENTO
 * ============================================================ */
function cancelar() {
  global $pdo, $cdempresa;

  $id = (int)($_GET['id'] ?? 0);
  if (!$id) json_out(['ok'=>false,'msg'=>'ID inválido'], 400);

  $st = $pdo->prepare("
    DELETE FROM back.realizado
     WHERE cdrealizado = :id
       AND cdempresa = :emp
  ");
  $st->execute([':id'=>$id, ':emp'=>$cdempresa]);
  json_out(['ok'=>true,'msg'=>'Lançamento excluído']);
}

/**
 * Sincroniza realizado para um período específico usando integrações cadastradas
 */
function sincronizarPeriodo() {
  global $pdo, $cdempresa;

  // Garantir que retorna JSON em qualquer circunstância
  ob_start();
  header('Content-Type: application/json; charset=utf-8');
  
  try {
    // Capturar dados POST
    $input = file_get_contents('php://input');
    if (empty($input)) {
      ob_end_clean();
      return json_out(['ok' => false, 'msg' => 'Dados POST vazios'], 400);
    }

    $data = json_decode($input, true);
    if ($data === null) {
      ob_end_clean();
      return json_out(['ok' => false, 'msg' => 'JSON inválido'], 400);
    }

    $dtinicio = $data['dtinicio'] ?? null;
    $dtfim = $data['dtfim'] ?? null;

    if (!$dtinicio || !$dtfim) {
      ob_end_clean();
      return json_out(['ok' => false, 'msg' => 'Datas inválidas'], 400);
    }

    // Validar formato de data
    $dtIniObj = DateTime::createFromFormat('Y-m-d', $dtinicio);
    $dtFimObj = DateTime::createFromFormat('Y-m-d', $dtfim);
    
    if (!$dtIniObj || !$dtFimObj || $dtIniObj > $dtFimObj) {
      ob_end_clean();
      return json_out(['ok' => false, 'msg' => 'Formato de data inválido ou período inválido'], 400);
    }

    // Regra: intervalo máximo de 30 dias dentro do mesmo mês
    if ($dtIniObj->format('Y-m') !== $dtFimObj->format('Y-m')) {
      ob_end_clean();
      return json_out(['ok' => false, 'msg' => 'O período deve estar dentro do mesmo mês.'], 400);
    }

    $diffDias = $dtIniObj->diff($dtFimObj)->days + 1; // inclui o dia inicial
    if ($diffDias > 31) {
      ob_end_clean();
      return json_out(['ok' => false, 'msg' => 'O intervalo máximo permitido para sincronização é de 31 dias.'], 400);
    }

    // Buscar integrações ativas do tipo conexão/api (origens são cascateadas depois)
    $stmtInt = $pdo->prepare("
      SELECT DISTINCT i.cdintegracao
        FROM back.integracao i
       WHERE i.flativo = true
         AND (i.fltipo IS NULL OR i.fltipo != 'origem')
       ORDER BY i.cdintegracao
    ");
    
    if (!$stmtInt) {
      ob_end_clean();
      return json_out(['ok' => false, 'msg' => 'Erro na preparação do statement'], 500);
    }
    
    $stmtInt->execute();
    $integracoes = $stmtInt->fetchAll(PDO::FETCH_COLUMN);

    if (empty($integracoes)) {
      ob_end_clean();
      return json_out(['ok' => false, 'msg' => 'Nenhuma integração ativa configurada']);
    }

    $totalIns = 0;
    $totalIgnorados = 0;
    $totalErros = 0;
    $totalIgnoradosMotivos = [
      'empresa_origem_sem_mapeamento' => 0,
      'empresa_nao_vinculada_integracao' => 0,
      'dtmovimento_vazio' => 0,
      'dtmovimento_invalido' => 0,
      'exercicio_nao_criado' => 0,
    ];

    // Para cada integração, chamar sincronizarIntegracaoAgora com datas literais
    // IMPORTANTE: chamada via tela => restringe à empresa logada ($cdempresa)
    foreach ($integracoes as $cdintegracao) {
      try {
        // Executa a sincronização dessa integração com as datas literais
        // e apenas para a empresa logada (melhor desempenho na ação manual)
        // Chamada manual pela tela -> tipo de integração "manual"
        // skipRefresh=true porque faremos o refresh uma única vez ao final
        $resultado = sincronizarIntegracaoAgora($cdintegracao, $dtinicio, $dtfim, $cdempresa, 'manual', true);
        
        $totalIns += $resultado['total'] ?? 0;
        $totalIgnorados += $resultado['ignorados'] ?? 0;
        $totalErros += $resultado['erros'] ?? 0;

        if (!empty($resultado['ignorados_motivos']) && is_array($resultado['ignorados_motivos'])) {
          foreach ($resultado['ignorados_motivos'] as $motivo => $qtd) {
            if (!isset($totalIgnoradosMotivos[$motivo])) {
              $totalIgnoradosMotivos[$motivo] = 0;
            }
            $totalIgnoradosMotivos[$motivo] += (int)$qtd;
          }
        }
        
        $detalhes[] = "Integração {$cdintegracao}: {$resultado['total']} inseridos, {$resultado['ignorados']} ignorados, {$resultado['erros']} erros";
      } catch (Exception $ex) {
        $totalErros++;
        $detalhes[] = "Integração {$cdintegracao}: ERRO - " . $ex->getMessage();
      }
    }

    // ✨ ETAPA 2: Consolidação dos dados (Refresh das Materialized Views)
    // Executa ao final, após todas as integrações do período.
    // Só consideramos a fase finalizada quando o refresh retornar ok.
    $consolidacao = refreshMaterializedViewsRealizado($pdo);

    // ✨ ETAPA 3: Cascading — sincronizar origens vinculadas às integrações que rodaram
    $cascadeOrigens = [];
    $origensJaProcessadas = [];
    foreach ($integracoes as $cdint) {
      $vinculadas = origemBuscarVinculadas((int)$cdint, $pdo);
      foreach ($vinculadas as $or) {
        $orId = (int)$or['cdintegracao'];
        if (isset($origensJaProcessadas[$orId])) continue;
        $origensJaProcessadas[$orId] = true;
        try {
          $resOr = origemExecutarSincronizacao($orId, $pdo);
          $cascadeOrigens[] = [
            'cdintegracao' => $orId,
            'dsintegracao' => $or['dsintegracao'],
            'ok' => !empty($resOr['ok']),
            'total' => $resOr['total'] ?? 0,
            'ignorados' => $resOr['ignorados'] ?? 0,
            'erros' => $resOr['erros'] ?? 0,
            'msg' => $resOr['msg'] ?? ''
          ];
          $detalhes[] = "Origem {$or['dsintegracao']}: {$resOr['total']} registros importados (origem_dados)";
        } catch (Exception $orEx) {
          $cascadeOrigens[] = [
            'cdintegracao' => $orId,
            'dsintegracao' => $or['dsintegracao'],
            'ok' => false,
            'msg' => $orEx->getMessage()
          ];
          $detalhes[] = "Origem {$or['dsintegracao']}: ERRO - " . $orEx->getMessage();
        }
      }
    }

    $msg = "Sincronização de {$dtinicio} até {$dtfim}: {$totalIns} registros inseridos";
    if ($totalIgnorados > 0) $msg .= ", {$totalIgnorados} ignorados";
    if ($totalErros > 0) $msg .= ", {$totalErros} com erro";
    if (!empty($cascadeOrigens)) {
      $totalOrigemIns = array_sum(array_column($cascadeOrigens, 'total'));
      $msg .= ". Origens: {$totalOrigemIns} registros em " . count($cascadeOrigens) . " origem(ns)";
    }

    if (!$consolidacao['ok']) {
      $msg .= ". Consolidação (refresh das views) falhou.";
    }

    ob_end_clean();
    return json_out([
      'ok' => (bool)$consolidacao['ok'],
      'msg' => $msg,
      'total' => $totalIns,
      'ignorados' => $totalIgnorados,
      'erros' => $totalErros,
      'ignorados_motivos' => $totalIgnoradosMotivos,
      'detalhes' => $detalhes,
      'consolidacao' => $consolidacao,
      'cascade_origens' => $cascadeOrigens
    ]);

  } catch (Exception $e) {
    ob_end_clean();
    return json_out(['ok' => false, 'msg' => 'Erro na sincronização: ' . $e->getMessage()], 500);
  } catch (Throwable $t) {
    ob_end_clean();
    return json_out(['ok' => false, 'msg' => 'Erro fatal: ' . $t->getMessage()], 500);
  }
}

/**
 * Executa sincronização de uma integração específica diretamente via PHP,
 * sem FUNCTION no banco. Faz:
 *  - Conexão na origem usando back.integracao
 *  - Aplica filtro por dtmovimento BETWEEN :dtinicio AND :dtfim
 *  - Mapeia cdempresa_origem -> cdempresa HELM via back.empresa.cdintegracao
 *  - Garante exercício (back.exercicio) por (cdempresa, ano)
 *  - Insere em back.realizado em uma transação (lote único com rollback em erro fatal)
 *  - Se $cdempresaRestrita for informado, processa **apenas** essa empresa
 *  - Se $skipRefresh for true, não executa refresh das views (usado quando será feito em lote ao final)
 */
function sincronizarIntegracaoAgora($cdintegracao, $dtinicio = null, $dtfim = null, $cdempresaRestrita = null, $tpintegracao = 'automatica', $skipRefresh = false) {
  global $pdo;

  // Usuário logado na aplicação (id da tabela back.pessoa)
  $appUserInt = 0;
  if (isset($_SESSION['usuario']) && is_array($_SESSION['usuario']) && isset($_SESSION['usuario']['id'])) {
    $id = $_SESSION['usuario']['id'];
    if (is_numeric($id)) {
      $appUserInt = (int)$id;
    }
  }

  $resultado = ['total' => 0, 'ignorados' => 0, 'erros' => 0];
  $ignoradosMotivos = [
    'empresa_origem_sem_mapeamento' => 0,
    'empresa_nao_vinculada_integracao' => 0,
    'dtmovimento_vazio' => 0,
    'dtmovimento_invalido' => 0,
    'exercicio_nao_criado' => 0,
  ];

  $msgErroExec = null;
  $dtExecInicio = date('Y-m-d H:i:s');

  try {
    // Evitar timeout em integrações maiores, se permitido pela hospedagem
    if (function_exists('set_time_limit')) {
      @set_time_limit(0);
    }

    if (!$cdintegracao) {
      return $resultado;
    }

    // Datas padrão para execução "automática" (sem período explícito):
    // - Regra base: sempre reimportar TODO o mês corrente
    // - Nos 2 primeiros dias do mês subsequente, também reimportar
    //   TODO o mês anterior (para capturar ajustes tardios).
    if (!$dtinicio || !$dtfim) {
      $hoje = new DateTimeImmutable('today');
      $dia  = (int)$hoje->format('d');

      $primeiroMesAtual = $hoje->modify('first day of this month')->format('Y-m-d');
      $ultimoMesAtual   = $hoje->modify('last day of this month')->format('Y-m-d');

      if ($dia <= 2) {
        // Até o 2º dia do mês, incluir também o mês anterior inteiro
        $primeiroMesAnterior = $hoje->modify('first day of last month')->format('Y-m-d');
        $dtinicio = $primeiroMesAnterior;
        $dtfim    = $ultimoMesAtual;
      } else {
        // Demais dias: apenas o mês corrente completo
        $dtinicio = $primeiroMesAtual;
        $dtfim    = $ultimoMesAtual;
      }
    }

    // Buscar configuração completa da integração
    $stmt = $pdo->prepare('
      SELECT cdintegracao, flativo, tipo_banco, host, porta, dbname, usuario, senha_encrypted,
             schema_banco, sql_origem
        FROM back.integracao
       WHERE cdintegracao = :id
    ');
    if (!$stmt) {
      return $resultado;
    }

    $stmt->execute([':id' => $cdintegracao]);
    $cfg = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$cfg || !$cfg['flativo']) {
      return $resultado;
    }

    // Empresas HELM permitidas para esta integração
    $stmtEmp = $pdo->prepare('SELECT cdempresa FROM back.integracao_empresa WHERE cdintegracao = :id');
    $stmtEmp->execute([':id' => $cdintegracao]);
    $empresasPermitidas = $stmtEmp->fetchAll(PDO::FETCH_COLUMN);
    if (empty($empresasPermitidas)) {
      return $resultado;
    }
    $empresasPermitidas = array_map('intval', $empresasPermitidas);
    // Guarda o conjunto completo de empresas vinculadas à integração
    $empresasIntegracao = $empresasPermitidas;

    // Se chamada com restrição de empresa (ação manual na tela),
    // sincroniza apenas essa empresa para melhor desempenho.
    if ($cdempresaRestrita !== null) {
      $cdempresaRestrita = (int)$cdempresaRestrita;

      if (!in_array($cdempresaRestrita, $empresasPermitidas, true)) {
        // Integração não atende a empresa logada, nada a fazer.
        return $resultado;
      }

      $empresasPermitidas = [$cdempresaRestrita];
    }

    // Mapa ERP cdempresa -> HELM cdempresa via back.empresa.cdintegracao
    $mapEmpresa = [];
    $sqlEmp = "SELECT cdempresa, cdintegracao FROM back.empresa WHERE cdintegracao IS NOT NULL";
    $resEmp = $pdo->query($sqlEmp);
    if ($resEmp) {
      foreach ($resEmp->fetchAll(PDO::FETCH_ASSOC) as $rowEmp) {
        $mapEmpresa[(int)$rowEmp['cdintegracao']] = (int)$rowEmp['cdempresa'];
      }
    }

    // Conectar no banco de origem
    $tipo   = $cfg['tipo_banco'];
    $porta  = (int)($cfg['porta'] ?? 0);
    $host   = $cfg['host'] ?? '';
    $dbname = $cfg['dbname'] ?? '';
    $dbUser = $cfg['usuario'] ?? '';
    $senha  = $cfg['senha_encrypted'] ?? '';
    $schema = $cfg['schema_banco'] ?? '';

    // Validar parâmetros mínimos da conexão de origem
    if (!$tipo || !$host || !$dbname || !$dbUser) {
      return $resultado;
    }

    if ($tipo === 'pgsql') {
      $dsn = "pgsql:host={$host};port=" . ($porta ?: 5432) . ";dbname={$dbname}" . ($schema ? ";options='--search_path={$schema}'" : '');
    } elseif ($tipo === 'mysql') {
      $dsn = "mysql:host={$host};port=" . ($porta ?: 3306) . ";dbname={$dbname}";
    } elseif ($tipo === 'sqlsrv') {
      $dsn = "sqlsrv:Server={$host}," . ($porta ?: 1433) . ";Database={$dbname}";
    } else {
      return $resultado;
    }

    $pdoOrigem = new PDO($dsn, $dbUser, $senha, [
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::ATTR_EMULATE_PREPARES => true,
    ]);

    // SQL de origem validado no cadastro (apenas SELECT/WITH). Aqui só aplicamos o filtro por período.
    $sqlOrigemBase = trim($cfg['sql_origem'] ?? '');
    if ($sqlOrigemBase === '') {
      return $resultado;
    }

    // Envolve em subselect para garantir sintaxe e aplicar filtro de dtmovimento
    $sqlOrigemFiltrado = "SELECT * FROM (" . $sqlOrigemBase . ") AS origem WHERE dtmovimento BETWEEN :dtinicio AND :dtfim";

    $stmtOrigem = $pdoOrigem->prepare($sqlOrigemFiltrado);
    $stmtOrigem->execute([
      ':dtinicio' => $dtinicio,
      ':dtfim'    => $dtfim,
    ]);

    // Pré-preparar objetos para exercício e insert no destino
    $cacheExercicio = [];
    $stmtExSel = $pdo->prepare('SELECT cdexercicio FROM back.exercicio WHERE cdempresa = :emp AND anoref = :ano LIMIT 1');
    $stmtExIns = $pdo->prepare('INSERT INTO back.exercicio (cdempresa, anoref, descricao, flativo, floficial) VALUES (:emp, :ano, :descricao, 0, 0) RETURNING cdexercicio');

    // Buscar (ou reaproveitar) idlote por combinação empresa/centro/ano para que
    // múltiplas sincronizações do mesmo período continuem aparecendo como um
    // único lote na tela em vez de criar linhas duplicadas de ERP.
    $cacheLote = [];
    $stmtLoteSel = $pdo->prepare('
      SELECT idlote
        FROM back.realizado
       WHERE cdempresa = :emp
         AND cdcentrocusto = :cc
         AND anoref = :ano
         AND florigem = 2
         AND cdintegracao = :cdintegracao
       LIMIT 1
    ');

    $inseridos = 0;
    $ignorados = 0;
    $erros = 0;

    // Estatísticas por empresa HELM para log consolidado
    $motivosBase = [
      'empresa_origem_sem_mapeamento' => 0,
      'empresa_nao_vinculada_integracao' => 0,
      'dtmovimento_vazio' => 0,
      'dtmovimento_invalido' => 0,
      'exercicio_nao_criado' => 0,
    ];
    $statsPorEmpresa = []; // cdempresaHelm => [inseridos, ignorados, erros, motivos[], cdorigensErro[], idlote]

    // Buffer para inserção em lote em back.realizado
    $batch = [];
    $batchSize = 500; // tamanho de lote (ajustável conforme ambiente)

    // Inicia transação no destino para garantir lote atômico
    $pdo->beginTransaction();

    // Antes de inserir novos lançamentos ERP, remover definitivamente
    // todos os lançamentos ERP existentes dessa integração, empresas
    // vinculadas e período selecionado (dtmovimento entre dtinicio/dtfim).
    if (!empty($empresasPermitidas)) {
      $placeholdersEmp = implode(',', array_fill(0, count($empresasPermitidas), '?'));

      $paramsCancel = array_merge([
        $cdintegracao,
        $dtinicio,
        $dtfim,
      ], array_map('intval', $empresasPermitidas));

      // Exclui diretamente os lançamentos ERP já existentes no período.
      // Os logs de integração são mantidos para fins de auditoria.
      $sqlCancel = "DELETE FROM back.realizado
                     WHERE cdintegracao = ?
                       AND florigem = 2
                       AND dtmovimento BETWEEN ? AND ?
                       AND cdempresa IN ($placeholdersEmp)";

      $stmtCancel = $pdo->prepare($sqlCancel);
      $stmtCancel->execute($paramsCancel);
    }

    $dtcriacao = date('Y-m-d H:i:s');

    $cdorigensErroSemEmpresa = [];

    while ($reg = $stmtOrigem->fetch(PDO::FETCH_ASSOC)) {
      // cdempresa da origem (ERP) -> mapeado para cdempresa HELM via back.empresa.cdintegracao
      $cdempresaOrigem = isset($reg['cdempresa']) ? (int)$reg['cdempresa'] : null;
      if (!$cdempresaOrigem || !isset($mapEmpresa[$cdempresaOrigem])) {
        $ignorados++;
        $ignoradosMotivos['empresa_origem_sem_mapeamento']++;
        if (!empty($reg['cdorigem'])) {
          $cdorigensErroSemEmpresa[] = $reg['cdorigem'];
        }
        continue;
      }

      $cdempresaHelm = $mapEmpresa[$cdempresaOrigem];

      // Garante estrutura de estatísticas por empresa
      if (!isset($statsPorEmpresa[$cdempresaHelm])) {
        $statsPorEmpresa[$cdempresaHelm] = [
          'inseridos' => 0,
          'ignorados' => 0,
          'erros' => 0,
          'motivos' => $motivosBase,
          'cdorigensErro' => [],
          'idlote' => null,
        ];
      }
      
      // Respeitar empresas vinculadas na integração: se a empresa mapeada
      // não estiver na lista completa da integração, é de fato não vinculada.
      if (!in_array($cdempresaHelm, $empresasIntegracao, true)) {
        $ignorados++;
        $ignoradosMotivos['empresa_nao_vinculada_integracao']++;
        $statsPorEmpresa[$cdempresaHelm]['ignorados']++;
        $statsPorEmpresa[$cdempresaHelm]['motivos']['empresa_nao_vinculada_integracao']++;
        if (!empty($reg['cdorigem'])) {
          $statsPorEmpresa[$cdempresaHelm]['cdorigensErro'][] = $reg['cdorigem'];
        }
        continue;
      }

      // Se a sincronização foi chamada manualmente (com empresa restrita),
      // simplesmente desconsidera registros de outras empresas vinculadas
      // sem contar como "ignorado" nem registrar log, pois é comportamento esperado.
      if ($cdempresaRestrita !== null && !in_array($cdempresaHelm, $empresasPermitidas, true)) {
        continue;
      }

      $dtmovimento = $reg['dtmovimento'] ?? null;
      if (!$dtmovimento) {
        $ignorados++;
        $ignoradosMotivos['dtmovimento_vazio']++;
        $statsPorEmpresa[$cdempresaHelm]['ignorados']++;
        $statsPorEmpresa[$cdempresaHelm]['motivos']['dtmovimento_vazio']++;
        if (!empty($reg['cdorigem'])) {
          $statsPorEmpresa[$cdempresaHelm]['cdorigensErro'][] = $reg['cdorigem'];
        }
        continue;
      }

      try {
        $dt = new DateTime($dtmovimento);
      } catch (Exception $e) {
        $ignorados++;
        $ignoradosMotivos['dtmovimento_invalido']++;
        $statsPorEmpresa[$cdempresaHelm]['ignorados']++;
        $statsPorEmpresa[$cdempresaHelm]['motivos']['dtmovimento_invalido']++;
        if (!empty($reg['cdorigem'])) {
          $statsPorEmpresa[$cdempresaHelm]['cdorigensErro'][] = $reg['cdorigem'];
        }
        continue;
      }

      $ano = (int)$dt->format('Y');
      $mes = (int)$dt->format('m');

      // Garantir exercício para (empresa, ano)
      $keyEx = $cdempresaHelm . ':' . $ano;
      if (!isset($cacheExercicio[$keyEx])) {
        $stmtExSel->execute([':emp' => $cdempresaHelm, ':ano' => $ano]);
        $cdexercicio = $stmtExSel->fetchColumn();

        if (!$cdexercicio) {
          $descricaoEx = 'Exercicio (Automatico) ' . $ano;
          $stmtExIns->execute([
            ':emp' => $cdempresaHelm,
            ':ano' => $ano,
            ':descricao' => $descricaoEx,
          ]);
          $cdexercicio = $stmtExIns->fetchColumn();
        }

        if (!$cdexercicio) {
          $ignorados++;
          $ignoradosMotivos['exercicio_nao_criado']++;
          $statsPorEmpresa[$cdempresaHelm]['ignorados']++;
          $statsPorEmpresa[$cdempresaHelm]['motivos']['exercicio_nao_criado']++;
          if (!empty($reg['cdorigem'])) {
            $statsPorEmpresa[$cdempresaHelm]['cdorigensErro'][] = $reg['cdorigem'];
          }
          continue;
        }

        $cacheExercicio[$keyEx] = (int)$cdexercicio;
      }

      $cdexercicio = $cacheExercicio[$keyEx];

      // Código de origem do lançamento vindo da query de origem (campo cdorigem)
      $cdorigem = isset($reg['cdorigem']) ? (int)$reg['cdorigem'] : null;

      $tpmovimento = strtoupper(trim($reg['tpmovimento'] ?? 'D'));
      if ($tpmovimento !== 'D' && $tpmovimento !== 'C') {
        $tpmovimento = 'D';
      }

      $valor = isset($reg['valor']) ? (float)$reg['valor'] : 0.0;
      $cdcentrocusto = isset($reg['cdcentrocusto']) ? (int)$reg['cdcentrocusto'] : null;
      $cdplanocontabil = isset($reg['cdplanocontabil']) ? (int)$reg['cdplanocontabil'] : null;
      $dscomplemento = $reg['dscomplemento'] ?? '';

      // Determinar idlote estável por (empresa, centro, ano) para ERP
      $chaveLote = $cdempresaHelm . ':' . ($cdcentrocusto ?? 0) . ':' . $ano;
      if (!isset($cacheLote[$chaveLote])) {
        $idloteExistente = null;
        if ($cdcentrocusto !== null) {
          $stmtLoteSel->execute([
            ':emp' => $cdempresaHelm,
            ':cc'  => $cdcentrocusto,
            ':ano' => $ano,
            ':cdintegracao' => $cdintegracao,
          ]);
          $idloteExistente = $stmtLoteSel->fetchColumn() ?: null;
        }

        if ($idloteExistente) {
          $cacheLote[$chaveLote] = $idloteExistente;
        } else {
          $cacheLote[$chaveLote] = uuid_v4();
        }
      }
      $idlote = $cacheLote[$chaveLote];

      // Atualiza idlote principal e contagem de inseridos da empresa
      if ($statsPorEmpresa[$cdempresaHelm]['idlote'] === null) {
        $statsPorEmpresa[$cdempresaHelm]['idlote'] = $idlote;
      }
      $statsPorEmpresa[$cdempresaHelm]['inseridos']++;

      // Adiciona registro ao batch para inserção em lote
      $batch[] = [
        'cdempresa'       => $cdempresaHelm,
        'cdexercicio'     => $cdexercicio,
        'cdcentrocusto'   => $cdcentrocusto,
        'cdplanocontabil' => $cdplanocontabil,
        'mesref'          => $mes,
        'anoref'          => $ano,
        'tpmovimento'     => $tpmovimento,
        'valor'           => $valor,
        'dscomplemento'   => $dscomplemento,
        'dtmovimento'     => $dt->format('Y-m-d'),
        'cdintegracao'    => (int)$cdintegracao,
        'cdorigem'        => $cdorigem,
        'json_origem'     => json_encode($reg, JSON_UNESCAPED_UNICODE),
        'dtcriacao'       => $dtcriacao,
        'idlote'          => $idlote,
      ];

      if (count($batch) >= $batchSize) {
        flushBatchInsertRealizado($pdo, $batch, (int)$cdintegracao, $tpintegracao, $appUserInt, $inseridos);
      }
    }

    // Flush final para quaisquer registros restantes no buffer
    if (!empty($batch)) {
      flushBatchInsertRealizado($pdo, $batch, (int)$cdintegracao, $tpintegracao, $appUserInt, $inseridos);
    }

    // Se chegou até aqui sem exceção, confirma lote completo
    $pdo->commit();

    // ✨ ETAPA 2: Consolidação dos dados (Refresh das Materialized Views)
    // IMPORTANTE: Deve ser executado APÓS o commit da transação
    // REFRESH MATERIALIZED VIEW CONCURRENTLY não pode rodar dentro de transaction block
    // Pula o refresh se $skipRefresh=true (quando será feito em lote ao final por outra função)
    if (!$skipRefresh) {
      $consolidacao = refreshMaterializedViewsRealizado($pdo);
      
      // Atualiza última sincronização apenas se consolidação teve sucesso
      if ($consolidacao['ok']) {
        $stmtUpd = $pdo->prepare('UPDATE back.integracao SET ultima_sincronizacao = NOW() WHERE cdintegracao = :id');
        $stmtUpd->execute([':id' => $cdintegracao]);
      }
      
      $resultado['consolidacao'] = $consolidacao;
    } else {
      // Quando skip refresh, atualiza última sincronização direto
      $stmtUpd = $pdo->prepare('UPDATE back.integracao SET ultima_sincronizacao = NOW() WHERE cdintegracao = :id');
      $stmtUpd->execute([':id' => $cdintegracao]);
      
      $resultado['consolidacao'] = ['ok' => true, 'skipped' => true, 'mensagens' => ['Consolidação será feita em lote ao final']];
    }

    $resultado['total'] = $inseridos;
    $resultado['ignorados'] = $ignorados;
    $resultado['erros'] = $erros;
    $resultado['ignorados_motivos'] = $ignoradosMotivos;

    // Logs consolidados da execução (novo modelo)
    $dtexecFim = date('Y-m-d H:i:s');

    if ($tpintegracao === 'manual') {
      // Manual: em geral restrita a uma empresa; log único focado nela
      $empLog = null;
      if ($cdempresaRestrita !== null) {
        $empLog = (int)$cdempresaRestrita;
      } elseif (count($statsPorEmpresa) === 1) {
        $empLog = (int)array_key_first($statsPorEmpresa);
      }

      $stats = null;
      if ($empLog !== null && isset($statsPorEmpresa[$empLog])) {
        $stats = $statsPorEmpresa[$empLog];
      }

      $ins = $stats ? $stats['inseridos'] : $inseridos;
      $ign = $stats ? $stats['ignorados'] : $ignorados;
      $mot = $stats ? $stats['motivos'] : $ignoradosMotivos;
      $cds = $stats ? $stats['cdorigensErro'] : [];
      $idl = $stats ? $stats['idlote'] : null;

      $statusExec = 'sucesso';
      if ($erros > 0 && $ins === 0) {
        $statusExec = 'erro';
      } elseif ($erros > 0 || $ign > 0) {
        $statusExec = 'parcial';
      }

      $mensagemExec = 'Integração ' . $cdintegracao . ' (' . $tpintegracao . ') - ' .
        $ins . ' inseridos, ' . $ign . ' ignorados, ' . $erros . ' erros.';

      registrarIntegracaoLogExecucao(
        $pdo,
        (int)$cdintegracao,
        $empLog,
        $tpintegracao === 'manual' ? $appUserInt : 0,
        $tpintegracao,
        2,
        $idl,
        $dtinicio,
        $dtfim,
        $dtExecInicio,
        $dtexecFim,
        $statusExec,
        $ins,
        $ign,
        $erros,
        $mot,
        $cds,
        $mensagemExec
      );
    } else {
      // Automática (schedule): um log por empresa HELM
      foreach ($statsPorEmpresa as $empId => $stats) {
        $ins = $stats['inseridos'];
        $ign = $stats['ignorados'];
        $mot = $stats['motivos'];
        $cds = $stats['cdorigensErro'];
        $idl = $stats['idlote'];

        $statusExec = 'sucesso';
        if ($erros > 0 && $ins === 0) {
          $statusExec = 'erro';
        } elseif ($erros > 0 || $ign > 0) {
          $statusExec = 'parcial';
        }

        $mensagemExec = 'Integração ' . $cdintegracao . ' (automatica) empresa ' . $empId . ' - ' .
          $ins . ' inseridos, ' . $ign . ' ignorados, ' . $erros . ' erros.';

        registrarIntegracaoLogExecucao(
          $pdo,
          (int)$cdintegracao,
          (int)$empId,
          0,
          $tpintegracao,
          2,
          $idl,
          $dtinicio,
          $dtfim,
          $dtExecInicio,
          $dtexecFim,
          $statusExec,
          $ins,
          $ign,
          $erros,
          $mot,
          $cds,
          $mensagemExec
        );
      }

      // Adicional: casos sem empresa mapeada em nenhum HELM
      if ($ignoradosMotivos['empresa_origem_sem_mapeamento'] > 0) {
        $ins = 0;
        $ign = $ignoradosMotivos['empresa_origem_sem_mapeamento'];
        $mot = [
          'empresa_origem_sem_mapeamento' => $ign,
        ];
        $mensagemExec = 'Integração ' . $cdintegracao . ' (automatica) - ' .
          $ign . ' registros ignorados por empresa_origem_sem_mapeamento.';

        registrarIntegracaoLogExecucao(
          $pdo,
          (int)$cdintegracao,
          null,
          0,
          $tpintegracao,
          2,
          null,
          $dtinicio,
          $dtfim,
          $dtExecInicio,
          $dtexecFim,
          'parcial',
          $ins,
          $ign,
          $erros,
          $mot,
          $cdorigensErroSemEmpresa,
          $mensagemExec
        );
      }
    }

  } catch (Exception $e) {
    if ($pdo->inTransaction()) {
      $pdo->rollBack();
    }
    $msgErroExec = $e->getMessage();
    error_log("Erro em sincronizarIntegracaoAgora($cdintegracao): " . $msgErroExec);
    $resultado['erros']++;
  } catch (Throwable $t) {
    if ($pdo->inTransaction()) {
      $pdo->rollBack();
    }
    $msgErroExec = $t->getMessage();
    error_log("Erro fatal em sincronizarIntegracaoAgora($cdintegracao): " . $msgErroExec);
    $resultado['erros']++;
  }

  return $resultado;
}
