import { BadRequestException, Injectable } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { PrismaService } from 'src/prisma/prisma.service';
import { CuentasPagarReportesService } from './cuentas-pagar-reportes.service';
import { OrdenAbierta, ordenesAbiertasPorCuota } from './ordenes-abiertas';

/**
 * Plan de pagos a proveedores (submódulo COMP_PLAN_PAGOS; Compras → Cuentas por
 * pagar → Plan de pagos).
 *
 * Reemplaza la planilla que los clientes arman a mano: una columna por período
 * de pago y cada factura en el período en que tesorería DECIDE pagarla, que no
 * siempre es el de su vencimiento. Esa decisión vive en
 * `cuentas_pagar.fecha_pago_programada` como una fecha: el período es solo una
 * forma de mirarla, así que cada empresa elige su periodicidad (config_compras)
 * y puede cambiarla sin invalidar lo programado.
 *
 * Dónde cae cada cuota pendiente:
 *  - programada en el período actual o uno futuro → ese período ('programada');
 *  - programada en un período ya pasado y sin pagar → «Sin programar» ('atrasada');
 *  - sin programar y por vencer → el período de su vencimiento, como sugerencia
 *    ('sugerida'): se ve distinta para no confundir lo decidido con lo propuesto;
 *  - sin programar y vencida (o sin vencimiento) → «Sin programar» ('sin_programar').
 * Lo que cae más allá del último período mostrado va a «Después».
 *
 * Una cuota que ya está en una orden de pago sin ejecutar lleva `orden_abierta`: no
 * se ofrece para otra orden. Si además no estaba programada (orden hecha a mano), ya
 * no está «por decidir»: va al período de la fecha de pago de la orden ('en_orden').
 */

export type Periodicidad = 'SEMANAL' | 'QUINCENAL' | 'MENSUAL';
export type OrigenPlan = 'programada' | 'sugerida' | 'sin_programar' | 'atrasada' | 'en_orden';

export interface ConfigPlanPagos {
  periodicidad: Periodicidad;
  /** SEMANAL: 1 = lunes … 7 = domingo. */
  diaSemana: number;
  /** MENSUAL: día del mes; si el mes es más corto, el último día. */
  diaMes: number;
}

export interface PeriodoPlan {
  clave: string;
  titulo: string;
  desde: string;
  hasta: string;
  /** Fecha que se graba al programar una cuota en este período. */
  fecha_pago: string;
}

export interface FilaPlan {
  cuenta_pagar_id: string;
  proveedor_id: string;
  proveedor: string;
  ruc: string;
  moneda: string;
  comprobante: string;
  tipo: string;
  concepto: string;
  cuota: string;
  vencimiento: string | null;
  dias: number;
  saldo: number;
  fecha_programada: string | null;
  columna: string;
  origen: OrigenPlan;
  /** Orden de pago sin ejecutar en la que ya está la cuota. */
  orden_abierta: OrdenAbierta | null;
}

export const PERIODICIDADES: Periodicidad[] = ['SEMANAL', 'QUINCENAL', 'MENSUAL'];
export const CONFIG_PLAN_DEFAULT: ConfigPlanPagos = { periodicidad: 'SEMANAL', diaSemana: 5, diaMes: 31 };
export const COL_SIN_PROGRAMAR = 'sin_programar';
export const COL_DESPUES = 'despues';
export const MAX_PERIODOS = 24;

const DIA = 86_400_000;
const DIAS_SEMANA = ['', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
const MESES = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];
const ESTADO_EXCEL: Record<OrigenPlan, string> = { programada: 'Programada', sugerida: 'Sugerida', sin_programar: 'Sin programar', atrasada: 'Atrasada', en_orden: 'En orden de pago' };

const r2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
const ymd = (d: Date) => d.toISOString().slice(0, 10);
const utc = (y: number, m: number, d: number) => new Date(Date.UTC(y, m, d));
const ddmm = (d: Date) => `${String(d.getUTCDate()).padStart(2, '0')}/${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
const finDeMes = (y: number, m: number) => utc(y, m + 1, 0);
const fmtFecha = (iso: string | null) => (iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}` : '');

export function normalizarConfigPlan(raw: { plan_pagos_periodicidad?: string | null; plan_pagos_dia_semana?: number | null; plan_pagos_dia_mes?: number | null } | null): ConfigPlanPagos {
  const periodicidad = PERIODICIDADES.includes(raw?.plan_pagos_periodicidad as Periodicidad) ? (raw.plan_pagos_periodicidad as Periodicidad) : CONFIG_PLAN_DEFAULT.periodicidad;
  const ds = Number(raw?.plan_pagos_dia_semana);
  const dm = Number(raw?.plan_pagos_dia_mes);
  return {
    periodicidad,
    diaSemana: Number.isInteger(ds) && ds >= 1 && ds <= 7 ? ds : CONFIG_PLAN_DEFAULT.diaSemana,
    diaMes: Number.isInteger(dm) && dm >= 1 && dm <= 31 ? dm : CONFIG_PLAN_DEFAULT.diaMes,
  };
}

/** Lunes (00:00 UTC) de la semana que contiene `d`. */
export function lunesDe(d: Date): Date {
  const dow = d.getUTCDay(); // 0 = domingo
  return new Date(utc(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()).getTime() - ((dow + 6) % 7) * DIA);
}

/** Períodos de pago desde el que contiene `hoy`, según la periodicidad de la empresa. */
export function armarPeriodos(hoy: Date, cantidad: number, cfg: ConfigPlanPagos): PeriodoPlan[] {
  const out: PeriodoPlan[] = [];
  // Si el día de pago del período en curso ya pasó (p. ej. mensual el 10 y hoy es
  // 12), programar ahí graba hoy: nunca se programa un pago en el pasado.
  const periodo = (desde: Date, hasta: Date, pago: Date, titulo: string) =>
    out.push({ clave: ymd(desde), titulo, desde: ymd(desde), hasta: ymd(hasta), fecha_pago: ymd(pago < hoy ? hoy : pago) });

  if (cfg.periodicidad === 'SEMANAL') {
    const lunes0 = lunesDe(hoy);
    for (let i = 0; i < cantidad; i++) {
      const desde = new Date(lunes0.getTime() + i * 7 * DIA);
      const pago = new Date(desde.getTime() + (cfg.diaSemana - 1) * DIA);
      periodo(desde, new Date(desde.getTime() + 6 * DIA), pago, `${DIAS_SEMANA[cfg.diaSemana]} ${ddmm(pago)}`);
    }
    return out;
  }

  // Date.UTC resuelve el desborde de mes (m = 12 → enero del año siguiente).
  const y = hoy.getUTCFullYear();
  let m = hoy.getUTCMonth();
  if (cfg.periodicidad === 'MENSUAL') {
    for (let i = 0; i < cantidad; i++, m++) {
      const fin = finDeMes(y, m);
      const pago = utc(fin.getUTCFullYear(), fin.getUTCMonth(), Math.min(cfg.diaMes, fin.getUTCDate()));
      periodo(utc(y, m, 1), fin, pago, `${MESES[fin.getUTCMonth()]} ${fin.getUTCFullYear()}`);
    }
    return out;
  }

  // QUINCENAL: del 1 al 15 (se paga el 15) y del 16 a fin de mes (se paga el último día).
  let segunda = hoy.getUTCDate() > 15;
  for (let i = 0; i < cantidad; i++) {
    const fin = finDeMes(y, m);
    const mes = `${MESES[fin.getUTCMonth()]} ${String(fin.getUTCFullYear()).slice(2)}`;
    if (segunda) {
      periodo(utc(y, m, 16), fin, fin, `2ª ${mes}`);
      m++;
    } else {
      const quince = utc(y, m, 15);
      periodo(utc(y, m, 1), quince, quince, `1ª ${mes}`);
    }
    segunda = !segunda;
  }
  return out;
}

/** Columna y origen de una cuota pendiente. `hoy` y las fechas son medianoche UTC. */
export function ubicarCuota(
  periodos: PeriodoPlan[],
  hoy: Date,
  vencimiento: Date | null,
  programada: Date | null,
  /** Fecha de pago de la orden abierta en la que ya está la cuota, si la hay. */
  fechaOrden: string | null = null,
): { columna: string; origen: OrigenPlan } {
  const inicio = periodos[0].desde;
  const columnaDe = (f: Date) => {
    const iso = ymd(f);
    return periodos.find((p) => p.desde <= iso && iso <= p.hasta)?.clave ?? COL_DESPUES;
  };

  // En una orden de pago sin programar: ya está decidida, va a la fecha de la orden
  // (si esa fecha ya pasó, al período en curso: la orden sigue pendiente).
  if (fechaOrden && !programada) {
    const f = fechaOrden < inicio ? new Date(`${inicio}T00:00:00Z`) : new Date(`${fechaOrden}T00:00:00Z`);
    return { columna: columnaDe(f), origen: 'en_orden' };
  }

  if (programada) {
    if (ymd(programada) < inicio) return { columna: COL_SIN_PROGRAMAR, origen: 'atrasada' };
    return { columna: columnaDe(programada), origen: 'programada' };
  }
  if (!vencimiento || vencimiento.getTime() < hoy.getTime()) return { columna: COL_SIN_PROGRAMAR, origen: 'sin_programar' };
  return { columna: columnaDe(vencimiento), origen: 'sugerida' };
}

@Injectable()
export class PlanPagosService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly reportes: CuentasPagarReportesService,
  ) {}

  private hoy(): Date {
    const n = new Date();
    return utc(n.getFullYear(), n.getMonth(), n.getDate());
  }

  private async config(empresaId: string): Promise<ConfigPlanPagos> {
    const cfg = await this.prisma.config_compras.findUnique({
      where: { empresa_id: empresaId },
      select: { plan_pagos_periodicidad: true, plan_pagos_dia_semana: true, plan_pagos_dia_mes: true },
    });
    return normalizarConfigPlan(cfg);
  }

  async plan(empresaId: string, usuarioId: string | undefined, opts: { periodos?: number; proveedorIds?: string[] } = {}) {
    const cantidad = Math.min(Math.max(opts.periodos ?? 6, 1), MAX_PERIODOS);
    const hoy = this.hoy();
    const [cfg, det] = await Promise.all([this.config(empresaId), this.reportes.detalle(empresaId, usuarioId, { proveedorIds: opts.proveedorIds })]);
    const periodos = armarPeriodos(hoy, cantidad, cfg);
    const claves = [COL_SIN_PROGRAMAR, ...periodos.map((p) => p.clave), COL_DESPUES];

    const cuotaIds = det.proveedores.flatMap((p) => p.documentos.flatMap((d) => d.cuotas.map((q) => q.cuenta_pagar_id)));
    const abiertas = await ordenesAbiertasPorCuota(this.prisma, empresaId, cuotaIds);

    const filas: FilaPlan[] = [];
    // Por moneda: total de cada columna y, aparte, lo que ya está decidido (programado).
    const totales = new Map<string, { total: Record<string, number>; programado: Record<string, number>; enOrden: Record<string, number> }>();
    const vacio = () => Object.fromEntries(claves.map((c) => [c, 0]));

    for (const p of det.proveedores) {
      for (const d of p.documentos) {
        for (const q of d.cuotas) {
          const orden = abiertas.get(q.cuenta_pagar_id) ?? null;
          const { columna, origen } = ubicarCuota(periodos, hoy, q.vencimiento, q.programada, orden?.fecha_pago ?? null);
          filas.push({
            cuenta_pagar_id: q.cuenta_pagar_id,
            proveedor_id: p.proveedor_id,
            proveedor: p.razon_social,
            ruc: p.ruc,
            moneda: p.moneda,
            comprobante: d.numero,
            tipo: d.tipo,
            concepto: d.concepto,
            cuota: `${q.nro}/${q.de}`,
            vencimiento: q.vencimiento ? ymd(q.vencimiento) : null,
            dias: q.dias,
            saldo: q.saldo,
            fecha_programada: q.programada ? ymd(q.programada) : null,
            columna,
            origen,
            orden_abierta: orden,
          });
          const t = totales.get(p.moneda) ?? { total: vacio(), programado: vacio(), enOrden: vacio() };
          t.total[columna] = r2(t.total[columna] + q.saldo);
          // Lo que está en una orden también está decidido: cuenta como programado.
          if (origen === 'programada' || origen === 'en_orden') t.programado[columna] = r2(t.programado[columna] + q.saldo);
          if (orden) t.enOrden[columna] = r2(t.enOrden[columna] + q.saldo);
          totales.set(p.moneda, t);
        }
      }
    }

    return {
      fecha_corte: ymd(hoy),
      periodicidad: cfg.periodicidad,
      periodos,
      filas,
      totales: [...totales.entries()].map(([moneda, t]) => ({ moneda, por_columna: t.total, programado_por_columna: t.programado, en_orden_por_columna: t.enOrden })),
    };
  }

  /**
   * Programa (o desprograma, con `fecha` null) un lote de cuotas. Solo toca
   * cuotas pendientes de la empresa que no estén en una orden de pago abierta; no
   * se programa en un período ya pasado.
   */
  async programar(empresaId: string, usuarioId: string, ids: string[], fecha: string | null) {
    const unicos = [...new Set(ids)];
    if (!unicos.length) throw new BadRequestException('Elegí al menos una cuota');

    let fechaPago: Date | null = null;
    if (fecha) {
      fechaPago = /^\d{4}-\d{2}-\d{2}$/.test(fecha) ? new Date(`${fecha}T00:00:00Z`) : null;
      if (!fechaPago || Number.isNaN(fechaPago.getTime()) || ymd(fechaPago) !== fecha) throw new BadRequestException('Fecha inválida');
      const [actual] = armarPeriodos(this.hoy(), 1, await this.config(empresaId));
      if (fecha < actual.desde) throw new BadRequestException('No se puede programar un pago en un período que ya pasó');
    }

    // Las que ya están en una orden de pago sin ejecutar no se mueven: su fecha la manda la orden.
    const enOrden = await ordenesAbiertasPorCuota(this.prisma, empresaId, unicos);
    const libres = unicos.filter((id) => !enOrden.has(id));
    if (!libres.length) return { actualizadas: 0, omitidas: unicos.length };

    const res = await this.prisma.cuentas_pagar.updateMany({
      where: { id: { in: libres }, empresa_id: empresaId, activo: true, estado: { notIn: ['pagada', 'anulada'] }, saldo_pendiente: { gt: 0 } },
      data: {
        fecha_pago_programada: fechaPago,
        fecha_pago_programada_por: fechaPago ? usuarioId : null,
        fecha_pago_programada_at: fechaPago ? new Date() : null,
        updated_at: new Date(),
      },
    });
    return { actualizadas: res.count, omitidas: unicos.length - res.count };
  }

  async planXlsx(empresaId: string, usuarioId: string | undefined, opts: { periodos?: number; proveedorIds?: string[] } = {}): Promise<Buffer> {
    const plan = await this.plan(empresaId, usuarioId, opts);
    const empresa = await this.prisma.empresas.findUnique({ where: { id: empresaId }, select: { razon_social: true } });
    const columnas = [{ clave: COL_SIN_PROGRAMAR, titulo: 'Sin programar' }, ...plan.periodos, { clave: COL_DESPUES, titulo: 'Después' }];
    const fijas = ['Proveedor', 'Comprobante', 'Concepto', 'Cuota', 'Vence', 'Estado'];
    const nCols = fijas.length + columnas.length;

    const aoa: unknown[][] = [
      [`${empresa?.razon_social ?? ''} — Plan de pagos a proveedores (${plan.periodicidad.toLowerCase()})`],
      [`Saldos al ${fmtFecha(plan.fecha_corte)} · cada cuota en el período en que se programó pagarla (o, si no se programó, en el de su vencimiento como sugerencia)`],
      [],
      [...fijas, ...columnas.map((c) => c.titulo)],
    ];
    const merges: XLSX.Range[] = [
      { s: { r: 0, c: 0 }, e: { r: 0, c: nCols - 1 } },
      { s: { r: 1, c: 0 }, e: { r: 1, c: nCols - 1 } },
    ];
    const filasMonto: number[] = [];
    const multiMoneda = plan.totales.length > 1;

    const ordenadas = [...plan.filas].sort((a, b) => a.proveedor.localeCompare(b.proveedor) || a.moneda.localeCompare(b.moneda));
    let previo = '';
    for (const f of ordenadas) {
      const nombre = `${f.proveedor}${multiMoneda ? ` (${f.moneda})` : ''}`;
      filasMonto.push(aoa.length);
      aoa.push([
        nombre === previo ? '' : nombre,
        f.comprobante,
        f.concepto,
        f.cuota,
        fmtFecha(f.vencimiento),
        f.orden_abierta ? `En ${f.orden_abierta.numero}` : ESTADO_EXCEL[f.origen],
        ...columnas.map((c) => (c.clave === f.columna ? f.saldo : '')),
      ]);
      previo = nombre;
    }
    aoa.push([]);
    for (const t of plan.totales) {
      const sufijo = multiMoneda ? ` (${t.moneda})` : '';
      filasMonto.push(aoa.length);
      aoa.push([`TOTAL${sufijo}`, '', '', '', '', '', ...columnas.map((c) => t.por_columna[c.clave] ?? 0)]);
      filasMonto.push(aoa.length);
      aoa.push([`  Programado${sufijo}`, '', '', '', '', '', ...columnas.map((c) => t.programado_por_columna[c.clave] ?? 0)]);
    }

    const ws = XLSX.utils.aoa_to_sheet(aoa);
    ws['!merges'] = merges;
    ws['!cols'] = [{ wch: 36 }, { wch: 18 }, { wch: 30 }, { wch: 7 }, { wch: 11 }, { wch: 13 }, ...columnas.map(() => ({ wch: 14 }))];
    for (const r of filasMonto) {
      for (let c = fijas.length; c < nCols; c++) {
        const cell = ws[XLSX.utils.encode_cell({ r, c })];
        if (cell && cell.t === 'n') cell.z = '#,##0';
      }
    }
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Plan de pagos');
    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }
}
