/**
 * Motor de cálculo del Módulo de Presupuestos (M38).
 *
 * Criterio paraguayo (Ley 125/91): el IVA está INCLUIDO en el precio.
 *   - IVA 10%  → iva = subtotal / 11
 *   - IVA 5%   → iva = subtotal / 21
 *   - EXENTO   → iva = 0
 *
 * Funciones puras, sin dependencias de Nest/Prisma, para poder testearlas
 * de forma aislada y reutilizarlas (también se replica en el frontend).
 */

export type IvaCategoria = 'EXENTO' | 'IVA_5' | 'IVA_10';
export type ItemTipo = 'PRODUCTO' | 'SERVICIO' | 'TEXTO_LIBRE' | 'SUBTITULO' | 'DESCUENTO_GLOBAL';

export interface CalcItemInput {
  tipo: ItemTipo;
  cantidad: number;
  precio_unitario: number;
  descuento_pct: number;
  /** Descuento por monto fijo sobre el bruto del ítem. Si > 0, tiene precedencia sobre descuento_pct. */
  descuento_monto?: number;
  iva_categoria: IvaCategoria;
}

export interface CalcItemResult {
  subtotal_con_iva: number;
  monto_iva: number;
}

export interface CalcTotalsResult {
  subtotal_exento: number;
  base_iva5: number;
  iva5: number;
  base_iva10: number;
  iva10: number;
  total_bruto: number;
  descuento_global_pct: number;
  descuento_global_monto: number;
  total: number;
  total_equivalente: number | null;
}

/** Sólo los ítems con precio aportan a los totales. */
const APORTAN_TOTAL: ReadonlySet<ItemTipo> = new Set<ItemTipo>(['PRODUCTO', 'SERVICIO']);

/** Redondea a `decimales` posiciones evitando el drift de punto flotante. */
export function round(value: number, decimales = 4): number {
  if (!Number.isFinite(value)) return 0;
  const factor = 10 ** decimales;
  return Math.round((value + Number.EPSILON) * factor) / factor;
}

/** Calcula subtotal (con IVA incluido) y el IVA contenido de una línea. */
export function calcItem(item: CalcItemInput): CalcItemResult {
  if (!APORTAN_TOTAL.has(item.tipo)) {
    return { subtotal_con_iva: 0, monto_iva: 0 };
  }
  const cantidad = Number(item.cantidad) || 0;
  const precio = Number(item.precio_unitario) || 0;
  const bruto = precio * cantidad;

  // Descuento por ítem: si hay monto fijo (> 0) tiene precedencia; si no, se usa el %.
  // El monto es POR UNIDAD (igual que Factura `dDescItem` y OV `descuento_monto`),
  // por eso se multiplica por la cantidad para obtener el descuento de la línea.
  const descMontoUnit = Math.max(Number(item.descuento_monto) || 0, 0);
  let descuento: number;
  if (descMontoUnit > 0) {
    descuento = Math.min(descMontoUnit * cantidad, bruto);
  } else {
    const desc = Math.min(Math.max(Number(item.descuento_pct) || 0, 0), 100);
    descuento = bruto * (desc / 100);
  }
  const subtotal = round(bruto - descuento);

  let monto_iva = 0;
  if (item.iva_categoria === 'IVA_10') monto_iva = round(subtotal / 11);
  else if (item.iva_categoria === 'IVA_5') monto_iva = round(subtotal / 21);

  return { subtotal_con_iva: subtotal, monto_iva };
}

/**
 * Agrega los ítems calculados y aplica el descuento global sobre el total bruto.
 * @param tipoCambioUsd  cotización congelada de la moneda del presupuesto. Cuando la moneda
 *   es ≠ PYG, el total está en la moneda extranjera y el equivalente en Gs es total × tc.
 */
export function calcTotals(
  items: CalcItemInput[],
  descuentoGlobalPct = 0,
  tipoCambioUsd?: number | null,
): { items: CalcItemResult[]; totals: CalcTotalsResult } {
  let subtotal_exento = 0;
  let base_iva5 = 0;
  let iva5 = 0;
  let base_iva10 = 0;
  let iva10 = 0;
  let total_bruto = 0;

  const calculated = items.map((raw) => {
    const r = calcItem(raw);
    if (APORTAN_TOTAL.has(raw.tipo)) {
      total_bruto += r.subtotal_con_iva;
      if (raw.iva_categoria === 'EXENTO') {
        subtotal_exento += r.subtotal_con_iva;
      } else if (raw.iva_categoria === 'IVA_5') {
        iva5 += r.monto_iva;
        base_iva5 += r.subtotal_con_iva - r.monto_iva;
      } else {
        iva10 += r.monto_iva;
        base_iva10 += r.subtotal_con_iva - r.monto_iva;
      }
    }
    return r;
  });

  const descPct = Math.min(Math.max(Number(descuentoGlobalPct) || 0, 0), 100);
  const descuento_global_monto = round(total_bruto * (descPct / 100));
  const total = round(total_bruto - descuento_global_monto);

  // El descuento global reduce proporcionalmente la base gravada, el IVA y las exentas,
  // para que el IVA declarado se corresponda con el total tras el descuento (criterio PY).
  const f = 1 - descPct / 100;

  const tc = Number(tipoCambioUsd) || 0;
  const total_equivalente = tc > 0 ? round(total * tc, 4) : null;

  return {
    items: calculated,
    totals: {
      subtotal_exento: round(subtotal_exento * f),
      base_iva5: round(base_iva5 * f),
      iva5: round(iva5 * f),
      base_iva10: round(base_iva10 * f),
      iva10: round(iva10 * f),
      total_bruto: round(total_bruto),
      descuento_global_pct: descPct,
      descuento_global_monto,
      total,
      total_equivalente,
    },
  };
}
