/**
 * FASE 3 (docs/plan-solicitud_credito.md): política de cobranza acordada por cuenta.
 *
 * Función central que resuelve la próxima fecha de vencimiento respetando:
 *   - intervalo_dias  (7=semanal / 15=quincenal / 30=mensual)
 *   - dia_fijo_pago_1 (1..31, mensual o primera quincena)
 *   - dia_fijo_pago_2 (1..31, segunda quincena — solo aplica quincenal)
 *   - dia_cobro_semana (1..7, 1=Lunes ... 7=Domingo)
 *
 * Cualquier campo NULL cae a fallback plano `fechaBase + intervalo_dias`.
 * Días > días-del-mes se clampean al último día del mes destino
 * (evita "vence el 31 de febrero → 3 de marzo").
 */

export interface PoliticaCobro {
  intervalo_dias?: number | null;
  dia_fijo_pago_1?: number | null;
  dia_fijo_pago_2?: number | null;
  dia_cobro_semana?: number | null;
}

export interface CuotaCalculada {
  nro_cuota: number;
  dvenccuo: Date;
  dmoncuota: number;
  saldo_pendiente: number;
}

// ─── Helpers ────────────────────────────────────────────────────────────────

/** Días del mes (year, month 0-based). */
function daysInMonth(year: number, month: number): number {
  return new Date(year, month + 1, 0).getDate();
}

/** Devuelve una fecha con el día clamped al último día del mes si excede. */
function dateWithDay(year: number, month: number, day: number): Date {
  const clamped = Math.min(day, daysInMonth(year, month));
  return new Date(year, month, clamped);
}

/**
 * Próxima ocurrencia del día D del mes (day 1..31) a partir de `fechaBase`
 * (inclusive si la base cae exactamente ese día).
 */
function nextOccurrenceOfDayOfMonth(fechaBase: Date, day: number): Date {
  const y = fechaBase.getFullYear();
  const m = fechaBase.getMonth();
  const candidato = dateWithDay(y, m, day);
  if (candidato >= atMidnight(fechaBase)) return candidato;
  // Pasar al mes siguiente
  return dateWithDay(y, m + 1, day);
}

/**
 * Próxima ocurrencia del día de la semana (dow 1..7, 1=Lunes) a partir de `fechaBase`.
 */
function nextOccurrenceOfDayOfWeek(fechaBase: Date, dow: number): Date {
  const base = atMidnight(fechaBase);
  // JS: 0=Domingo..6=Sábado → convertir a 1=Lunes..7=Domingo
  const jsDow = base.getDay();
  const currentDow = jsDow === 0 ? 7 : jsDow;
  let delta = dow - currentDow;
  if (delta < 0) delta += 7;
  const out = new Date(base);
  out.setDate(out.getDate() + delta);
  return out;
}

function atMidnight(d: Date): Date {
  const out = new Date(d);
  out.setHours(0, 0, 0, 0);
  return out;
}

function addDays(d: Date, days: number): Date {
  const out = new Date(d);
  out.setDate(out.getDate() + days);
  return out;
}

function addMonths(d: Date, months: number): Date {
  const out = new Date(d);
  const day = out.getDate();
  out.setDate(1);
  out.setMonth(out.getMonth() + months);
  const maxDay = daysInMonth(out.getFullYear(), out.getMonth());
  out.setDate(Math.min(day, maxDay));
  return out;
}

// ─── Función central ────────────────────────────────────────────────────────

/**
 * Calcula la fecha del próximo vencimiento a partir de `fechaBase`.
 *
 * @param politica Política acordada.
 * @param fechaBase Fecha desde la cual buscar (típicamente hoy o la cuota anterior).
 * @param posicion  0..N — índice de la cuota. Para quincenal con dos días,
 *                  permite alternar (par → día_1, impar → día_2).
 *                  Para el resto es informativo.
 */
export function calcularProximoVencimiento(
  politica: PoliticaCobro,
  fechaBase: Date,
  posicion: number = 0,
): Date {
  const intervalo = politica.intervalo_dias ?? 30;

  // ── Semanal ─────────────────────────────────────────────────────────────
  if (intervalo === 7) {
    if (politica.dia_cobro_semana) {
      return nextOccurrenceOfDayOfWeek(fechaBase, politica.dia_cobro_semana);
    }
    return atMidnight(addDays(fechaBase, 7));
  }

  // ── Quincenal ───────────────────────────────────────────────────────────
  if (intervalo === 15) {
    const { dia_fijo_pago_1: d1, dia_fijo_pago_2: d2, dia_cobro_semana: dow } = politica;

    if (d1 && d2) {
      // Alternar por posición: pares → d1, impares → d2.
      const dia = posicion % 2 === 0 ? d1 : d2;
      return nextOccurrenceOfDayOfMonth(fechaBase, dia);
    }
    if (d1 && !d2) {
      // Un solo día conocido: alternar entre d1 y d1+15 (con clamp).
      const dia = posicion % 2 === 0 ? d1 : Math.min(d1 + 15, 31);
      return nextOccurrenceOfDayOfMonth(fechaBase, dia);
    }
    if (dow) {
      // Cada 15 días el mismo día de la semana.
      const proximo = nextOccurrenceOfDayOfWeek(fechaBase, dow);
      return posicion === 0 ? proximo : addDays(proximo, 0); // se resuelve iterativo desde el llamador
    }
    return atMidnight(addDays(fechaBase, 15));
  }

  // ── Mensual (default) ───────────────────────────────────────────────────
  if (intervalo === 30) {
    if (politica.dia_fijo_pago_1) {
      return nextOccurrenceOfDayOfMonth(fechaBase, politica.dia_fijo_pago_1);
    }
    return atMidnight(addMonths(fechaBase, 1));
  }

  // ── Intervalo custom (no 7/15/30) ───────────────────────────────────────
  return atMidnight(addDays(fechaBase, intervalo));
}

/**
 * Genera cronograma completo. Cada cuota se calcula a partir de la anterior
 * (no de la fechaBase original) — así el patrón se acumula respetando la política.
 */
export function calcularCronograma(
  politica: PoliticaCobro,
  params: {
    saldoFinanciar: number;
    montoCuota: number;
    fechaBase: Date;
    fechaPrimeraCuotaHint?: Date | null;
  },
): CuotaCalculada[] {
  const { saldoFinanciar, montoCuota } = params;
  if (montoCuota <= 0 || saldoFinanciar <= 0) return [];

  const cuotas: CuotaCalculada[] = [];
  const nroCuotas = Math.floor(saldoFinanciar / montoCuota);
  const residuo = Math.round(saldoFinanciar - montoCuota * nroCuotas);

  const intervalo = politica.intervalo_dias ?? 30;
  let fechaAnterior = atMidnight(
    params.fechaPrimeraCuotaHint ?? addDays(params.fechaBase, -1),
  );

  let saldoRest = saldoFinanciar;
  for (let i = 0; i < nroCuotas; i++) {
    const dvenccuo =
      i === 0 && params.fechaPrimeraCuotaHint
        ? atMidnight(params.fechaPrimeraCuotaHint)
        : calcularProximoVencimiento(politica, addDays(fechaAnterior, 1), i);
    const monto = montoCuota;
    saldoRest = Math.max(0, saldoRest - monto);
    cuotas.push({
      nro_cuota: i + 1,
      dvenccuo,
      dmoncuota: monto,
      saldo_pendiente: saldoRest,
    });
    fechaAnterior = dvenccuo;
    // Para el modo "cada 15 días el mismo día de la semana": avanzar 15 días base.
    if (intervalo === 15 && !politica.dia_fijo_pago_1 && !politica.dia_fijo_pago_2 && politica.dia_cobro_semana) {
      fechaAnterior = addDays(dvenccuo, 14);
    }
  }

  // Residuo → se absorbe en la última cuota (nunca genera una cuota extra corta).
  if (residuo > 0 && cuotas.length > 0) {
    const last = cuotas[cuotas.length - 1];
    last.dmoncuota += residuo;
  } else if (residuo > 0) {
    // saldoFinanciar < montoCuota: una única cuota igual al saldo.
    const dvenccuo = params.fechaPrimeraCuotaHint
      ? atMidnight(params.fechaPrimeraCuotaHint)
      : calcularProximoVencimiento(politica, params.fechaBase, 0);
    cuotas.push({
      nro_cuota: 1,
      dvenccuo,
      dmoncuota: residuo,
      saldo_pendiente: 0,
    });
  }

  return cuotas;
}

/**
 * Devuelve el día de gracia efectivo para una cuenta a cobrar.
 * Override de la cuenta gana sobre el config global de mora.
 */
export function resolverDiasGracia(
  cuenta: { dias_gracia_override?: number | null },
  configMora: { periodo_gracia_dias?: number | null } | null,
): number {
  if (cuenta.dias_gracia_override != null) return cuenta.dias_gracia_override;
  if (configMora?.periodo_gracia_dias != null) return configMora.periodo_gracia_dias;
  return 0;
}
