import type { ResolvedorMapeo } from './mapeo-cuentas.service';
import { DimensionesDocumento } from './reglas-mapeo';

/**
 * Apertura por ítem (spec 2026-10-13 "reglas de cuenta por dimensión", §5.2).
 * Cuando la empresa tiene reglas con dimensiones de ítem para un concepto de
 * ventas o costo, la línea de ese concepto se abre en una por cuenta resultante,
 * conservando el total de la tasa de la cabecera.
 */

export type ItemNeto = { neto: number; dims: DimensionesDocumento };

const redondear = (n: number, d: number) => Math.round(n * 10 ** d) / 10 ** d;

/** Agrupa los netos por la cuenta que resuelve cada ítem y ajusta el redondeo para que la suma sea exactamente `totalTasa`. */
export async function abrirPorCuenta(
  items: ItemNeto[],
  totalTasa: number,
  cuentaDe: (dims: DimensionesDocumento) => Promise<string>,
  decimales: number,
): Promise<Map<string, number>> {
  const sumaNetos = items.reduce((a, i) => a + i.neto, 0);
  const factor = sumaNetos > 0 ? totalTasa / sumaNetos : 0; // prorrateo al total de la cabecera
  const porCuenta = new Map<string, number>();
  for (const it of items) {
    const cuenta = await cuentaDe(it.dims);
    porCuenta.set(cuenta, (porCuenta.get(cuenta) ?? 0) + it.neto * factor);
  }
  if (!porCuenta.size) return porCuenta;
  for (const [c, v] of porCuenta) porCuenta.set(c, redondear(v, decimales));
  // La línea mayor se lleva la diferencia: la suma da exactamente el total, aun
  // si el total trae más decimales que el redondeo (compras en guaraníes con
  // centavos). Con una sola cuenta, esa línea es el total tal cual: el asiento de hoy.
  const mayor = [...porCuenta.entries()].sort((a, b) => b[1] - a[1])[0][0];
  const otros = [...porCuenta.entries()].reduce((a, [c, v]) => (c === mayor ? a : a + v), 0);
  const resto = totalTasa - otros;
  porCuenta.set(mayor, porCuenta.size > 1 && redondear(totalTasa, decimales) === totalTasa ? redondear(resto, decimales) : resto);
  return porCuenta;
}

/** Producto del ítem, con lo que hace falta para sus dimensiones. */
export type ProductoItem = { tipo: string | null; rubro_id: string | null; categoria_id: string | null; categoria?: { rubro_id: string | null } | null };

/** Select de Prisma del producto de un ítem (factura_det, nota_credito_det, compra_det, nota_credito_compra_det). */
export const SELECT_PRODUCTO_ITEM = {
  select: { tipo: true, rubro_id: true, categoria_id: true, categoria: { select: { rubro_id: true } } },
} as const;

/**
 * Dimensiones de ítem: categoría del producto, su rubro (el propio o el de la
 * categoría) y su tipo. Un ítem sin producto no publica ninguna.
 */
export function dimensionesItem(p: ProductoItem | null | undefined): DimensionesDocumento {
  if (!p) return {};
  const dims: DimensionesDocumento = {
    CATEGORIA_PRODUCTO: p.categoria_id,
    RUBRO: p.rubro_id ?? p.categoria?.rubro_id ?? null,
    TIPO_ITEM: p.tipo,
  };
  return Object.fromEntries(Object.entries(dims).filter(([, v]) => v !== undefined && v !== null && v !== '')) as DimensionesDocumento;
}

/** Una línea abierta: la cuenta, su monto y las dimensiones del ítem con que se resolvió (para la traza). */
export type LineaAbierta = { cuenta_id: string; monto: number; dimsItem?: DimensionesDocumento };

/**
 * Abre el total de un concepto por la cuenta de cada ítem. Las cuentas se
 * resuelven una vez por combinación de dimensiones (no una por ítem). Cada línea
 * lleva las dimensiones del primer ítem que la produjo, para que `anotar` le
 * ponga la traza de la regla (o del defecto) que la eligió. Una cuenta que queda
 * en cero no se asienta.
 */
export async function abrirConcepto(
  res: Pick<ResolvedorMapeo, 'cuenta'>,
  concepto: string,
  items: ItemNeto[],
  total: number,
  decimales: number,
): Promise<LineaAbierta[]> {
  const cache = new Map<string, Promise<string>>();
  const dimsDeCuenta = new Map<string, DimensionesDocumento>();
  const porCuenta = await abrirPorCuenta(
    items,
    total,
    async (dims) => {
      const clave = JSON.stringify(Object.entries(dims).sort(([a], [b]) => a.localeCompare(b)));
      if (!cache.has(clave)) cache.set(clave, res.cuenta(concepto, dims));
      const cuenta = await cache.get(clave)!;
      if (!dimsDeCuenta.has(cuenta)) dimsDeCuenta.set(cuenta, dims);
      return cuenta;
    },
    decimales,
  );
  const lineas = [...porCuenta].map(([cuenta_id, monto]) => {
    const dims = dimsDeCuenta.get(cuenta_id);
    return { cuenta_id, monto, ...(dims && Object.keys(dims).length ? { dimsItem: dims } : {}) };
  });
  const conMonto = lineas.filter((l) => l.monto !== 0);
  return conMonto.length ? conMonto : lineas.slice(0, 1);
}
