import type { Pool } from 'pg';
import type { IvaMensual, IsrMensual, ResumenIva, IvaRegimenDetalle, ResumenIsr } from '@horux/shared';
import { getRegimenesIgnoradosClaves } from './regimen.service.js';
import { calcularIngresosPorRegimen, calcularEgresosPorRegimen } from './dashboard.service.js';
import { prisma } from '../config/database.js';
import { resolveContribuyenteContext } from '../utils/contribuyente-context.js';
import { planCache, type CacheRange } from '../utils/metricas-cache.js';
import { buildExtraFilters, buildExtraFiltersAlias } from './_shared/cfdi-filters.js';

const VIGENTE = `status NOT IN ('Cancelado', '0')`;
// Para cálculos de IVA/ISR la fecha efectiva depende del tipo de comprobante:
//   - tipo P (complemento de pago): fecha real del cobro (fecha_pago_p)
//   - otros tipos (I, E, T, N): fecha_emision del comprobante
// El CASE se evalúa por fila, garantizando que un P emitido en mayo por un pago
// real de noviembre quede contabilizado en noviembre.
const FECHA_EFECTIVA = `CASE WHEN tipo_comprobante = 'P' THEN fecha_pago_p ELSE fecha_emision END`;
const FECHA_RANGO = `${FECHA_EFECTIVA} >= $1::date AND ${FECHA_EFECTIVA} < ($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 getFR(conciliacion?: boolean): string {
  return conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
}
const TODOS_REGIMENES = ['605', '606', '612', '621', '625', '626', '601', '603', '607', '608', '610', '611', '614', '615', '620', '622', '623', '624'];

// Claves de producto/servicio excluidas de cálculos fiscales
const CLAVES_EXCLUIDAS = `('84121603','93161608','85101501','85121800')`;
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)`;
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)`;
const EXCL_ISR_RET = `COALESCE((SELECT SUM(COALESCE(cc.isr_retencion_mxn,0)) FROM cfdi_conceptos cc WHERE cc.cfdi_id = cfdis.id AND cc.clave_prod_serv IN ${CLAVES_EXCLUIDAS}), 0)`;

// Fragmentos IVA: CFDIs tipo P usan campos `_pago_mxn` directos; los demás
// tipos usan los campos base del CFDI restando IVA de conceptos excluidos
// (claves prod/serv 84121603, 93161608, 85101501, 85121800).
//
// Los campos `_pago_mxn` se usan tal cual sin clamp — el spec del usuario
// los toma directos. (El clamp `LEAST(iva, monto*0.16)` defendía contra XMLs
// que reportaban IVA de la factura completa en P parciales; se removió a
// petición del owner; ver doc 2026-04-26-iva-refactor.md.)
const IVA_TRAS_EXPR = `CASE WHEN tipo_comprobante = 'P'
  THEN COALESCE(iva_traslado_pago_mxn, 0)
  ELSE COALESCE(iva_traslado_mxn, 0) - (${EXCL_IVA_TRAS})
END`;
const IVA_RET_EXPR = `CASE WHEN tipo_comprobante = 'P'
  THEN COALESCE(iva_retencion_pago_mxn, 0)
  ELSE COALESCE(iva_retencion_mxn, 0) - (${EXCL_IVA_RET})
END`;

// Versiones parametrizadas por alias — usadas en subqueries que resuelven
// las E que cancelan I PPD/07 (rama nueva). Mismo tratamiento sin clamp.
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 IVA_TRAS_EXPR_ALIAS = (alias: string) => `CASE WHEN ${alias}.tipo_comprobante = 'P'
  THEN COALESCE(${alias}.iva_traslado_pago_mxn, 0)
  ELSE COALESCE(${alias}.iva_traslado_mxn, 0) - (${EXCL_IVA_TRAS_ALIAS(alias)})
END`;
const IVA_RET_EXPR_ALIAS = (alias: string) => `CASE WHEN ${alias}.tipo_comprobante = 'P'
  THEN COALESCE(${alias}.iva_retencion_pago_mxn, 0)
  ELSE COALESCE(${alias}.iva_retencion_mxn, 0) - (${EXCL_IVA_RET_ALIAS(alias)})
END`;

/**
 * Condición que identifica I PPD con TipoRelacion=07 (aplicación de anticipo
 * en operación PPD). La PPD no aporta IVA en su mes de emisión (se causa al
 * cobrar via tipo P). Cuando existe una E del mismo mes/año que la referencia
 * y la cancela, la E resta IVA en su mes pero la I PPD/07 no aportó nada que
 * compensar. La regla (mirror de `i07PpdComp` en dashboard.service.ts) es
 * darle a la I PPD/07 el IVA de la E que la cancela, así I PPD + E netean a 0
 * dentro del mes.
 */
const IS_I_PPD_07 = `(tipo_comprobante = 'I' AND metodo_pago = 'PPD' AND COALESCE(cfdi_tipo_relacion, '') = '07')`;

/**
 * Subqueries que suman el IVA_TRAS (ó IVA_RET) de las E del **mismo lado**
 * (emisor o receptor del contribuyente) que referencian este CFDI (la I PPD/07)
 * en `cfdis_relacionados` y caen en el **mismo mes/año**. Aplicado a I PPD/07
 * via las ramas nuevas en signed exprs: la I PPD/07 hereda como aporte el
 * IVA de TODAS las E que la cancelan (sin importar su tipoRelación),
 * igualando lo que esas mismas E restan en NEG.
 *
 * No filtra por tipoRelación: en PPD, las E/07 que referencian la I PPD/07
 * SÍ entran al NEG (vía la condición `E_REFERENCIA_I_PPD_07_MISMO_MES` agregada
 * al `bucketCausadoNeg`/`bucketAcreditableNeg`). Como ambas patas — la E
 * resta en NEG y la I PPD hereda — usan el mismo set de E's, los IVAs se
 * cancelan exactamente dentro del mes. Esto es distinto al triángulo PUE
 * (anticipo + I PUE/07 + E/07) donde la E/07 sigue excluida del NEG porque
 * apunta al anticipo, no a una I PPD/07.
 *
 * `esLadoE` es la cláusula `UPPER(rfc_emisor)='X'` o `UPPER(rfc_receptor)='X'`
 * traducida al alias `e` (el caller hace el rewrite). Filtrar por mismo
 * lado evita capturar E's de otros contribuyentes del tenant que casualmente
 * referencien el mismo UUID.
 */
const SUM_E_REFERENCING_TRAS = (
  esLadoE: string,
  considerarActivos: boolean,
  considerarNCs: boolean,
) => `COALESCE((
  SELECT SUM(${IVA_TRAS_EXPR_ALIAS('e')})
    FROM cfdis e
   WHERE e.tipo_comprobante = 'E'
     AND e.metodo_pago = 'PUE'
     AND e.status NOT IN ('Cancelado', '0')
     AND ${esLadoE}
     AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
     AND date_trunc('month', e.fecha_emision)
       = date_trunc('month', cfdis.fecha_emision)${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
), 0)`;
const SUM_E_REFERENCING_RET = (
  esLadoE: string,
  considerarActivos: boolean,
  considerarNCs: boolean,
) => `COALESCE((
  SELECT SUM(${IVA_RET_EXPR_ALIAS('e')})
    FROM cfdis e
   WHERE e.tipo_comprobante = 'E'
     AND e.metodo_pago = 'PUE'
     AND e.status NOT IN ('Cancelado', '0')
     AND ${esLadoE}
     AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
     AND date_trunc('month', e.fecha_emision)
       = date_trunc('month', cfdis.fecha_emision)${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
), 0)`;
// Régimen del contribuyente según su lado: emisor/receptor del CFDI.
// Usa el RFC del contribuyente (via `ctx.esEmisor`/`ctx.esReceptor`) para
// determinar el lado, no el `type` de BD.
const regimenTenantExpr = (ctx: { esEmisor: string; esReceptor: string }) =>
  `CASE WHEN ${ctx.esEmisor} THEN regimen_fiscal_emisor
        WHEN ${ctx.esReceptor} THEN regimen_fiscal_receptor
        ELSE NULL END`;

/**
 * Predicado EXISTS que detecta si el CFDI actual (alias implícito `cfdis`) es
 * referenciado en `cfdis_relacionados` por al menos una E del **mismo lado**
 * y **mismo mes/año**. Usado para incluir I PPD/07 en los buckets Any — sin
 * esto, las I PPD/07 quedan fuera del WHERE y las ramas nuevas en signed
 * exprs nunca se evalúan. No filtra tipoRelación: en PPD cualquier E que
 * referencie la I PPD/07 cuenta (incluyendo las 07, fiscalmente correctas).
 */
const HAS_E_REFERENCING_MISMO_MES = (
  esLadoE: string,
  considerarActivos: boolean,
  considerarNCs: boolean,
) => `EXISTS (
  SELECT 1 FROM cfdis e
   WHERE e.tipo_comprobante = 'E'
     AND e.metodo_pago = 'PUE'
     AND e.status NOT IN ('Cancelado', '0')
     AND ${esLadoE}
     AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
     AND date_trunc('month', e.fecha_emision)
       = date_trunc('month', cfdis.fecha_emision)${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
)`;

// Atribución por lado usando RFC en lugar de `type`. Los buckets son
// factories que reciben el context del contribuyente:
//   POS: CFDIs que suman (I PUE + P del lado correcto)
//   NEG: NC del mismo lado (todas las E PUE, sin filtrar TipoRelación —
//        las E/07 también restan, en línea con la lógica del owner que asume
//        que el contador emite la E/07 cuando aplica el anticipo).
// Queries deben sumar signed: CASE WHEN POS THEN +X WHEN NEG THEN -X.
// Balance final = Causado − Acreditable.
const bucketCausadoPos = (ctx: { esEmisor: string }) => `(
  ${ctx.esEmisor} AND (
    (tipo_comprobante = 'I' AND metodo_pago = 'PUE')
    OR tipo_comprobante = 'P'
  )
)`;
const bucketCausadoNeg = (ctx: { esEmisor: string }) => `(
  ${ctx.esEmisor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE'
)`;
const bucketAcreditablePos = (ctx: { esReceptor: string }) => `(
  ${ctx.esReceptor} AND (
    (tipo_comprobante = 'I' AND metodo_pago = 'PUE')
    OR tipo_comprobante = 'P'
  )
)`;
const bucketAcreditableNeg = (ctx: { esReceptor: string }) => `(
  ${ctx.esReceptor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE'
)`;

const bucketCausadoAny = (
  ctx: { esEmisor: string },
  considerarActivos: boolean,
  considerarNCs: boolean,
) => {
  const esEmisorE = ctx.esEmisor.replace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1');
  return `(${bucketCausadoPos(ctx)} OR ${bucketCausadoNeg(ctx)} OR (
    ${ctx.esEmisor} AND ${IS_I_PPD_07} AND ${HAS_E_REFERENCING_MISMO_MES(esEmisorE, considerarActivos, considerarNCs)}
  ))`;
};
const bucketAcreditableAny = (
  ctx: { esReceptor: string },
  considerarActivos: boolean,
  considerarNCs: boolean,
) => {
  const esReceptorE = ctx.esReceptor.replace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1');
  return `(${bucketAcreditablePos(ctx)} OR ${bucketAcreditableNeg(ctx)} OR (
    ${ctx.esReceptor} AND ${IS_I_PPD_07} AND ${HAS_E_REFERENCING_MISMO_MES(esReceptorE, considerarActivos, considerarNCs)}
  ))`;
};

// Signed SUM expressions. La compensación I PUE/07 se removió a petición del
// owner — las I PUE/07 ahora aportan IVA completo y la E/07 (si se emite)
// resta normalmente vía bucket NEG (que ya no filtra TipoRelación). El owner
// asume que la E/07 se emitirá; si el contador la omite, el IVA del anticipo
// se sobrecausa (vs el flujo previo, que era robusto a E/07 ausente).
//
// Rama I PPD/07 con E del mismo mes: la I PPD/07 hereda el IVA de la E que
// la cancela. Sin esto, la E resta IVA en su mes pero la I PPD/07 nunca
// aportó nada (PPD espera al P). Mirror de `i07PpdComp` en dashboard.service.ts.
// El subquery se filtra por mismo lado (emisor↔emisor o receptor↔receptor)
// usando el predicado `esEmisor`/`esReceptor` reescrito al alias `e`.
const signedCausadoTras = (
  ctx: { esEmisor: string },
  considerarActivos: boolean,
  considerarNCs: boolean,
) => {
  const esEmisorE = ctx.esEmisor.replace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1');
  return `CASE
    WHEN ${bucketCausadoPos(ctx)} THEN ${IVA_TRAS_EXPR}
    WHEN ${ctx.esEmisor} AND ${IS_I_PPD_07} THEN ${SUM_E_REFERENCING_TRAS(esEmisorE, considerarActivos, considerarNCs)}
    WHEN ${bucketCausadoNeg(ctx)} THEN -(${IVA_TRAS_EXPR})
    ELSE 0
  END`;
};
const signedCausadoRet = (
  ctx: { esEmisor: string },
  considerarActivos: boolean,
  considerarNCs: boolean,
) => {
  const esEmisorE = ctx.esEmisor.replace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1');
  return `CASE
    WHEN ${bucketCausadoPos(ctx)} THEN ${IVA_RET_EXPR}
    WHEN ${ctx.esEmisor} AND ${IS_I_PPD_07} THEN ${SUM_E_REFERENCING_RET(esEmisorE, considerarActivos, considerarNCs)}
    WHEN ${bucketCausadoNeg(ctx)} THEN -(${IVA_RET_EXPR})
    ELSE 0
  END`;
};
const signedAcreditableTras = (
  ctx: { esReceptor: string },
  considerarActivos: boolean,
  considerarNCs: boolean,
) => {
  const esReceptorE = ctx.esReceptor.replace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1');
  return `CASE
    WHEN ${bucketAcreditablePos(ctx)} THEN ${IVA_TRAS_EXPR}
    WHEN ${ctx.esReceptor} AND ${IS_I_PPD_07} THEN ${SUM_E_REFERENCING_TRAS(esReceptorE, considerarActivos, considerarNCs)}
    WHEN ${bucketAcreditableNeg(ctx)} THEN -(${IVA_TRAS_EXPR})
    ELSE 0
  END`;
};
const signedAcreditableRet = (
  ctx: { esReceptor: string },
  considerarActivos: boolean,
  considerarNCs: boolean,
) => {
  const esReceptorE = ctx.esReceptor.replace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1');
  return `CASE
    WHEN ${bucketAcreditablePos(ctx)} THEN ${IVA_RET_EXPR}
    WHEN ${ctx.esReceptor} AND ${IS_I_PPD_07} THEN ${SUM_E_REFERENCING_RET(esReceptorE, considerarActivos, considerarNCs)}
    WHEN ${bucketAcreditableNeg(ctx)} THEN -(${IVA_RET_EXPR})
    ELSE 0
  END`;
};

// Regímenes que SIEMPRE restan deducciones para ISR, sin importar PF/PM.
const REGIMENES_RESTA_DEDUCCIONES = ['606', '612'];

/**
 * Determina la fórmula de base gravable para un régimen fiscal dado el tipo
 * de persona (PF o PM via rfcLength).
 *
 * - `ingresos-deducciones`: base = max(0, ingresos − deducciones)
 * - `ingresos`:              base = max(0, ingresos)   (tasa plana en RESICO PF)
 *
 * Los regímenes 606 (Arrendamiento) y 612 (PF Empresarial) siempre restan.
 * El régimen 626 (RESICO) distingue por tipo de persona: PM (RFC 12) resta
 * deducciones, PF (RFC 13) usa tasa plana sobre ingresos. Otros regímenes PM
 * (601, 603, 607…) no restan aquí — sus deducciones se consideran vía el
 * coeficiente de utilidad en el cálculo del ISR causado (Art. 14 LISR).
 *
 * Single source of truth — usada por `calcularResumenIsr` (KPI del periodo)
 * y `getIsrMensual` (tabla histórico). Duplicar esto antes causó que el
 * histórico mostrara `base = ingresos` para RESICO PM.
 */
export function determinarFormulaBaseGravable(
  clave: string,
  rfcLength: number,
): 'ingresos-deducciones' | 'ingresos' {
  if (REGIMENES_RESTA_DEDUCCIONES.includes(clave)) return 'ingresos-deducciones';
  if (clave === '626' && rfcLength === 12) return 'ingresos-deducciones';
  return 'ingresos';
}

// Régimen 605 (Sueldos y Salarios): el patrón ya retuvo ISR, no genera
// ingreso/deducción para ISR del contribuyente. Se muestra en Dashboard
// como ingreso general, pero se excluye de cálculos de ISR.
const ISR_EXCLUIR_REGIMEN = `AND regimen_fiscal_emisor != '605'`;
const ISR_EXCLUIR_REGIMEN_REC = `AND regimen_fiscal_receptor != '605'`;

/**
 * Lee IVA mensual agregado por mes desde metricas_mensuales. Solo aplica a
 * años cerrados (< año actual) y contribuyente seleccionado. Agrega los 3
 * campos canónicos: iva_trasladado_total, iva_acreditable, iva_retenido_cobrado
 * (todos post-refactor, alineados con dashboard). Retorna null si no hay
 * filas cacheadas (caller debe caer a on-the-fly).
 */
async function readIvaMensualFromCache(
  pool: Pool,
  año: number,
  contribuyenteId: string,
): Promise<Map<number, { t: number; a: number; r: number }> | null> {
  const safe = contribuyenteId.replace(/[^a-f0-9-]/gi, '');
  if (!safe) return null;
  const { rows } = await pool.query<{ mes: number; t: string; a: string; r: string }>(`
    SELECT mes,
           COALESCE(SUM(iva_trasladado_total), 0)::numeric(14,2) as t,
           COALESCE(SUM(iva_acreditable), 0)::numeric(14,2) as a,
           COALESCE(SUM(iva_retenido_cobrado), 0)::numeric(14,2) as r
    FROM metricas_mensuales
    WHERE contribuyente_id = $1 AND anio = $2
    GROUP BY mes
  `, [safe, año]);
  if (rows.length === 0) return null;
  const map = new Map<number, { t: number; a: number; r: number }>();
  for (const row of rows) {
    map.set(Number(row.mes), { t: Number(row.t), a: Number(row.a), r: Number(row.r) });
  }
  return map;
}

/**
 * IVA Mensual desglosado: trasladado, acreditable, retenido, resultado por mes.
 *
 * Usa la misma fórmula canónica que `getResumenIva` (6 buckets, retención neta):
 *   Trasladado  = causado bruto (Emit+I+PUE + Emit+P + Recib+E+PUE)
 *   Acreditable = acreditable bruto (Recib+I+PUE + Recib+P + Emit+E+PUE)
 *   Retenido    = retención(causado) − retención(acreditable)
 *   Resultado   = T − A − R
 *
 * Read-through cache: años pasados con contribuyente seleccionado leen de
 * `metricas_mensuales`. El año actual y sin-contribuyente siguen on-the-fly.
 */
export async function getIvaMensual(
  pool: Pool,
  año: number,
  tenantId: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
  considerarActivos: boolean = true,
  considerarNCs: boolean = true,
): Promise<IvaMensual[]> {
  // Cache read-through: solo si año pasado, sin conciliación, con contribuyente y flags default
  const currentYear = new Date().getFullYear();
  const cacheable =
    process.env.METRICAS_BYPASS_CACHE !== '1' &&
    año < currentYear &&
    !conciliacion &&
    considerarActivos &&
    considerarNCs &&
    !!contribuyenteId;

  let perMes: Map<number, { t: number; a: number; r: number }> | null = null;
  if (cacheable) {
    perMes = await readIvaMensualFromCache(pool, año, contribuyenteId!);
  }

  if (!perMes) {
    // On-the-fly: dos queries agregadas por mes (causado y acreditable),
    // mismos buckets que getResumenIva. Filtro por RFC (ctx.esEmisor/esReceptor)
    // en vez de `type` para evitar inconsistencias multi-contribuyente.
    const FR = getFR(conciliacion);
    const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
    const REGIMEN_TENANT = regimenTenantExpr(ctx);
    const añoStart = `${año}-01-01`;
    const añoEnd = `${año}-12-31`;
    const extra = buildExtraFilters(considerarActivos, considerarNCs);

    const [{ rows: causadoRows }, { rows: acreditableRows }] = await Promise.all([
      pool.query<{ mes: number; trasladado: string; retencion: string }>(`
        SELECT EXTRACT(MONTH FROM ${FECHA_EFECTIVA})::int as mes,
               COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
               COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
        FROM cfdis
        WHERE ${bucketCausadoAny(ctx, considerarActivos, considerarNCs)}
          AND ${VIGENTE} AND ${FR}${extra}
          AND (${REGIMEN_TENANT}) = ANY($3)
        GROUP BY mes
      `, [añoStart, añoEnd, TODOS_REGIMENES]),
      pool.query<{ mes: number; trasladado: string; retencion: string }>(`
        SELECT EXTRACT(MONTH FROM ${FECHA_EFECTIVA})::int as mes,
               COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
               COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
        FROM cfdis
        WHERE ${bucketAcreditableAny(ctx, considerarActivos, considerarNCs)}
          AND ${VIGENTE} AND ${FR}${extra}
          AND (${REGIMEN_TENANT}) = ANY($3)
        GROUP BY mes
      `, [añoStart, añoEnd, TODOS_REGIMENES]),
    ]);

    perMes = new Map();
    for (const row of causadoRows) {
      const acc = perMes.get(Number(row.mes)) || { t: 0, a: 0, r: 0 };
      acc.t += Number(row.trasladado);
      acc.r += Number(row.retencion);
      perMes.set(Number(row.mes), acc);
    }
    for (const row of acreditableRows) {
      const acc = perMes.get(Number(row.mes)) || { t: 0, a: 0, r: 0 };
      acc.a += Number(row.trasladado);
      acc.r -= Number(row.retencion);
      perMes.set(Number(row.mes), acc);
    }
  }

  const result: IvaMensual[] = [];
  let acumulado = 0;

  for (let m = 1; m <= 12; m++) {
    const monthData = perMes.get(m) || { t: 0, a: 0, r: 0 };
    const t = monthData.t;
    const a = monthData.a;
    const r = monthData.r;
    const resultado = t - a - r;
    acumulado += resultado;

    result.push({
      id: 0,
      año,
      mes: m,
      ivaTrasladado: t,
      ivaAcreditable: a,
      ivaRetenido: r,
      resultado,
      acumulado,
      estado: 'pendiente',
      fechaDeclaracion: null,
    });
  }

  return result;
}

/**
 * ISR Mensual desglosado: ingresos, deducciones, base gravable por mes del año.
 */
export async function getIsrMensual(
  pool: Pool,
  año: number,
  tenantId: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
  regimenClave?: string | null,
  considerarActivos: boolean = true,
  considerarNCs: boolean = true,
): Promise<IsrMensual[]> {
  // Reutiliza la misma lógica que las cards (calcular{Ingresos,Egresos}PorRegimen)
  // aplicada a cada mes del año. Esto garantiza que la tabla "Histórico ISR" cuadre
  // célula a célula con los KPIs del periodo activo (reglas por grupo de régimen,
  // resta de notas de crédito, pagos P solo cuentan lo cobrado, etc.).
  // El RFC del contribuyente determina si régimen 626 resta deducciones (PM) o no (PF).
  const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
  const rfcLength = ctx.rfcLength;
  const result: IsrMensual[] = [];

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

    const [ingresosData, egresosData] = await Promise.all([
      calcularIngresosPorRegimen(pool, tenantId, fi, ff, undefined, undefined, conciliacion, contribuyenteId, considerarActivos, considerarNCs),
      calcularEgresosPorRegimen(pool, tenantId, fi, ff, undefined, undefined, conciliacion, contribuyenteId, considerarActivos, considerarNCs),
    ]);

    let ing: number;
    let ded: number;
    let base: number;

    if (regimenClave) {
      ing = ingresosData.porRegimen.find(r => r.regimenClave === regimenClave)?.monto || 0;
      ded = egresosData.porRegimen.find(r => r.regimenClave === regimenClave)?.monto || 0;
      const formula = determinarFormulaBaseGravable(regimenClave, rfcLength);
      base = formula === 'ingresos-deducciones' ? Math.max(0, ing - ded) : Math.max(0, ing);
    } else {
      // Sin régimen: agregar por régimen aplicando la fórmula correcta a cada
      // uno y sumar las bases (no aplicar ing − ded global, porque algunos
      // regímenes no restan deducciones — ej. RESICO PF, otros PM).
      const regimenesConDatos = new Set<string>([
        ...ingresosData.porRegimen.map(r => r.regimenClave).filter(c => c !== '605'),
        ...egresosData.porRegimen.map(r => r.regimenClave).filter(c => c !== '605'),
      ]);
      ing = 0;
      ded = 0;
      base = 0;
      for (const clave of regimenesConDatos) {
        const ingReg = ingresosData.porRegimen.find(r => r.regimenClave === clave)?.monto || 0;
        const dedReg = egresosData.porRegimen.find(r => r.regimenClave === clave)?.monto || 0;
        const formula = determinarFormulaBaseGravable(clave, rfcLength);
        ing += ingReg;
        ded += dedReg;
        base += formula === 'ingresos-deducciones'
          ? Math.max(0, ingReg - dedReg)
          : Math.max(0, ingReg);
      }
    }

    result.push({
      id: 0,
      año,
      mes: m,
      ingresosAcumulados: ing,
      deducciones: ded,
      baseGravable: base,
      ingresosAcum: 0,    // se llena en el segundo pase abajo
      deduccionesAcum: 0,
      baseGravableAcum: 0,
      isrCausado: 0,
      isrRetenido: 0,
      isrAPagar: 0,
      estado: 'pendiente',
      fechaDeclaracion: null,
    });
  }

  // Running totals: para cada mes, acumular ingresos y deducciones desde enero
  // hasta ese mes inclusive. baseGravableAcum NO se clampa — los déficits se
  // muestran negativos en la UI y solo se clampan al pasar a ISR causado.
  let ingAcum = 0;
  let dedAcum = 0;
  for (const row of result) {
    ingAcum += row.ingresosAcumulados; // (campo mensual, naming heredado)
    dedAcum += row.deducciones;
    row.ingresosAcum = ingAcum;
    row.deduccionesAcum = dedAcum;
    row.baseGravableAcum = ingAcum - dedAcum;
  }

  return result;
}

/**
 * Read-through cache para ResumenIva: lee `iva_trasladado_total`,
 * `iva_acreditable`, `iva_retenido_cobrado` desde `metricas_mensuales` cuando
 * el rango cae en años pasados con contribuyente seleccionado. Calcula el
 * acumulado anual on-the-fly (su rango difiere de fechaInicio-fechaFin).
 *
 * Si no hay filas cacheadas, retorna `null` y el caller cae al path on-the-fly.
 */
async function readResumenIvaFromCache(
  pool: Pool,
  range: CacheRange,
  fechaInicio: string,
  fechaFin: string,
  conciliacion: boolean | undefined,
  ctx: { esEmisor: string; esReceptor: string },
  considerarActivos: boolean = true,
  considerarNCs: boolean = true,
): Promise<ResumenIva | null> {
  const { rows } = await pool.query<{
    regimen: string;
    trasladado: string;
    acreditable: string;
    retenido: string;
  }>(`
    SELECT regimen_fiscal AS regimen,
           COALESCE(SUM(iva_trasladado_total), 0)::numeric(14,2) AS trasladado,
           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 catalogo = await prisma.regimen.findMany({ where: { activo: true } });
  const descMap = new Map(catalogo.map(r => [r.clave, r.descripcion]));

  let trasladado = 0;
  let acreditable = 0;
  let retenido = 0;
  const trasladadoPorRegimen: IvaRegimenDetalle[] = [];
  const acreditablePorRegimen: IvaRegimenDetalle[] = [];
  const retenidoPorRegimen: IvaRegimenDetalle[] = [];

  for (const r of rows) {
    const tras = Number(r.trasladado);
    const acr = Number(r.acreditable);
    const ret = Number(r.retenido);
    trasladado += tras;
    acreditable += acr;
    retenido += ret;
    const desc = descMap.get(r.regimen) || r.regimen;
    if (tras !== 0) trasladadoPorRegimen.push({ regimenClave: r.regimen, regimenDescripcion: desc, monto: tras });
    if (acr !== 0) acreditablePorRegimen.push({ regimenClave: r.regimen, regimenDescripcion: desc, monto: acr });
    if (ret !== 0) retenidoPorRegimen.push({ regimenClave: r.regimen, regimenDescripcion: desc, monto: ret });
  }

  const resultado = trasladado - acreditable - retenido;

  // Acumulado anual: su rango (year-01-01 → fechaFin) difiere del rango cacheado;
  // se calcula on-the-fly contra raw cfdis. Una sola query.
  const añoInicio = new Date(fechaInicio + 'T00:00:00').getFullYear();
  const acumFR = conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
  const REGIMEN_TENANT = regimenTenantExpr(ctx);
  const acumRow = (await pool.query(`
    SELECT
      COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
      COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
      (
        COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
        COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
      ) as total
    FROM cfdis
    WHERE ${VIGENTE}
      AND (${REGIMEN_TENANT}) = ANY($3)
      AND ${acumFR}
      AND (${ctx.esEmisor} OR ${ctx.esReceptor})
  `, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES])).rows[0];

  return {
    trasladado,
    trasladadoPorRegimen,
    acreditable,
    acreditablePorRegimen,
    retenido,
    retenidoPorRegimen,
    resultado,
    acumuladoAnual: Number(acumRow?.total || 0),
  };
}

/**
 * Resumen IVA para un rango de fechas, desglosado por régimen.
 *
 * Alineado con la fórmula del dashboard `calcularIvaBalancePorRegimen`:
 *   Resultado = Trasladado − Acreditable − Retenido
 *
 * Donde cada tarjeta usa los mismos 6 buckets del dashboard, pero con
 * retención **separada** (en el dashboard la retención está embebida en
 * el IVA neto de cada bucket; aquí se exhibe en su propia tarjeta para
 * Control de Impuestos).
 *
 *   Trasladado  = causado bruto (Emit+I+PUE) + (Emit+P) + (Recib+E+PUE)
 *   Acreditable = acreditable bruto (Recib+I+PUE) + (Recib+P) + (Emit+E+PUE)
 *   Retenido    = retención(causado) − retención(acreditable)
 *
 * Algebraicamente: T − A − R == dashboard.balance, céntimo por céntimo.
 */
export async function getResumenIva(
  pool: Pool,
  fechaInicio: string,
  fechaFin: string,
  tenantId: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
  considerarActivos: boolean = true,
  considerarNCs: boolean = true,
): Promise<ResumenIva> {
  const FR = getFR(conciliacion);
  const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
  const REGIMEN_TENANT = regimenTenantExpr(ctx);
  const extra = buildExtraFilters(considerarActivos, considerarNCs);

  // Read-through cache (pasa ctx completo para derivar regimen + signed exprs).
  // Solo aplica cuando flags son default (true) para garantizar que las queries
  // filtradas no lean datos del caché completo.
  const cacheRange =
    considerarActivos && considerarNCs
      ? planCache(fechaInicio, fechaFin, conciliacion, contribuyenteId)
      : null;
  if (cacheRange) {
    const cached = await readResumenIvaFromCache(pool, cacheRange, fechaInicio, fechaFin, conciliacion, ctx, considerarActivos, considerarNCs);
    if (cached) return cached;
  }

  // Una query por lado (causado / acreditable). Filtro por RFC via
  // ctx.esEmisor/esReceptor (embedded en buckets/signed exprs).
  const [{ rows: causadoRows }, { rows: acreditableRows }] = await Promise.all([
    pool.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
      SELECT ${REGIMEN_TENANT} as regimen,
             COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
             COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
      FROM cfdis
      WHERE ${bucketCausadoAny(ctx, considerarActivos, considerarNCs)}
        AND ${VIGENTE} AND ${FR}${extra}
        AND (${REGIMEN_TENANT}) = ANY($3)
      GROUP BY ${REGIMEN_TENANT}
    `, [fechaInicio, fechaFin, TODOS_REGIMENES]),
    pool.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
      SELECT ${REGIMEN_TENANT} as regimen,
             COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
             COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
      FROM cfdis
      WHERE ${bucketAcreditableAny(ctx, considerarActivos, considerarNCs)}
        AND ${VIGENTE} AND ${FR}${extra}
        AND (${REGIMEN_TENANT}) = ANY($3)
      GROUP BY ${REGIMEN_TENANT}
    `, [fechaInicio, fechaFin, TODOS_REGIMENES]),
  ]);

  // Combinar por régimen: el set de régimenes posibles es la unión de ambos lados.
  type Acc = { trasCausado: number; retCausado: number; trasAcreditable: number; retAcreditable: number };
  const porRegimen = new Map<string, Acc>();
  const ensure = (k: string): Acc => {
    let v = porRegimen.get(k);
    if (!v) { v = { trasCausado: 0, retCausado: 0, trasAcreditable: 0, retAcreditable: 0 }; porRegimen.set(k, v); }
    return v;
  };
  for (const r of causadoRows) {
    if (!r.regimen) continue;
    const acc = ensure(r.regimen);
    acc.trasCausado += Number(r.trasladado);
    acc.retCausado += Number(r.retencion);
  }
  for (const r of acreditableRows) {
    if (!r.regimen) continue;
    const acc = ensure(r.regimen);
    acc.trasAcreditable += Number(r.trasladado);
    acc.retAcreditable += Number(r.retencion);
  }

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

  let trasladado = 0;
  let acreditable = 0;
  let retenido = 0;
  const trasladadoPorRegimen: IvaRegimenDetalle[] = [];
  const acreditablePorRegimen: IvaRegimenDetalle[] = [];
  const retenidoPorRegimen: IvaRegimenDetalle[] = [];

  for (const [regimen, acc] of porRegimen) {
    const tras = acc.trasCausado;
    const acre = acc.trasAcreditable;
    const ret = acc.retCausado - acc.retAcreditable;
    trasladado += tras;
    acreditable += acre;
    retenido += ret;
    const desc = descMap.get(regimen) || regimen;
    if (tras !== 0) trasladadoPorRegimen.push({ regimenClave: regimen, regimenDescripcion: desc, monto: tras });
    if (acre !== 0) acreditablePorRegimen.push({ regimenClave: regimen, regimenDescripcion: desc, monto: acre });
    if (ret !== 0) retenidoPorRegimen.push({ regimenClave: regimen, regimenDescripcion: desc, monto: ret });
  }

  const resultado = trasladado - acreditable - retenido;

  // Acumulado anual (misma fórmula T − A − R, pero rango = enero → fechaFin).
  const añoInicio = new Date(fechaInicio + 'T00:00:00').getFullYear();
  const acumFR = conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
  const { rows: [acumRow] } = await pool.query(`
    SELECT
      COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
      COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
      (
        COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
        COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
      ) as total
    FROM cfdis
    WHERE ${VIGENTE}
      AND (${REGIMEN_TENANT}) = ANY($3)
      AND ${acumFR}${extra}
      AND (${ctx.esEmisor} OR ${ctx.esReceptor})
  `, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES]);

  return {
    trasladado,
    trasladadoPorRegimen,
    acreditable,
    acreditablePorRegimen,
    retenido,
    retenidoPorRegimen,
    resultado,
    acumuladoAnual: Number(acumRow?.total || 0),
  };
}

/**
 * Calcula ISR progresivo según tarifa del Art. 96
 */
async function calcularIsrProgresivo(baseGravable: number, anio: number): Promise<number> {
  if (baseGravable <= 0) return 0;

  const tarifas = await prisma.isrTarifa.findMany({
    where: { anio },
    orderBy: { limiteInferior: 'asc' },
  });

  if (tarifas.length === 0) return 0;

  // Encontrar el rango correcto
  let tarifa = tarifas[tarifas.length - 1]; // default: último rango
  for (const t of tarifas) {
    const ls = t.limiteSuperior ? Number(t.limiteSuperior) : Infinity;
    if (baseGravable >= Number(t.limiteInferior) && baseGravable <= ls) {
      tarifa = t;
      break;
    }
  }

  const excedente = baseGravable - Number(tarifa.limiteInferior);
  const impuestoMarginal = excedente * (Number(tarifa.porcentajeExcedente) / 100);
  return Number(tarifa.cuotaFija) + impuestoMarginal;
}

/**
 * Calcula ISR RESICO PF según Art. 113-E
 */
async function calcularIsrResicoPF(ingresos: number, anio: number): Promise<number> {
  if (ingresos <= 0) return 0;

  const tasas = await prisma.isrResicoTasa.findMany({
    where: { anio },
    orderBy: { montoMaximo: 'asc' },
  });

  if (tasas.length === 0) return 0;

  for (const t of tasas) {
    if (ingresos <= Number(t.montoMaximo)) {
      return ingresos * (Number(t.porcentaje) / 100);
    }
  }

  // Si supera todos los rangos, usar el último
  const ultima = tasas[tasas.length - 1];
  return ingresos * (Number(ultima.porcentaje) / 100);
}

/**
 * Resumen ISR con cálculo por régimen, coeficiente de utilidad, ISR progresivo/RESICO.
 */
export async function getResumenIsr(
  pool: Pool,
  fechaInicio: string,
  fechaFin: string,
  tenantId: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
  considerarActivos: boolean = true,
  considerarNCs: boolean = true,
): Promise<ResumenIsr> {
  const FR = getFR(conciliacion);
  const extra = buildExtraFilters(considerarActivos, considerarNCs);

  // Ingresos ISR = misma lógica que "Ingresos del Mes" del dashboard
  const ingresosData = await calcularIngresosPorRegimen(pool, tenantId, fechaInicio, fechaFin, undefined, undefined, conciliacion, contribuyenteId, considerarActivos, considerarNCs);

  // Deducciones = gastos del periodo
  const egresosData = await calcularEgresosPorRegimen(pool, tenantId, fechaInicio, fechaFin, undefined, undefined, conciliacion, contribuyenteId, considerarActivos, considerarNCs);

  // RFC del contribuyente (o tenant) para determinar persona moral/física
  const ctx = await resolveContribuyenteContext(pool, tenantId, contribuyenteId);
  const rfcLength = ctx.rfcLength;

  // Base gravable por régimen
  const baseGravablePorRegimen: import('@horux/shared').BaseGravableRegimen[] = [];

  // Todos los regímenes que tienen ingresos (excluir 605 — sueldos, ISR retenido por patrón)
  const regimenesConDatos = new Set([
    ...ingresosData.porRegimen.map(r => r.regimenClave).filter(c => c !== '605'),
    ...egresosData.porRegimen.map(r => r.regimenClave).filter(c => c !== '605'),
  ]);

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

  for (const clave of regimenesConDatos) {
    const ing = ingresosData.porRegimen.find(r => r.regimenClave === clave)?.monto || 0;
    const ded = egresosData.porRegimen.find(r => r.regimenClave === clave)?.monto || 0;

    const formula = determinarFormulaBaseGravable(clave, rfcLength);
    const baseGravable = formula === 'ingresos-deducciones'
      ? Math.max(0, ing - ded)
      : Math.max(0, ing);

    if (baseGravable !== 0 || ing !== 0) {
      baseGravablePorRegimen.push({
        regimenClave: clave,
        regimenDescripcion: descMap.get(clave) || clave,
        ingresos: ing,
        deducciones: formula === 'ingresos-deducciones' ? ded : 0,
        baseGravable,
        isrCausado: 0, // calculated below
        formula,
      });
    }
  }

  // Exclude 605 from ISR totals (sueldos — ISR already withheld by employer)
  const ingresosPorRegimen = ingresosData.porRegimen
    .filter(r => r.regimenClave !== '605')
    .map(r => ({ regimenClave: r.regimenClave, regimenDescripcion: r.regimenDescripcion, monto: r.monto }));
  const ingresosAcumulados = ingresosPorRegimen.reduce((s, r) => s + r.monto, 0);
  const deduccionesPorRegimen = egresosData.porRegimen
    .filter(r => r.regimenClave !== '605')
    .map(r => ({ regimenClave: r.regimenClave, regimenDescripcion: r.regimenDescripcion, monto: r.monto }));
  const deducciones = deduccionesPorRegimen.reduce((s, r) => s + r.monto, 0);
  const baseGravableTotal = baseGravablePorRegimen.reduce((s, r) => s + r.baseGravable, 0);

  // ISR Retenido — filtro por RFC del contribuyente (cualquier lado).
  const { rows: [retRow] } = await pool.query(`
    SELECT COALESCE(SUM(COALESCE(isr_retencion_mxn,0) - (${EXCL_ISR_RET})), 0) as total
    FROM cfdis
    WHERE ${VIGENTE} AND ${FR}${extra}
      AND (${ctx.esEmisor} OR ${ctx.esReceptor})
  `, [fechaInicio, fechaFin]);

  // ISR Causado por régimen
  const anio = new Date(fechaFin + 'T00:00:00').getFullYear();

  // Coeficiente de utilidad del tenant (para PM y otros)
  const coefData = await prisma.coeficienteUtilidad.findUnique({
    where: { tenantId_anio: { tenantId, anio } },
  });
  const coeficiente = coefData ? Number(coefData.coeficiente) : 0;

  let isrCausado = 0;

  for (const reg of baseGravablePorRegimen) {
    let regIsrCausado = 0;
    if (reg.regimenClave === '626' && rfcLength === 13) {
      // RESICO PF: tasa plana por bracket
      regIsrCausado = await calcularIsrResicoPF(reg.baseGravable, anio);
    } else if (['606', '612', '621', '625'].includes(reg.regimenClave)) {
      // PF Empresarial: tarifa progresiva Art. 96
      regIsrCausado = await calcularIsrProgresivo(reg.baseGravable, anio);
    } else {
      // PM: base × coeficiente × tasa (30%)
      const basePM = reg.baseGravable * (coeficiente || 0.30);
      regIsrCausado = basePM * 0.30;
    }
    reg.isrCausado = Math.round(regIsrCausado * 100) / 100;
    isrCausado += regIsrCausado;
  }

  const isrRetenido = Number(retRow?.total || 0);
  const isrAPagar = Math.max(0, isrCausado - isrRetenido);

  return {
    ingresosAcumulados,
    ingresosPorRegimen,
    deducciones,
    deduccionesPorRegimen,
    baseGravable: baseGravableTotal,
    baseGravablePorRegimen,
    isrCausado: Math.round(isrCausado * 100) / 100,
    isrRetenido,
    isrAPagar: Math.round(isrAPagar * 100) / 100,
  };
}

/**
 * Desglose del cálculo provisional ISR para el mes final del filtro.
 *
 * Tres llamadas a getResumenIsr con rangos distintos:
 *   - delPeriodo: solo el mes final del filtro (1 mes calendario)
 *   - anteriores: enero hasta el mes anterior al final (vacío si mesFinal=1)
 *   - total:      enero hasta el mes final inclusive
 *
 * Si mesFinal === 1, la rama "anteriores" no llama al backend — retorna ceros
 * para evitar un query inútil.
 */
export async function getResumenIsrDesglosado(
  pool: Pool,
  fechaFin: string,
  tenantId: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
  considerarActivos: boolean = true,
  considerarNCs: boolean = true,
): Promise<import('@horux/shared').ResumenIsrDesglosado> {
  const fechaFinDate = new Date(fechaFin + 'T00:00:00');
  const anio = fechaFinDate.getFullYear();
  const mesFinal = fechaFinDate.getMonth() + 1; // 1-12

  // Helper para construir rango fin de mes
  const mmFinal = String(mesFinal).padStart(2, '0');
  const ultDiaFinal = new Date(anio, mesFinal, 0).getDate();
  const ultDiaFinalStr = String(ultDiaFinal).padStart(2, '0');

  // delPeriodo: 1er a último día del mes final
  const fiPeriodo = `${anio}-${mmFinal}-01`;
  const ffPeriodo = `${anio}-${mmFinal}-${ultDiaFinalStr}`;

  // anteriores: enero 1 al último día del (mesFinal - 1). Vacío si mesFinal=1.
  let anteriores: import('@horux/shared').ResumenIsr;
  if (mesFinal === 1) {
    anteriores = emptyResumenIsr();
  } else {
    const mesAntes = mesFinal - 1;
    const mmAntes = String(mesAntes).padStart(2, '0');
    const ultDiaAntes = new Date(anio, mesAntes, 0).getDate();
    const ultDiaAntesStr = String(ultDiaAntes).padStart(2, '0');
    const fiAnt = `${anio}-01-01`;
    const ffAnt = `${anio}-${mmAntes}-${ultDiaAntesStr}`;
    anteriores = await getResumenIsr(pool, fiAnt, ffAnt, tenantId, conciliacion, contribuyenteId, considerarActivos, considerarNCs);
  }

  // total: enero 1 al último día del mes final
  const fiTotal = `${anio}-01-01`;
  const ffTotal = `${anio}-${mmFinal}-${ultDiaFinalStr}`;

  const [delPeriodo, total] = await Promise.all([
    getResumenIsr(pool, fiPeriodo, ffPeriodo, tenantId, conciliacion, contribuyenteId, considerarActivos, considerarNCs),
    getResumenIsr(pool, fiTotal, ffTotal, tenantId, conciliacion, contribuyenteId, considerarActivos, considerarNCs),
  ]);

  return { delPeriodo, anteriores, total, mesFinal, anio };
}

function emptyResumenIsr(): import('@horux/shared').ResumenIsr {
  return {
    ingresosAcumulados: 0,
    ingresosPorRegimen: [],
    deducciones: 0,
    deduccionesPorRegimen: [],
    baseGravable: 0,
    baseGravablePorRegimen: [],
    isrCausado: 0,
    isrRetenido: 0,
    isrAPagar: 0,
  };
}

/**
 * Obtener coeficiente de utilidad del tenant para un año.
 */
export async function getCoeficiente(tenantId: string, anio: number) {
  const data = await prisma.coeficienteUtilidad.findUnique({
    where: { tenantId_anio: { tenantId, anio } },
  });
  return { anio, coeficiente: data ? Number(data.coeficiente) : null };
}

/**
 * Establecer coeficiente de utilidad del tenant para un año.
 */
export async function setCoeficiente(tenantId: string, anio: number, coeficiente: number) {
  const data = await prisma.coeficienteUtilidad.upsert({
    where: { tenantId_anio: { tenantId, anio } },
    update: { coeficiente },
    create: { tenantId, anio, coeficiente },
  });
  return { anio: data.anio, coeficiente: Number(data.coeficiente) };
}
