/**
 * Generador del cronograma de una venta a partir de los tramos de un plan de pago.
 *
 * Tramos: seña, entrega, cuotas, refuerzos, cuota atada a un hito de obra y saldo
 * contra posesión. Cada tramo define su monto como:
 *   - PORCENTAJE: % del precio final (el total del tramo, repartido en `cantidad`).
 *   - MONTO: monto POR CUOTA, en la moneda de la venta.
 *   - SALDO: lo que falta para llegar al precio (a lo sumo un tramo SALDO).
 *
 * Garantía (criterio de aceptación del spec): la suma de las cuotas es igual al
 * precio final al centavo. Se calcula en unidades mínimas (guaraníes o centavos)
 * para que no aparezcan errores de punto flotante.
 *
 * Fechas: cada cuota vence `inicio_meses + i × periodicidad_meses` después de la
 * fecha base (la firma del boleto; para la seña, la reserva). Las cuotas de HITO
 * y de POSESION no tienen fecha: se vuelven exigibles cuando se cumple el hito o
 * se entrega la unidad.
 *
 * La financiación con interés no está en v1: los planes son en cuotas fijas,
 * ajustables por IPC si la venta es en guaraníes.
 */

export type TramoTipo = 'SENA' | 'ENTREGA' | 'CUOTA' | 'REFUERZO' | 'HITO' | 'POSESION';
export type TramoBase = 'PORCENTAJE' | 'MONTO' | 'SALDO';

export interface TramoPlan {
  orden?: number;
  tipo: TramoTipo;
  base: TramoBase;
  valor: number;
  cantidad?: number;
  periodicidad_meses?: number;
  inicio_meses?: number;
  hito_id?: string | null;
  descripcion?: string | null;
}

export interface CuotaGenerada {
  nro: number;
  tipo: TramoTipo;
  descripcion: string;
  fecha_vencimiento: string | null; // AAAA-MM-DD
  hito_id: string | null;
  monto: number;
}

export class CronogramaError extends Error {}

const NOMBRE: Record<TramoTipo, string> = {
  SENA: 'Seña',
  ENTREGA: 'Entrega inicial',
  CUOTA: 'Cuota',
  REFUERZO: 'Refuerzo',
  HITO: 'Cuota contra hito',
  POSESION: 'Saldo contra posesión',
};

/** Suma meses a una fecha AAAA-MM-DD; el 31 pasa al último día del mes destino. */
export function sumarMeses(fecha: string, meses: number): string {
  const [y, m, d] = fecha.slice(0, 10).split('-').map(Number);
  const total = y * 12 + (m - 1) + meses;
  const anio = Math.floor(total / 12);
  const mes = total % 12;
  const ultimo = new Date(Date.UTC(anio, mes + 1, 0)).getUTCDate();
  return `${anio}-${String(mes + 1).padStart(2, '0')}-${String(Math.min(d, ultimo)).padStart(2, '0')}`;
}

export function generarCronograma(
  precio: number,
  tramos: TramoPlan[],
  fechaBase: string,
  decimales = 0,
): CuotaGenerada[] {
  const escala = 10 ** decimales;
  const precioMin = Math.round(Number(precio) * escala);
  if (!(precioMin > 0)) throw new CronogramaError('El precio tiene que ser mayor a cero');
  if (!tramos.length) throw new CronogramaError('El plan no tiene tramos');

  const ordenados = [...tramos].sort((a, b) => (a.orden ?? 0) - (b.orden ?? 0));
  const saldos = ordenados.filter((t) => t.base === 'SALDO');
  if (saldos.length > 1) throw new CronogramaError('Sólo puede haber un tramo "saldo"');

  // 1) Capital de cada tramo, en unidades mínimas
  const capitales = new Map<TramoPlan, number>();
  let sumaPct = 0;
  for (const t of ordenados) {
    const cantidad = Math.max(1, Math.trunc(Number(t.cantidad ?? 1)));
    if (t.tipo === 'HITO' && !t.hito_id) throw new CronogramaError('Un tramo contra hito necesita el hito de obra');
    if (t.base === 'SALDO') continue;
    const valor = Number(t.valor) || 0;
    if (valor < 0) throw new CronogramaError('Un tramo no puede tener valor negativo');
    if (t.base === 'PORCENTAJE') {
      sumaPct += valor;
      capitales.set(t, Math.round((precioMin * valor) / 100));
    } else {
      capitales.set(t, Math.round(valor * escala) * cantidad);
    }
  }

  const sinSaldo = [...capitales.values()].reduce((a, b) => a + b, 0);
  if (saldos.length === 1) {
    const resto = precioMin - sinSaldo;
    if (resto < 0) {
      throw new CronogramaError(`Los tramos suman ${sinSaldo / escala}, más que el precio (${precioMin / escala})`);
    }
    capitales.set(saldos[0], resto);
  } else if (sinSaldo !== precioMin) {
    // Porcentajes que suman 100 pueden dejar un residuo de redondeo: va al último tramo.
    const diferencia = precioMin - sinSaldo;
    const ultimoPct = [...ordenados].reverse().find((t) => t.base === 'PORCENTAJE');
    if (Math.abs(sumaPct - 100) < 1e-9 && ultimoPct && Math.abs(diferencia) <= ordenados.length) {
      capitales.set(ultimoPct, (capitales.get(ultimoPct) ?? 0) + diferencia);
    } else {
      throw new CronogramaError(
        `Los tramos suman ${sinSaldo / escala} y el precio es ${precioMin / escala}. Agregá un tramo "saldo" o ajustá los valores.`,
      );
    }
  }

  // 2) Reparto en cuotas y fechas
  const cuotas: CuotaGenerada[] = [];
  for (const t of ordenados) {
    const capital = capitales.get(t) ?? 0;
    if (capital <= 0) continue;
    const cantidad = Math.max(1, Math.trunc(Number(t.cantidad ?? 1)));
    const periodicidad = Math.max(1, Math.trunc(Number(t.periodicidad_meses ?? 1)));
    const inicio = Math.max(0, Math.trunc(Number(t.inicio_meses ?? 0)));
    const base = Math.floor(capital / cantidad);
    for (let i = 0; i < cantidad; i++) {
      const montoMin = i === cantidad - 1 ? capital - base * (cantidad - 1) : base;
      if (montoMin <= 0) continue;
      const sinFecha = t.tipo === 'HITO' || t.tipo === 'POSESION';
      const etiqueta = t.descripcion?.trim() || NOMBRE[t.tipo];
      cuotas.push({
        nro: cuotas.length + 1,
        tipo: t.tipo,
        descripcion: cantidad > 1 ? `${etiqueta} ${i + 1}/${cantidad}` : etiqueta,
        fecha_vencimiento: sinFecha ? null : sumarMeses(fechaBase, inicio + i * periodicidad),
        hito_id: t.tipo === 'HITO' ? (t.hito_id ?? null) : null,
        monto: montoMin / escala,
      });
    }
  }
  return cuotas;
}

/** Suma exacta de un cronograma, en la escala de la moneda. */
export function totalCronograma(cuotas: Array<{ monto: number }>, decimales = 0): number {
  const escala = 10 ** decimales;
  return cuotas.reduce((a, c) => a + Math.round(Number(c.monto) * escala), 0) / escala;
}
