import type { Pool } from 'pg';
import type { KpiData, IngresoRegimen, EgresoRegimen, IvaBalanceRegimen, IngresosEgresosData, ResumenFiscal, Alerta } from '@horux/shared';
import { getRegimenesIgnoradosClaves } from './regimen.service.js';
import { prisma } from '../config/database.js';
import { planCache, type CacheRange } from '../utils/metricas-cache.js';
import { resolveContribuyenteContext } from '../utils/contribuyente-context.js';
import { buildExtraFilters } from './_shared/cfdi-filters.js';

// Status vigente
const VIGENTE = `status NOT IN ('Cancelado', '0')`;

// Impuestos trasladados del comprobante
const IMP_TRAS = `COALESCE(iva_traslado_mxn,0) + COALESCE(ieps_traslado_mxn,0) + COALESCE(impuestos_locales_trasladado_mxn,0)`;

// Impuestos trasladados del pago.
// El IVA se clampa a `monto_pago_mxn × 0.16` (tasa SAT máxima) como defensa
// contra XMLs malformados donde el proveedor reporta el IVA de la factura
// original completa en vez del proporcional al pago parcial. Caso real:
// CFDI 079ace7d con monto_pago=$43,611 e iva_traslado_pago=$30,076 cuando
// el proporcional sería ~$6,017. IEPS NO se clampa (rates van hasta 53%).
const IVA_TRAS_PAGO_CLAMPED = `LEAST(COALESCE(iva_traslado_pago_mxn, 0), COALESCE(monto_pago_mxn, 0) * 0.16)`;
const IVA_RET_PAGO_CLAMPED = `LEAST(COALESCE(iva_retencion_pago_mxn, 0), COALESCE(monto_pago_mxn, 0) * 0.16)`;
const IMP_TRAS_PAGO = `${IVA_TRAS_PAGO_CLAMPED} + COALESCE(ieps_traslado_pago_mxn, 0)`;

// Claves de producto/servicio excluidas de cálculos fiscales
const CLAVES_EXCLUIDAS = `('84121603','93161608','85101501','85121800')`;

// Subtotal de conceptos excluidos por CFDI (importe - descuento)
const EXCL_MONTO = `COALESCE((SELECT SUM(COALESCE(cc.importe_mxn,0) - COALESCE(cc.descuento_mxn,0)) FROM cfdi_conceptos cc WHERE cc.cfdi_id = cfdis.id AND cc.clave_prod_serv IN ${CLAVES_EXCLUIDAS}), 0)`;

// IVA trasladado de conceptos excluidos
const EXCL_IVA_TRAS = `COALESCE((SELECT SUM(COALESCE(cc.iva_traslado_mxn,0)) FROM cfdi_conceptos cc WHERE cc.cfdi_id = cfdis.id AND cc.clave_prod_serv IN ${CLAVES_EXCLUIDAS}), 0)`;

// IVA retenido de conceptos excluidos
const EXCL_IVA_RET = `COALESCE((SELECT SUM(COALESCE(cc.iva_retencion_mxn,0)) FROM cfdi_conceptos cc WHERE cc.cfdi_id = cfdis.id AND cc.clave_prod_serv IN ${CLAVES_EXCLUIDAS}), 0)`;

// IVA neto excluido (trasladado - retenido)
const EXCL_IVA_NETO = `(${EXCL_IVA_TRAS}) - (${EXCL_IVA_RET})`;

// "Total sin impuestos" custom usado para I/07 (aplicación de anticipo) en
// Grupo 1. Fórmula: total − traslados + retenciones. Semánticamente
// representa la base gravable del anticipo considerando que las retenciones
// sí son ingreso (aunque el retenedor se las haya quedado).
const NETO_CUSTOM = (alias: string) => `(
  COALESCE(${alias}.total_mxn, 0)
  - COALESCE(${alias}.iva_traslado_mxn, 0) + COALESCE(${alias}.iva_retencion_mxn, 0)
  + COALESCE(${alias}.isr_retencion_mxn, 0)
  - COALESCE(${alias}.ieps_traslado_mxn, 0) + COALESCE(${alias}.ieps_retencion_mxn, 0)
  - COALESCE(${alias}.impuestos_locales_trasladado_mxn, 0) + COALESCE(${alias}.impuestos_locales_retenidos_mxn, 0)
)`;

// EXCL_MONTO parametrizado por alias — para aplicarlo tanto al CFDI base
// como a las facturas relacionadas dentro de un subquery.
const EXCL_MONTO_ALIAS = (alias: string) => `COALESCE((
  SELECT SUM(COALESCE(cc.importe_mxn, 0) - COALESCE(cc.descuento_mxn, 0))
    FROM cfdi_conceptos cc
   WHERE cc.cfdi_id = ${alias}.id
     AND cc.clave_prod_serv IN ${CLAVES_EXCLUIDAS}
), 0)`;

// EXCL_IVA parametrizado por alias (para compensación I/07 en IVA).
const EXCL_IVA_TRAS_ALIAS = (alias: string) => `COALESCE((
  SELECT SUM(COALESCE(cc.iva_traslado_mxn, 0))
    FROM cfdi_conceptos cc
   WHERE cc.cfdi_id = ${alias}.id
     AND cc.clave_prod_serv IN ${CLAVES_EXCLUIDAS}
), 0)`;
const EXCL_IVA_RET_ALIAS = (alias: string) => `COALESCE((
  SELECT SUM(COALESCE(cc.iva_retencion_mxn, 0))
    FROM cfdi_conceptos cc
   WHERE cc.cfdi_id = ${alias}.id
     AND cc.clave_prod_serv IN ${CLAVES_EXCLUIDAS}
), 0)`;
const EXCL_IVA_NETO_ALIAS = (alias: string) =>
  `((${EXCL_IVA_TRAS_ALIAS(alias)}) - (${EXCL_IVA_RET_ALIAS(alias)}))`;

// IVA neto por fila, parametrizado por alias (iva_traslado - iva_retencion).
const IVA_NETO_ALIAS = (alias: string) =>
  `(COALESCE(${alias}.iva_traslado_mxn, 0) - COALESCE(${alias}.iva_retencion_mxn, 0))`;

// Grupos de regímenes por lógica de cálculo
export const GRUPO_PF_EMPRESARIAL = ['606', '612', '621', '625', '626'];
export const GRUPO_SUELDOS = ['605'];
export const GRUPO_PM_OTROS = ['601', '603', '607', '608', '610', '611', '614', '615', '620', '622', '623', '624'];
const TODOS_REGIMENES = [...GRUPO_PF_EMPRESARIAL, ...GRUPO_SUELDOS, ...GRUPO_PM_OTROS];

// Filtro de fecha por rango — normal o conciliación
const FECHA_RANGO = `fecha_emision >= $1::date AND fecha_emision < ($2::date + interval '1 day')`;
// Para CFDIs tipo P (complementos de pago): el ingreso/gasto se reconoce en la
// fecha_pago_p (cuándo el cliente realmente pagó), no cuando se emitió el
// complemento — el CFDI P puede emitirse hasta el día 5 del mes siguiente al
// pago, o incluso después, y cruzar meses (ej. pago de noviembre 2024 con
// complemento emitido en mayo 2025).
const FECHA_PAGO_RANGO = `fecha_pago_p >= $1::date AND fecha_pago_p < ($2::date + interval '1 day')`;
const FECHA_RANGO_CONCILIACION = `id_conciliacion IS NOT NULL AND id_conciliacion IN (
  SELECT id FROM conciliaciones WHERE fecha_de_pago >= $1::date AND fecha_de_pago < ($2::date + interval '1 day')
)`;

function getFechaRango(conciliacion?: boolean): string {
  return conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
}

/** Igual que getFechaRango pero para CFDIs tipo P: filtra por fecha_pago_p. */
function getFechaPagoRango(conciliacion?: boolean): string {
  return conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_PAGO_RANGO;
}

/**
 * Filtro de contribuyente **inclusivo**: matchea por `contribuyente_id`
 * directo O por RFC en cualquiera de los dos lados (emisor/receptor).
 *
 * Necesario porque cuando dos contribuyentes del mismo tenant tienen
 * relación emisor-receptor, el sync SAT del primero inserta el CFDI con
 * su `contribuyente_id` y el sync del segundo (UPSERT) NO actualiza el
 * campo. Resultado: el CFDI queda asignado al primer contribuyente aunque
 * desde la perspectiva del segundo es "su" CFDI con el `type` inverso.
 *
 * Solución: en vez de filtrar `contribuyente_id = X`, filtrar por RFC del
 * contribuyente en ambos lados. El `type` del CFDI + el lado del query
 * (EMITIDO vs RECIBIDO) ya determina si es ingreso o gasto de ese
 * contribuyente — no requiere `contribuyente_id` para la correcta
 * atribución.
 *
 * Devuelve fragmento SQL con `AND` prefijo; string vacío si no hay
 * `contribuyenteId` provisto.
 */
async function getContribFilter(pool: Pool, contribuyenteId: string | null | undefined): Promise<string> {
  if (!contribuyenteId) return '';
  const safeId = contribuyenteId.replace(/[^a-f0-9-]/gi, '');
  if (!safeId) return '';

  const { rows } = await pool.query<{ rfc: string | null }>(
    `SELECT rfc FROM contribuyentes WHERE entidad_id = $1`,
    [safeId],
  );
  if (rows.length === 0 || !rows[0].rfc) {
    // Fallback: solo contribuyente_id (contribuyente sin RFC registrado).
    return `AND contribuyente_id = '${safeId}'`;
  }
  const rfc = rows[0].rfc.replace(/[^A-Z0-9]/gi, '').toUpperCase();
  return `AND (contribuyente_id = '${safeId}' OR UPPER(rfc_emisor) = '${rfc}' OR UPPER(rfc_receptor) = '${rfc}')`;
}

/**
 * Calcula "Ingresos del Mes" desglosados por régimen fiscal.
 */
async function getDescMap(cache?: Map<string, string>): Promise<Map<string, string>> {
  if (cache) return cache;
  const catalogo = await prisma.regimen.findMany({ where: { activo: true } });
  return new Map(catalogo.map(r => [r.clave, r.descripcion]));
}

async function getIgnorados(tenantId: string, cache?: string[]): Promise<string[]> {
  if (cache) return cache;
  return getRegimenesIgnoradosClaves(tenantId);
}

// ────────────────────────────────────────────────────────────────────
// Read-through cache (Tanda B hot/cold)
//
// Para contribuyentes con datos en `metricas_mensuales`, un rango que cubre
// meses completos dentro de años pasados (previos al actual) se puede leer
// directo de la tabla en vez de recomputar desde raw CFDIs. El año actual y
// los rangos parciales siguen on-the-fly.
//
// Requisitos para usar cache:
//   - `contribuyenteId` presente (sin él no hay filas en la tabla)
//   - `conciliacion` desactivada (la tabla guarda flujo normal, no usa id_conciliacion)
//   - `fechaFin` antes del primer día del año actual
//   - `fechaInicio` es día 1 del mes, `fechaFin` es último día del mes (rango
//     de meses completos; parciales no mapean a filas de la tabla)
// ────────────────────────────────────────────────────────────────────

/** Lee ingresos_cobrados agregados por régimen desde metricas_mensuales. */
async function readIngresosFromCache(
  pool: Pool,
  range: CacheRange,
  ignorados: string[],
  descMap: Map<string, string>,
): Promise<{ total: number; porRegimen: IngresoRegimen[] } | null> {
  const { rows } = await pool.query<{ regimen: string; monto: string; rows_n: string }>(`
    SELECT regimen_fiscal AS regimen,
           COALESCE(SUM(ingresos_cobrados), 0)::numeric(14,2) AS monto,
           COUNT(*) AS rows_n
    FROM metricas_mensuales
    WHERE contribuyente_id = $1
      AND make_date(anio, mes, 1) BETWEEN $2::date AND $3::date
      AND regimen_fiscal IS NOT NULL
    GROUP BY regimen_fiscal
  `, [range.contribuyenteId, range.startDate, range.endDate]);

  // Si no hay filas cacheadas, señal al caller de hacer fallback on-the-fly.
  if (rows.length === 0) return null;

  const porRegimen: IngresoRegimen[] = [];
  for (const r of rows) {
    if (ignorados.includes(r.regimen)) continue;
    const monto = Number(r.monto);
    if (monto !== 0) {
      porRegimen.push({
        regimenClave: r.regimen,
        regimenDescripcion: descMap.get(r.regimen) || r.regimen,
        monto,
      });
    }
  }
  return { total: porRegimen.reduce((s, r) => s + r.monto, 0), porRegimen };
}

/** Lee egresos_pagados agregados por régimen desde metricas_mensuales. */
async function readEgresosFromCache(
  pool: Pool,
  range: CacheRange,
  ignorados: string[],
  descMap: Map<string, string>,
): Promise<{ total: number; porRegimen: EgresoRegimen[] } | null> {
  const { rows } = await pool.query<{ regimen: string; monto: string }>(`
    SELECT regimen_fiscal AS regimen,
           COALESCE(SUM(egresos_pagados), 0)::numeric(14,2) AS monto
    FROM metricas_mensuales
    WHERE contribuyente_id = $1
      AND make_date(anio, mes, 1) BETWEEN $2::date AND $3::date
      AND regimen_fiscal IS NOT NULL
    GROUP BY regimen_fiscal
  `, [range.contribuyenteId, range.startDate, range.endDate]);

  if (rows.length === 0) return null;

  const porRegimen: EgresoRegimen[] = [];
  for (const r of rows) {
    if (ignorados.includes(r.regimen)) continue;
    const monto = Number(r.monto);
    if (monto !== 0) {
      porRegimen.push({
        regimenClave: r.regimen,
        regimenDescripcion: descMap.get(r.regimen) || r.regimen,
        monto,
      });
    }
  }
  return { total: porRegimen.reduce((s, r) => s + r.monto, 0), porRegimen };
}

/**
 * Lee IVA balance por régimen desde metricas_mensuales.
 * Fórmula: monto = iva_trasladado_total − iva_acreditable − iva_retenido_cobrado
 * (alineada con impuestos.resultado tras el refactor que separó retención).
 */
async function readIvaBalanceFromCache(
  pool: Pool,
  range: CacheRange,
  ignorados: string[],
  descMap: Map<string, string>,
): Promise<{ total: number; porRegimen: IvaBalanceRegimen[] } | null> {
  const { rows } = await pool.query<{ regimen: string; causado: string; acreditable: string; retenido: string }>(`
    SELECT regimen_fiscal AS regimen,
           COALESCE(SUM(iva_trasladado_total), 0)::numeric(14,2) AS causado,
           COALESCE(SUM(iva_acreditable), 0)::numeric(14,2) AS acreditable,
           COALESCE(SUM(iva_retenido_cobrado), 0)::numeric(14,2) AS retenido
    FROM metricas_mensuales
    WHERE contribuyente_id = $1
      AND make_date(anio, mes, 1) BETWEEN $2::date AND $3::date
      AND regimen_fiscal IS NOT NULL
    GROUP BY regimen_fiscal
  `, [range.contribuyenteId, range.startDate, range.endDate]);

  if (rows.length === 0) return null;

  const porRegimen: IvaBalanceRegimen[] = [];
  for (const r of rows) {
    if (ignorados.includes(r.regimen)) continue;
    const causado = Number(r.causado);
    const acreditable = Number(r.acreditable);
    const retenido = Number(r.retenido);
    const monto = causado - acreditable - retenido;
    if (monto !== 0 || causado !== 0 || acreditable !== 0 || retenido !== 0) {
      porRegimen.push({
        regimenClave: r.regimen,
        regimenDescripcion: descMap.get(r.regimen) || r.regimen,
        monto,
      });
    }
  }
  return { total: porRegimen.reduce((s, r) => s + r.monto, 0), porRegimen };
}

export async function calcularIngresosPorRegimen(
  pool: Pool,
  tenantId: string,
  fechaInicio: string,
  fechaFin: string,
  _ignorados?: string[],
  _descMap?: Map<string, string>,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
  considerarActivos: boolean = true,
  considerarNCs: boolean = true,
): Promise<{ total: number; porRegimen: IngresoRegimen[] }> {
  const ignorados = await getIgnorados(tenantId, _ignorados);
  const descMap = await getDescMap(_descMap);

  // Read-through cache: si el rango cae en años pasados con meses completos
  // y hay un contribuyente seleccionado, lee de metricas_mensuales. Si hit,
  // retorna de inmediato (evita ~3 queries SQL por régimen).
  const cacheRange = planCache(fechaInicio, fechaFin, conciliacion, contribuyenteId);
  if (cacheRange) {
    const cached = await readIngresosFromCache(pool, cacheRange, ignorados, descMap);
    if (cached) return cached;
  }

  const FR = getFechaRango(conciliacion);
  const FR_PAGO = getFechaPagoRango(conciliacion);
  const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
  const esEmisor = ctx.esEmisor;
  const esReceptor = ctx.esReceptor;
  const extra = buildExtraFilters(considerarActivos, considerarNCs);

  const porRegimen: IngresoRegimen[] = [];

  // ─── GRUPO 1: PF Empresarial (606, 612, 621, 625, 626) ───
  // Método A (ingenuo): todas las I PUE suman completas, incluidas I/07
  // (aplicación de anticipo). Las E/07 (cancelación) restan en el término
  // de NC abajo. Algebraicamente: anticipo + I/07 − E/07 se neutraliza
  // correctamente cuando los tres CFDIs están en el universo de la query.
  // La compensación con NETO_CUSTOM + clamp a 0 se retiró — robusta ante
  // E/07 ausentes pero subcontaba cuando un mismo anticipo es referenciado
  // por múltiples I/07 (ver docs/plans/2026-04-24 §3 decisión final).
  //
  // Filtro por RFC del emisor (`${esEmisor}`) en vez de `type='EMITIDO' AND
  // contribuyente_id=X` — el RFC es fuente de verdad, type/contribuyente_id
  // pueden ser inconsistentes cuando dos contribuyentes del tenant se facturan.
  const { rows: g1Facturas } = await pool.query(`
    SELECT regimen_fiscal_emisor as regimen,
      COALESCE(SUM(COALESCE(total_mxn, 0) - (${IMP_TRAS}) - (${EXCL_MONTO})), 0) as monto
    FROM cfdis
    WHERE ${esEmisor} AND tipo_comprobante = 'I' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}${extra}
      AND regimen_fiscal_emisor = ANY($3)
    GROUP BY regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, GRUPO_PF_EMPRESARIAL]);

  const { rows: g1Pagos } = await pool.query(`
    SELECT regimen_fiscal_emisor as regimen,
      COALESCE(SUM(COALESCE(monto_pago_mxn,0) - (${IMP_TRAS_PAGO})), 0) as monto
    FROM cfdis
    WHERE ${esEmisor} AND tipo_comprobante = 'P'
      AND ${VIGENTE} AND ${FR_PAGO}${extra}
      AND regimen_fiscal_emisor = ANY($3)
    GROUP BY regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, GRUPO_PF_EMPRESARIAL]);

  // NC incluye E/07 (cancelación de anticipo) — Método A. Restar todos los
  // E PUE cierra el ciclo algebraico con la I/07 que antes compensábamos.
  const { rows: g1NC } = await pool.query(`
    SELECT regimen_fiscal_emisor as regimen,
      COALESCE(SUM(COALESCE(total_mxn,0) - (${IMP_TRAS}) - (${EXCL_MONTO})), 0) as monto
    FROM cfdis
    WHERE ${esEmisor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}${extra}
      AND regimen_fiscal_emisor = ANY($3)
    GROUP BY regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, GRUPO_PF_EMPRESARIAL]);

  // Compensación I/07 PPD ↔ E mismo mes/año (regla SAT, lado EMISOR):
  // simétrico al caso de gastos. Cuando el contribuyente emite I/07 PPD y
  // emite también una E (cualquier TipoRelacion) en el mismo mes/año cuya
  // cfdis_relacionados contiene esa I/07 PPD, la I/07 PPD aporta = base de
  // la E. La E sigue restando arriba.
  const esEmisorE = esEmisor.replace(/\brfc_emisor\b/g, 'e.rfc_emisor');
  const { rows: g1I07PpdComp } = await pool.query(`
    SELECT i.regimen_fiscal_emisor AS regimen,
      COALESCE(SUM((
        SELECT COALESCE(SUM(
          COALESCE(e.total_mxn, 0)
          - COALESCE(e.iva_traslado_mxn, 0)
          - COALESCE(e.ieps_traslado_mxn, 0)
          - COALESCE(e.impuestos_locales_trasladado_mxn, 0)
        ), 0)
          FROM cfdis e
         WHERE e.tipo_comprobante = 'E'
           AND e.metodo_pago = 'PUE'
           AND e.status NOT IN ('Cancelado','0')
           AND ${esEmisorE}
           AND LOWER(i.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
           AND date_trunc('month', e.fecha_emision) = date_trunc('month', i.fecha_emision)
      )), 0) AS monto
    FROM cfdis i
    WHERE ${esEmisor.replace(/\brfc_emisor\b/g, 'i.rfc_emisor')}
      AND i.tipo_comprobante = 'I' AND i.metodo_pago = 'PPD'
      AND i.cfdi_tipo_relacion = '07'
      AND i.status NOT IN ('Cancelado', '0')
      AND ${FR.replace(/\bfecha_emision\b/g, 'i.fecha_emision')}
      AND i.regimen_fiscal_emisor = ANY($3)
    GROUP BY i.regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, GRUPO_PF_EMPRESARIAL]);

  for (const clave of GRUPO_PF_EMPRESARIAL) {
    if (ignorados.includes(clave)) continue;
    const facturas = Number(g1Facturas.find((r: any) => r.regimen === clave)?.monto || 0);
    const pagos = Number(g1Pagos.find((r: any) => r.regimen === clave)?.monto || 0);
    const nc = Number(g1NC.find((r: any) => r.regimen === clave)?.monto || 0);
    const i07Comp = Number(g1I07PpdComp.find((r: any) => r.regimen === clave)?.monto || 0);
    const monto = facturas + pagos + i07Comp - nc;
    if (monto !== 0 || facturas !== 0 || pagos !== 0 || i07Comp !== 0) {
      porRegimen.push({ regimenClave: clave, regimenDescripcion: descMap.get(clave) || clave, monto });
    }
  }

  // ─── GRUPO 2: Sueldos y Salarios (605) ───
  // Nómina recibida por el contribuyente (lado RECEPTOR).
  if (!ignorados.includes('605')) {
    const { rows: g2 } = await pool.query(`
      SELECT COALESCE(SUM(COALESCE(total_mxn,0)), 0) as monto
      FROM cfdis
      WHERE ${esReceptor} AND tipo_comprobante = 'N' AND metodo_pago = 'PUE'
        AND ${VIGENTE} AND ${FR}${extra}
        AND regimen_fiscal_receptor = '605'
    `, [fechaInicio, fechaFin]);
    const monto = Number(g2[0]?.monto || 0);
    if (monto !== 0) {
      porRegimen.push({ regimenClave: '605', regimenDescripcion: descMap.get('605') || 'Sueldos y Salarios', monto });
    }
  }

  // ─── GRUPO 3: Resto de regímenes (PM y otros) ───
  const { rows: g3Facturas } = await pool.query(`
    SELECT regimen_fiscal_emisor as regimen,
      COALESCE(SUM(COALESCE(total_mxn,0) - (${IMP_TRAS}) - (${EXCL_MONTO})), 0) as monto
    FROM cfdis
    WHERE ${esEmisor} AND tipo_comprobante = 'I' AND metodo_pago IN ('PUE', 'PPD')
      AND ${VIGENTE} AND ${FR}${extra}
      AND regimen_fiscal_emisor = ANY($3)
    GROUP BY regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, GRUPO_PM_OTROS]);

  const { rows: g3NC } = await pool.query(`
    SELECT regimen_fiscal_emisor as regimen,
      COALESCE(SUM(COALESCE(total_mxn,0) - (${IMP_TRAS}) - (${EXCL_MONTO})), 0) as monto
    FROM cfdis
    WHERE ${esEmisor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}${extra}
      AND regimen_fiscal_emisor = ANY($3)
    GROUP BY regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, GRUPO_PM_OTROS]);

  for (const clave of GRUPO_PM_OTROS) {
    if (ignorados.includes(clave)) continue;
    const facturas = Number(g3Facturas.find((r: any) => r.regimen === clave)?.monto || 0);
    const nc = Number(g3NC.find((r: any) => r.regimen === clave)?.monto || 0);
    const monto = facturas - nc;
    if (monto !== 0 || facturas !== 0) {
      porRegimen.push({ regimenClave: clave, regimenDescripcion: descMap.get(clave) || clave, monto });
    }
  }

  return { total: porRegimen.reduce((s, r) => s + r.monto, 0), porRegimen };
}

/**
 * Calcula "Gastos del Mes" desglosados por régimen fiscal del receptor.
 */
export async function calcularEgresosPorRegimen(
  pool: Pool,
  tenantId: string,
  fechaInicio: string,
  fechaFin: string,
  _ignorados?: string[],
  _descMap?: Map<string, string>,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
  considerarActivos: boolean = true,
  considerarNCs: boolean = true,
): Promise<{ total: number; porRegimen: EgresoRegimen[] }> {
  const ignorados = await getIgnorados(tenantId, _ignorados);
  const descMap = await getDescMap(_descMap);

  // Read-through cache: ver nota en calcularIngresosPorRegimen.
  const cacheRange = planCache(fechaInicio, fechaFin, conciliacion, contribuyenteId);
  if (cacheRange) {
    const cached = await readEgresosFromCache(pool, cacheRange, ignorados, descMap);
    if (cached) return cached;
  }

  const FR = getFechaRango(conciliacion);
  const FR_PAGO = getFechaPagoRango(conciliacion);
  const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
  const esReceptor = ctx.esReceptor;
  const extra = buildExtraFilters(considerarActivos, considerarNCs);

  const porRegimen: EgresoRegimen[] = [];

  // Gastos: lado RECEPTOR (el contribuyente recibe). Método A (ingenuo):
  // todas las I PUE suman completas, incluidas I/07. Las E/07 restan en
  // el término de NC abajo. Simétrico con ingresos — anticipo + I/07 − E/07
  // se neutraliza algebraicamente cuando los tres CFDIs están en la query.
  // La compensación con NETO_CUSTOM + clamp 0 se retiró por el mismo
  // motivo que en ingresos: subcontaba cuando un anticipo es referenciado
  // por múltiples I/07 (N:1). Filtro por RFC (esReceptor) en vez de `type`.
  const { rows: facturas } = await pool.query(`
    SELECT regimen_fiscal_receptor as regimen,
      COALESCE(SUM(COALESCE(total_mxn, 0) - (${IMP_TRAS}) - (${EXCL_MONTO})), 0) as monto
    FROM cfdis
    WHERE ${esReceptor} AND tipo_comprobante = 'I' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}${extra}
      AND regimen_fiscal_receptor = ANY($3)
    GROUP BY regimen_fiscal_receptor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  const { rows: pagos } = await pool.query(`
    SELECT regimen_fiscal_receptor as regimen,
      COALESCE(SUM(COALESCE(monto_pago_mxn,0) - (${IMP_TRAS_PAGO})), 0) as monto
    FROM cfdis
    WHERE ${esReceptor} AND tipo_comprobante = 'P'
      AND ${VIGENTE} AND ${FR_PAGO}${extra}
      AND regimen_fiscal_receptor = ANY($3)
    GROUP BY regimen_fiscal_receptor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // NC incluye E/07 (cancelación de anticipo) — Método A cierra el ciclo
  // algebraico con la I/07 que antes compensábamos.
  const { rows: nc } = await pool.query(`
    SELECT regimen_fiscal_receptor as regimen,
      COALESCE(SUM(COALESCE(total_mxn,0) - (${IMP_TRAS}) - (${EXCL_MONTO})), 0) as monto
    FROM cfdis
    WHERE ${esReceptor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}${extra}
      AND regimen_fiscal_receptor = ANY($3)
    GROUP BY regimen_fiscal_receptor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // Compensación I/07 PPD ↔ E mismo mes/año (regla SAT):
  // cuando una I/07 PPD recibida tiene una E (cualquier TipoRelacion)
  // recibida en el MISMO mes/año cuya cfdis_relacionados contiene el UUID
  // de la I/07 PPD, la I/07 PPD aporta = base de la E (porción del servicio
  // ya cobrada vía anticipo). La E sigue restando como NC normal arriba.
  // Net I/07 + E = 0 en ese periodo; el anticipo aporta en su periodo y los
  // P futuros cubren el resto.
  const esReceptorE = esReceptor.replace(/\brfc_receptor\b/g, 'e.rfc_receptor');
  const { rows: i07PpdComp } = await pool.query(`
    SELECT i.regimen_fiscal_receptor AS regimen,
      COALESCE(SUM((
        SELECT COALESCE(SUM(
          COALESCE(e.total_mxn, 0)
          - COALESCE(e.iva_traslado_mxn, 0)
          - COALESCE(e.ieps_traslado_mxn, 0)
          - COALESCE(e.impuestos_locales_trasladado_mxn, 0)
        ), 0)
          FROM cfdis e
         WHERE e.tipo_comprobante = 'E'
           AND e.metodo_pago = 'PUE'
           AND e.status NOT IN ('Cancelado','0')
           AND ${esReceptorE}
           AND LOWER(i.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
           AND date_trunc('month', e.fecha_emision) = date_trunc('month', i.fecha_emision)
      )), 0) AS monto
    FROM cfdis i
    WHERE ${esReceptor.replace(/\brfc_receptor\b/g, 'i.rfc_receptor')}
      AND i.tipo_comprobante = 'I' AND i.metodo_pago = 'PPD'
      AND i.cfdi_tipo_relacion = '07'
      AND i.status NOT IN ('Cancelado', '0')
      AND ${FR.replace(/\bfecha_emision\b/g, 'i.fecha_emision')}
      AND i.regimen_fiscal_receptor = ANY($3)
    GROUP BY i.regimen_fiscal_receptor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  for (const clave of TODOS_REGIMENES) {
    if (ignorados.includes(clave)) continue;
    const montoF = Number(facturas.find((r: any) => r.regimen === clave)?.monto || 0);
    const montoP = Number(pagos.find((r: any) => r.regimen === clave)?.monto || 0);
    const montoNC = Number(nc.find((r: any) => r.regimen === clave)?.monto || 0);
    const montoI07Comp = Number(i07PpdComp.find((r: any) => r.regimen === clave)?.monto || 0);
    const monto = montoF + montoP + montoI07Comp - montoNC;
    if (monto !== 0 || montoF !== 0 || montoP !== 0 || montoI07Comp !== 0) {
      porRegimen.push({ regimenClave: clave, regimenDescripcion: descMap.get(clave) || clave, monto });
    }
  }

  return { total: porRegimen.reduce((s, r) => s + r.monto, 0), porRegimen };
}

/**
 * Calcula "Adquisición de Mercancías" — misma lógica que egresos pero solo CFDIs con uso_cfdi = 'G01'
 */
export async function calcularAdquisicionesMercancias(
  pool: Pool,
  tenantId: string,
  fechaInicio: string,
  fechaFin: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
): Promise<{ total: number; porRegimen: { regimenClave: string; monto: number }[] }> {
  const FR = getFechaRango(conciliacion);
  const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
  const esReceptor = ctx.esReceptor;

  // Adquisiciones G01 = subset de gastos con uso_cfdi='G01'. Lado receptor.
  // Método A (ingenuo), consistente con calcularEgresosPorRegimen.
  const { rows: facturas } = await pool.query(`
    SELECT regimen_fiscal_receptor as regimen,
      COALESCE(SUM(COALESCE(total_mxn, 0) - (${IMP_TRAS}) - (${EXCL_MONTO})), 0) as monto
    FROM cfdis
    WHERE ${esReceptor} AND tipo_comprobante = 'I' AND metodo_pago = 'PUE'
      AND uso_cfdi = 'G01'
      AND ${VIGENTE} AND ${FR}
    GROUP BY regimen_fiscal_receptor
  `, [fechaInicio, fechaFin]);

  const { rows: nc } = await pool.query(`
    SELECT regimen_fiscal_receptor as regimen,
      COALESCE(SUM(COALESCE(total_mxn,0) - (${IMP_TRAS}) - (${EXCL_MONTO})), 0) as monto
    FROM cfdis
    WHERE ${esReceptor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE'
      AND uso_cfdi = 'G01'
      AND ${VIGENTE} AND ${FR}
    GROUP BY regimen_fiscal_receptor
  `, [fechaInicio, fechaFin]);

  // Consolidar por régimen
  const regimenMap = new Map<string, number>();
  for (const r of facturas) {
    const clave = r.regimen || 'sin';
    regimenMap.set(clave, (regimenMap.get(clave) || 0) + Number(r.monto));
  }
  for (const r of nc) {
    const clave = r.regimen || 'sin';
    regimenMap.set(clave, (regimenMap.get(clave) || 0) - Number(r.monto));
  }

  const porRegimen = Array.from(regimenMap.entries()).map(([regimenClave, monto]) => ({ regimenClave, monto }));
  const total = porRegimen.reduce((s, r) => s + r.monto, 0);

  return { total, porRegimen };
}

// IVA neto del comprobante: trasladado - retenido
const IVA_NETO = `COALESCE(iva_traslado_mxn,0) - COALESCE(iva_retencion_mxn,0)`;
// IVA neto del pago. Refactor 2026-04-26: campos directos, sin clamp.
// Alineado con impuestos.service.ts post-refactor (ver doc 2026-04-26-iva-refactor.md).
const IVA_NETO_PAGO = `COALESCE(iva_traslado_pago_mxn, 0) - COALESCE(iva_retencion_pago_mxn, 0)`;

/**
 * Calcula "Balance IVA" desglosado por régimen fiscal del receptor.
 */
export async function calcularIvaBalancePorRegimen(
  pool: Pool,
  tenantId: string,
  fechaInicio: string,
  fechaFin: string,
  _ignorados?: string[],
  _descMap?: Map<string, string>,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
): Promise<{ total: number; porRegimen: IvaBalanceRegimen[] }> {
  const ignorados = await getIgnorados(tenantId, _ignorados);
  const descMap = await getDescMap(_descMap);

  // Read-through cache: años pasados con contribuyente seleccionado leen de
  // metricas_mensuales (iva_trasladado_total, iva_acreditable). El año actual
  // y rangos parciales siguen on-the-fly.
  const cacheRange = planCache(fechaInicio, fechaFin, conciliacion, contribuyenteId);
  if (cacheRange) {
    const cached = await readIvaBalanceFromCache(pool, cacheRange, ignorados, descMap);
    if (cached) return cached;
  }

  const FR = getFechaRango(conciliacion);
  const FR_PAGO = getFechaPagoRango(conciliacion);
  const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
  const esEmisor = ctx.esEmisor;
  const esReceptor = ctx.esReceptor;

  const porRegimen: IvaBalanceRegimen[] = [];

  // 6 buckets — 3 causados (emisor) + 3 acreditables (receptor).
  // Filtro por RFC (esEmisor/esReceptor) en vez de type.

  // s1 — Emisor + I + PUE.
  // Refactor 2026-04-26: removida la compensación I PUE/07. Las I PUE/07
  // ahora aportan IVA neto completo. La E (cualquier tipoRelación) que
  // las cancele resta vía s3/r3 — fidelidad al XML, sin interpretación.
  const { rows: s1 } = await pool.query(`
    SELECT regimen_fiscal_emisor as regimen,
      COALESCE(SUM(${IVA_NETO} - (${EXCL_IVA_NETO})), 0) as monto
    FROM cfdis
    WHERE ${esEmisor} AND tipo_comprobante = 'I' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}
      AND regimen_fiscal_emisor = ANY($3)
    GROUP BY regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // s2 — Emisor + P
  const { rows: s2 } = await pool.query(`
    SELECT regimen_fiscal_emisor as regimen,
      COALESCE(SUM(${IVA_NETO_PAGO}), 0) as monto
    FROM cfdis
    WHERE ${esEmisor} AND tipo_comprobante = 'P'
      AND ${VIGENTE} AND ${FR_PAGO}
      AND regimen_fiscal_emisor = ANY($3)
    GROUP BY regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // s3 — Receptor + E + PUE (NC recibida) — resta de acreditable.
  // Refactor 2026-04-26: removido filtro `<> '07'`. Todas las E PUE entran.
  const { rows: s3 } = await pool.query(`
    SELECT regimen_fiscal_receptor as regimen,
      COALESCE(SUM(${IVA_NETO} - (${EXCL_IVA_NETO})), 0) as monto
    FROM cfdis
    WHERE ${esReceptor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}
      AND regimen_fiscal_receptor = ANY($3)
    GROUP BY regimen_fiscal_receptor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // r1 — Receptor + I + PUE.
  // Refactor 2026-04-26: removida compensación I PUE/07.
  const { rows: r1 } = await pool.query(`
    SELECT regimen_fiscal_receptor as regimen,
      COALESCE(SUM(${IVA_NETO} - (${EXCL_IVA_NETO})), 0) as monto
    FROM cfdis
    WHERE ${esReceptor} AND tipo_comprobante = 'I' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}
      AND regimen_fiscal_receptor = ANY($3)
    GROUP BY regimen_fiscal_receptor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // r2 — Receptor + P
  const { rows: r2 } = await pool.query(`
    SELECT regimen_fiscal_receptor as regimen,
      COALESCE(SUM(${IVA_NETO_PAGO}), 0) as monto
    FROM cfdis
    WHERE ${esReceptor} AND tipo_comprobante = 'P'
      AND ${VIGENTE} AND ${FR_PAGO}
      AND regimen_fiscal_receptor = ANY($3)
    GROUP BY regimen_fiscal_receptor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // r3 — Emisor + E + PUE (NC emitida resta de causado).
  // Refactor 2026-04-26: removido filtro `<> '07'`. Todas las E PUE entran.
  const { rows: r3 } = await pool.query(`
    SELECT regimen_fiscal_emisor as regimen,
      COALESCE(SUM(${IVA_NETO} - (${EXCL_IVA_NETO})), 0) as monto
    FROM cfdis
    WHERE ${esEmisor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE'
      AND ${VIGENTE} AND ${FR}
      AND regimen_fiscal_emisor = ANY($3)
    GROUP BY regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // s4 — Emisor I PPD/07 hereda IVA neto de E que la cancelan en mismo mes.
  // Mirror de SUM_E_REFERENCING en impuestos.service.ts. La I PPD/07 normalmente
  // no aporta IVA (espera al P), pero si una E la referencia en su mismo mes
  // hereda el IVA de la E para netear el efecto del NEG (caso PPD ↔ E).
  const { rows: s4 } = await pool.query(`
    SELECT i.regimen_fiscal_emisor as regimen,
      COALESCE(SUM((
        SELECT COALESCE(SUM(${IVA_NETO_ALIAS('e')} - (${EXCL_IVA_NETO_ALIAS('e')})), 0)
          FROM cfdis e
         WHERE e.tipo_comprobante = 'E'
           AND e.metodo_pago = 'PUE'
           AND e.status NOT IN ('Cancelado', '0')
           AND ${esEmisor.replace(/\brfc_emisor\b/g, 'e.rfc_emisor')}
           AND LOWER(i.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
           AND date_trunc('month', e.fecha_emision) = date_trunc('month', i.fecha_emision)
      )), 0) as monto
    FROM cfdis i
    WHERE ${esEmisor.replace(/\brfc_emisor\b/g, 'i.rfc_emisor')}
      AND i.tipo_comprobante = 'I' AND i.metodo_pago = 'PPD'
      AND COALESCE(i.cfdi_tipo_relacion, '') = '07'
      AND i.status NOT IN ('Cancelado','0')
      AND ${FR.replace(/\bfecha_emision\b/g, 'i.fecha_emision')}
      AND i.regimen_fiscal_emisor = ANY($3)
    GROUP BY i.regimen_fiscal_emisor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  // r4 — Receptor I PPD/07 hereda IVA neto de E recibidas que la cancelan.
  const { rows: r4 } = await pool.query(`
    SELECT i.regimen_fiscal_receptor as regimen,
      COALESCE(SUM((
        SELECT COALESCE(SUM(${IVA_NETO_ALIAS('e')} - (${EXCL_IVA_NETO_ALIAS('e')})), 0)
          FROM cfdis e
         WHERE e.tipo_comprobante = 'E'
           AND e.metodo_pago = 'PUE'
           AND e.status NOT IN ('Cancelado', '0')
           AND ${esReceptor.replace(/\brfc_receptor\b/g, 'e.rfc_receptor')}
           AND LOWER(i.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
           AND date_trunc('month', e.fecha_emision) = date_trunc('month', i.fecha_emision)
      )), 0) as monto
    FROM cfdis i
    WHERE ${esReceptor.replace(/\brfc_receptor\b/g, 'i.rfc_receptor')}
      AND i.tipo_comprobante = 'I' AND i.metodo_pago = 'PPD'
      AND COALESCE(i.cfdi_tipo_relacion, '') = '07'
      AND i.status NOT IN ('Cancelado','0')
      AND ${FR.replace(/\bfecha_emision\b/g, 'i.fecha_emision')}
      AND i.regimen_fiscal_receptor = ANY($3)
    GROUP BY i.regimen_fiscal_receptor
  `, [fechaInicio, fechaFin, TODOS_REGIMENES]);

  const find = (rows: any[], clave: string) => Number(rows.find((r: any) => r.regimen === clave)?.monto || 0);

  // Atribución directa por lado (refactor 2026-04-26):
  //   Causado     = (EMIT I PUE) + (EMIT P) + (EMIT I PPD/07 hereda E) − (EMIT E PUE)
  //   Acreditable = (RECIB I PUE) + (RECIB P) + (RECIB I PPD/07 hereda E) − (RECIB E PUE)
  // Balance = Causado − Acreditable. Sin compensación I PUE/07; sin filtro
  // tipoRel en E. Las I PPD/07 con E que las cancelan heredan el IVA neto de
  // la E para netear dentro del mes.
  for (const clave of TODOS_REGIMENES) {
    if (ignorados.includes(clave)) continue;

    const causado = find(s1, clave) + find(s2, clave) + find(s4, clave) - find(r3, clave);
    const acreditable = find(r1, clave) + find(r2, clave) + find(r4, clave) - find(s3, clave);
    const monto = causado - acreditable;

    if (monto !== 0 || causado !== 0 || acreditable !== 0) {
      porRegimen.push({ regimenClave: clave, regimenDescripcion: descMap.get(clave) || clave, monto });
    }
  }

  return { total: porRegimen.reduce((s, r) => s + r.monto, 0), porRegimen };
}

/**
 * Calcula IVA a favor acumulado mes a mes desde añoDesde/enero hasta fechaFin.
 * Lógica SAT: saldo positivo se paga (no acumula), saldo negativo se arrastra.
 */
async function calcularIvaAFavorAcumulado(
  pool: Pool,
  tenantId: string,
  fechaFin: string,
  añoDesde?: number,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
): Promise<number> {
  const añoFin = new Date(fechaFin + 'T00:00:00').getFullYear();
  const mesFin = new Date(fechaFin + 'T00:00:00').getMonth() + 1;
  const inicio = añoDesde ?? añoFin;

  // Precachear
  const ignorados = await getRegimenesIgnoradosClaves(tenantId);
  const catalogo = await prisma.regimen.findMany({ where: { activo: true } });
  const descMap = new Map(catalogo.map(r => [r.clave, r.descripcion]));

  let saldoAFavor = 0;

  for (let y = inicio; y <= añoFin; y++) {
    const ultimoMes = y === añoFin ? mesFin : 12;
    for (let m = 1; m <= ultimoMes; m++) {
      const lastDay = new Date(y, m, 0).getDate();
      const fi = `${y}-${String(m).padStart(2, '0')}-01`;
      const ff = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;

      const ivaMes = await calcularIvaBalancePorRegimen(pool, tenantId, fi, ff, ignorados, descMap, conciliacion, contribuyenteId);
      const balanceMes = ivaMes.total;

      if (balanceMes >= 0) {
        if (saldoAFavor >= balanceMes) {
          saldoAFavor = saldoAFavor - balanceMes;
        } else {
          saldoAFavor = 0;
        }
      } else {
        saldoAFavor = saldoAFavor + Math.abs(balanceMes);
      }
    }
  }

  return saldoAFavor;
}

export async function getKpis(
  pool: Pool,
  fechaInicio: string,
  fechaFin: string,
  tenantId: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
): Promise<KpiData> {
  const FR = getFechaRango(conciliacion);
  const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
  const esEmisor = ctx.esEmisor;
  const esReceptor = ctx.esReceptor;
  const ingresosData = await calcularIngresosPorRegimen(pool, tenantId, fechaInicio, fechaFin, undefined, undefined, conciliacion, contribuyenteId);
  const egresosData = await calcularEgresosPorRegimen(pool, tenantId, fechaInicio, fechaFin, undefined, undefined, conciliacion, contribuyenteId);
  const adquisicionData = await calcularAdquisicionesMercancias(pool, tenantId, fechaInicio, fechaFin, conciliacion, contribuyenteId);
  const ivaData = await calcularIvaBalancePorRegimen(pool, tenantId, fechaInicio, fechaFin, undefined, undefined, conciliacion, contribuyenteId);

  // IVA a favor año actual: desde enero del año en curso
  const ivaAFavorAcumulado = await calcularIvaAFavorAcumulado(pool, tenantId, fechaFin, undefined, conciliacion, contribuyenteId);

  // IVA a favor histórico: desde 5 años atrás
  const añoFin = new Date(fechaFin + 'T00:00:00').getFullYear();
  const ivaAFavorHistorico = await calcularIvaAFavorAcumulado(pool, tenantId, fechaFin, añoFin - 5, conciliacion, contribuyenteId);

  // Conteos por lado: derivamos el "type" efectivo del RFC del contribuyente
  // en vez de la columna `type` (que puede ser inconsistente).
  const { rows: countRows } = await pool.query(`
    SELECT
      CASE WHEN ${esEmisor} THEN 'EMITIDO'
           WHEN ${esReceptor} THEN 'RECIBIDO'
           ELSE NULL END AS type,
      COALESCE(CASE WHEN ${esEmisor} THEN regimen_fiscal_emisor ELSE regimen_fiscal_receptor END, '') as regimen,
      COUNT(*)::int as total
    FROM cfdis
    WHERE ${VIGENTE} AND ${FR}
      AND (${esEmisor} OR ${esReceptor})
    GROUP BY 1, regimen
  `, [fechaInicio, fechaFin]);

  const ingresosVal = ingresosData.total;
  const egresosVal = egresosData.total;
  const utilidad = ingresosVal - egresosVal;
  const margen = ingresosVal > 0 ? (utilidad / ingresosVal) * 100 : 0;

  const emitidosPorRegimen = countRows
    .filter((r: any) => r.type === 'EMITIDO')
    .map((r: any) => ({ regimen: r.regimen, total: r.total }));
  const recibidosPorRegimen = countRows
    .filter((r: any) => r.type === 'RECIBIDO')
    .map((r: any) => ({ regimen: r.regimen, total: r.total }));

  return {
    ingresos: ingresosVal,
    ingresosPorRegimen: ingresosData.porRegimen,
    egresos: egresosVal,
    egresosPorRegimen: egresosData.porRegimen,
    adquisicionMercancias: adquisicionData.total,
    adquisicionMercanciasPorRegimen: adquisicionData.porRegimen,
    utilidad,
    margen: Math.round(margen * 100) / 100,
    ivaBalance: ivaData.total,
    ivaBalancePorRegimen: ivaData.porRegimen,
    ivaAFavorAcumulado,
    ivaAFavorHistorico,
    cfdisEmitidos: emitidosPorRegimen.reduce((s: number, r: any) => s + r.total, 0),
    cfdisEmitidosPorRegimen: emitidosPorRegimen,
    cfdisRecibidos: recibidosPorRegimen.reduce((s: number, r: any) => s + r.total, 0),
    cfdisRecibidosPorRegimen: recibidosPorRegimen,
  };
}

export async function getIngresosEgresos(pool: Pool, año: number, tenantId: string, conciliacion?: boolean, contribuyenteId?: string | null): Promise<IngresosEgresosData[]> {
  const mesesLabel = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];

  // Precachear catálogo e ignorados para no consultar 24 veces
  const ignorados = await getRegimenesIgnoradosClaves(tenantId);
  const catalogo = await prisma.regimen.findMany({ where: { activo: true } });
  const descMap = new Map(catalogo.map(r => [r.clave, r.descripcion]));

  const result: IngresosEgresosData[] = [];

  for (let m = 1; m <= 12; m++) {
    const lastDay = new Date(año, m, 0).getDate();
    const fi = `${año}-${String(m).padStart(2, '0')}-01`;
    const ff = `${año}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;

    const ing = await calcularIngresosPorRegimen(pool, tenantId, fi, ff, ignorados, descMap, conciliacion, contribuyenteId);
    const egr = await calcularEgresosPorRegimen(pool, tenantId, fi, ff, ignorados, descMap, conciliacion, contribuyenteId);

    result.push({
      mes: mesesLabel[m - 1],
      ingresos: ing.total,
      egresos: egr.total,
    });
  }

  return result;
}


/**
 * Devuelve los regímenes fiscales presentes en los CFDIs del rango de fechas.
 */
export async function getRegimenesDelPeriodo(
  pool: Pool,
  fechaInicio: string,
  fechaFin: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
  tenantId?: string,
): Promise<{ clave: string; descripcion: string }[]> {
  const FR = getFechaRango(conciliacion);
  const ctx = await resolveContribuyenteContext(pool, tenantId || '', contribuyenteId);
  const esEmisor = ctx.esEmisor;
  const esReceptor = ctx.esReceptor;
  // Régimen del contribuyente: emisor cuando él emitió, receptor cuando él recibió.
  const { rows } = await pool.query(`
    SELECT DISTINCT regimen FROM (
      SELECT regimen_fiscal_emisor AS regimen
      FROM cfdis
      WHERE regimen_fiscal_emisor IS NOT NULL AND ${esEmisor} AND ${FR}
      UNION
      SELECT regimen_fiscal_receptor AS regimen
      FROM cfdis
      WHERE regimen_fiscal_receptor IS NOT NULL AND ${esReceptor} AND ${FR}
    ) sub
    ORDER BY regimen
  `, [fechaInicio, fechaFin]);

  if (rows.length === 0) return [];

  const claves = rows.map((r: any) => r.regimen);
  const catalogo = await prisma.regimen.findMany({
    where: { clave: { in: claves }, activo: true },
    orderBy: { clave: 'asc' },
  });

  return catalogo.map(r => ({ clave: r.clave, descripcion: r.descripcion }));
}

export async function getAlertas(pool: Pool, limit = 5): Promise<Alerta[]> {
  const { rows } = await pool.query(`
    SELECT id, tipo, titulo, mensaje, prioridad,
           fecha_vencimiento as "fechaVencimiento",
           leida, resuelta,
           created_at as "createdAt"
    FROM alertas
    WHERE resuelta = false
    ORDER BY
      CASE prioridad WHEN 'alta' THEN 1 WHEN 'media' THEN 2 ELSE 3 END,
      created_at DESC
    LIMIT $1
  `, [limit]);

  return rows;
}
