import { Injectable } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { PrismaService } from 'src/prisma/prisma.service';
import { alcanceSucursalUsuario } from 'src/utils/alcance-sucursal';

/**
 * Reportes consolidados de cuentas a pagar (Compras → Cuentas a pagar → Exportar).
 *
 *  - Detalle por proveedor: cada comprobante pendiente con sus cuotas, los
 *    pagos parciales que ya recibió y la antigüedad de la deuda. Es lo que los
 *    clientes armaban a mano cruzando el resumen con el estado de cuenta de a
 *    un proveedor.
 *  - Plan de pagos: lo pendiente ubicado en el mes en que vence, para saber
 *    cuánta plata hace falta y cuándo.
 *
 * Los pagos salen de `pagos_proveedor`: la orden de pago, al pagarse, escribe
 * ahí una fila por cuota con su número (numero_recibo), así que es la fuente
 * única y no hay que sumarle las órdenes aparte.
 */

export interface FiltrosCuentasPagar {
  proveedorIds?: string[];
  soloVencidas?: boolean;
}

export interface PagoAplicado {
  fecha: Date | null;
  numero: string | null;
  medio: string | null;
  monto: number;
}

export interface CuotaDetalle {
  cuenta_pagar_id: string;
  nro: number;
  de: number;
  vencimiento: Date | null;
  /** Positivo: días vencida. Negativo: días que faltan. */
  dias: number;
  original: number;
  pagado: number;
  saldo: number;
  estado: 'Vencida' | 'Por vencer';
  pagos: PagoAplicado[];
  /** Día en que tesorería programó pagarla (plan de pagos); null = sin programar. */
  programada: Date | null;
}

export interface DocumentoDetalle {
  numero: string;
  tipo: string;
  concepto: string;
  fecha_emision: Date | null;
  cuotas: CuotaDetalle[];
}

export interface Antiguedad {
  al_dia: number;
  d1_30: number;
  d31_60: number;
  d61_90: number;
  mas_90: number;
}

export interface ProveedorDetalle {
  proveedor_id: string;
  razon_social: string;
  ruc: string;
  moneda: string;
  documentos: DocumentoDetalle[];
  saldo: number;
  vencido: number;
  antiguedad: Antiguedad;
}

const r2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
const num = (v: unknown) => {
  const n = Number(v);
  return Number.isFinite(n) ? n : 0;
};
const DIA = 86_400_000;
const MESES = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];
const antiguedadVacia = (): Antiguedad => ({ al_dia: 0, d1_30: 0, d31_60: 0, d61_90: 0, mas_90: 0 });

function sumarAntiguedad(a: Antiguedad, dias: number, saldo: number) {
  if (dias <= 0) a.al_dia += saldo;
  else if (dias <= 30) a.d1_30 += saldo;
  else if (dias <= 60) a.d31_60 += saldo;
  else if (dias <= 90) a.d61_90 += saldo;
  else a.mas_90 += saldo;
}

const fmtFecha = (d: Date | null) => {
  if (!d) return '';
  const f = new Date(d);
  return `${String(f.getUTCDate()).padStart(2, '0')}/${String(f.getUTCMonth() + 1).padStart(2, '0')}/${f.getUTCFullYear()}`;
};

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

  /** Hoy a las 00:00 UTC: las fechas de vencimiento son @db.Date (medianoche UTC). */
  private hoy(): Date {
    const n = new Date();
    return new Date(Date.UTC(n.getFullYear(), n.getMonth(), n.getDate()));
  }

  // ── Detalle por proveedor ─────────────────────────────────────────────

  async detalle(empresaId: string, usuarioId: string | undefined, filtros: FiltrosCuentasPagar = {}) {
    const hoy = this.hoy();
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresaId);

    const cuentas = await this.prisma.cuentas_pagar.findMany({
      where: {
        empresa_id: empresaId,
        activo: true,
        estado: { notIn: ['pagada', 'anulada'] },
        saldo_pendiente: { gt: 0 },
        ...(filtros.proveedorIds?.length ? { proveedor_id: { in: filtros.proveedorIds } } : {}),
        ...(filtros.soloVencidas ? { fecha_vencimiento: { lt: hoy } } : {}),
      },
      include: {
        compra_cab: {
          select: {
            establecimiento: true,
            punto_expedicion: true,
            numero_factura: true,
            fecha_emision: true,
            sucursal_id: true,
            observaciones: true,
          },
        },
        moneda: { select: { codigo: true } },
        proveedores: { select: { personas: { select: { razon_social: true, ruc: true, dv: true } } } },
      },
      orderBy: [{ fecha_vencimiento: 'asc' }, { numero_cuota: 'asc' }],
    });

    // Gastos: número, sucursal y concepto. El concepto es el tipo de gasto de sus
    // líneas; la descripción del gasto suele ser genérica ("Importado desde...").
    const gastoIds = [...new Set(cuentas.filter((c) => c.origen_tipo === 'gasto' && c.origen_id).map((c) => c.origen_id))];
    const gastos = gastoIds.length
      ? await this.prisma.gasto_cab.findMany({
          where: { id: { in: gastoIds } },
          select: {
            id: true,
            numero_factura: true,
            fecha_emision: true,
            sucursal_id: true,
            descripcion: true,
            gasto_det: { select: { tipo_gasto: { select: { descripcion: true } } } },
          },
        })
      : [];
    const gastoPorId = new Map(gastos.map((g) => [g.id, g]));

    const sucursalDe = (c: (typeof cuentas)[number]) =>
      c.compra_cab?.sucursal_id ?? (c.origen_tipo === 'gasto' && c.origen_id ? gastoPorId.get(c.origen_id)?.sucursal_id : null) ?? null;
    const visibles = alcance
      ? cuentas.filter((c) => {
          const s = sucursalDe(c);
          return !!s && alcance.sucursalIds.includes(s);
        })
      : cuentas;

    const pagos = visibles.length
      ? await this.prisma.pagos_proveedor.findMany({
          where: {
            cuenta_pagar_id: { in: visibles.map((c) => c.id) },
            empresa_id: empresaId,
            activo: true,
            OR: [{ anulado: false }, { anulado: null }],
          },
          select: {
            cuenta_pagar_id: true,
            fecha_pago: true,
            numero_recibo: true,
            monto: true,
            medio_pago: { select: { descripcion: true } },
          },
          orderBy: { fecha_pago: 'asc' },
        })
      : [];
    const pagosPorCuenta = new Map<string, PagoAplicado[]>();
    for (const p of pagos) {
      const lista = pagosPorCuenta.get(p.cuenta_pagar_id) ?? [];
      lista.push({ fecha: p.fecha_pago, numero: p.numero_recibo, medio: p.medio_pago?.descripcion ?? null, monto: num(p.monto) });
      pagosPorCuenta.set(p.cuenta_pagar_id, lista);
    }

    const proveedores = new Map<string, ProveedorDetalle>();
    const docsPorProveedor = new Map<string, Map<string, DocumentoDetalle>>();

    for (const c of visibles) {
      const moneda = c.moneda?.codigo ?? 'PYG';
      const persona = c.proveedores?.personas;
      // Un bloque por proveedor y moneda: no se suman guaraníes con dólares.
      const provKey = `${c.proveedor_id}|${moneda}`;
      if (!proveedores.has(provKey)) {
        proveedores.set(provKey, {
          proveedor_id: c.proveedor_id,
          razon_social: persona?.razon_social ?? '(sin nombre)',
          ruc: persona?.ruc ? `${persona.ruc}${persona.dv != null && persona.dv !== '' ? `-${persona.dv}` : ''}` : '',
          moneda,
          documentos: [],
          saldo: 0,
          vencido: 0,
          antiguedad: antiguedadVacia(),
        });
        docsPorProveedor.set(provKey, new Map());
      }
      const prov = proveedores.get(provKey);
      const docs = docsPorProveedor.get(provKey);

      const docKey = c.compra_id || c.origen_id || c.id;
      if (!docs.has(docKey)) {
        const doc = this.describirDocumento(c, c.origen_id ? gastoPorId.get(c.origen_id) : undefined);
        docs.set(docKey, doc);
        prov.documentos.push(doc);
      }

      const saldo = num(c.saldo_pendiente);
      const original = num(c.monto_original) || saldo;
      const venc = c.fecha_vencimiento ? new Date(c.fecha_vencimiento) : null;
      const dias = venc ? Math.round((hoy.getTime() - venc.getTime()) / DIA) : 0;

      docs.get(docKey).cuotas.push({
        cuenta_pagar_id: c.id,
        nro: c.numero_cuota ?? 1,
        de: c.total_cuotas ?? 1,
        vencimiento: venc,
        dias,
        original,
        pagado: r2(Math.max(0, original - saldo)),
        saldo,
        estado: dias > 0 ? 'Vencida' : 'Por vencer',
        pagos: pagosPorCuenta.get(c.id) ?? [],
        programada: c.fecha_pago_programada ? new Date(c.fecha_pago_programada) : null,
      });

      prov.saldo = r2(prov.saldo + saldo);
      if (dias > 0) prov.vencido = r2(prov.vencido + saldo);
      sumarAntiguedad(prov.antiguedad, dias, saldo);
    }

    // Mayor deuda primero: es lo que se mira antes.
    const lista = [...proveedores.values()].sort((a, b) => b.saldo - a.saldo);
    const totalesPorMoneda = new Map<string, { saldo: number; vencido: number; antiguedad: Antiguedad }>();
    for (const p of lista) {
      const t = totalesPorMoneda.get(p.moneda) ?? { saldo: 0, vencido: 0, antiguedad: antiguedadVacia() };
      t.saldo = r2(t.saldo + p.saldo);
      t.vencido = r2(t.vencido + p.vencido);
      for (const k of Object.keys(t.antiguedad) as (keyof Antiguedad)[]) t.antiguedad[k] = r2(t.antiguedad[k] + p.antiguedad[k]);
      totalesPorMoneda.set(p.moneda, t);
    }

    return {
      fecha_corte: hoy,
      proveedores: lista,
      totales: [...totalesPorMoneda.entries()].map(([moneda, t]) => ({ moneda, ...t })),
    };
  }

  describirDocumento(
    c: {
      origen_tipo: string | null;
      compra_cab: { establecimiento: string | null; punto_expedicion: string | null; numero_factura: string | null; fecha_emision: Date | null; observaciones: string | null } | null;
      fecha_emision: Date | null;
    },
    gasto?: { numero_factura: string | null; fecha_emision: Date | null; descripcion: string | null; gasto_det: { tipo_gasto: { descripcion: string } | null }[] },
  ): DocumentoDetalle {
    if (c.compra_cab) {
      const cc = c.compra_cab;
      return {
        numero: [cc.establecimiento, cc.punto_expedicion, cc.numero_factura].filter(Boolean).join('-') || cc.numero_factura || '-',
        tipo: c.origen_tipo === 'importacion' ? 'Importación' : 'Compra',
        concepto: cc.observaciones?.trim() || 'Compra de mercadería',
        fecha_emision: cc.fecha_emision ?? c.fecha_emision,
        cuotas: [],
      };
    }
    if (gasto) {
      const tipos = [...new Set(gasto.gasto_det.map((d) => d.tipo_gasto?.descripcion).filter((t): t is string => !!t))];
      const descripcion = gasto.descripcion?.trim() ?? '';
      const descripcionUtil = descripcion && !/^importado desde/i.test(descripcion) ? descripcion : '';
      return {
        numero: gasto.numero_factura || '-',
        tipo: 'Gasto',
        concepto: tipos.join(', ') || descripcionUtil || 'Gasto',
        fecha_emision: gasto.fecha_emision ?? c.fecha_emision,
        cuotas: [],
      };
    }
    return {
      numero: '-',
      tipo: c.origen_tipo === 'importacion' ? 'Importación' : 'Otro',
      concepto: c.origen_tipo === 'importacion' ? 'Importación' : '',
      fecha_emision: c.fecha_emision,
      cuotas: [],
    };
  }

  async detalleXlsx(empresaId: string, usuarioId: string | undefined, filtros: FiltrosCuentasPagar = {}): Promise<Buffer> {
    const data = await this.detalle(empresaId, usuarioId, filtros);
    const empresa = await this.prisma.empresas.findUnique({ where: { id: empresaId }, select: { razon_social: true } });

    const wb = XLSX.utils.book_new();

    // Hoja 1: detalle agrupado por proveedor.
    const aoa: unknown[][] = [];
    const merges: XLSX.Range[] = [];
    const filasMonto: number[] = [];
    const COLS = 11;
    const titulo = (texto: string) => {
      merges.push({ s: { r: aoa.length, c: 0 }, e: { r: aoa.length, c: COLS - 1 } });
      aoa.push([texto]);
    };

    titulo(`${empresa?.razon_social ?? ''} — Cuentas a pagar: detalle por proveedor`);
    titulo(`Saldos al ${fmtFecha(data.fecha_corte)}${filtros.soloVencidas ? ' · solo cuotas vencidas' : ''}`);
    aoa.push([]);

    for (const p of data.proveedores) {
      titulo(`${p.razon_social}${p.ruc ? ` · RUC ${p.ruc}` : ''}${p.moneda !== 'PYG' ? ` · ${p.moneda}` : ''}`);
      aoa.push(['Emisión', 'Comprobante', 'Tipo', 'Concepto', 'Cuota', 'Vence', 'Días', 'Original', 'Pagado', 'Saldo', 'Estado']);
      for (const d of p.documentos) {
        for (const q of d.cuotas) {
          filasMonto.push(aoa.length);
          aoa.push([
            fmtFecha(d.fecha_emision),
            d.numero,
            d.tipo,
            d.concepto,
            `${q.nro}/${q.de}`,
            fmtFecha(q.vencimiento),
            q.dias,
            q.original,
            q.pagado,
            q.saldo,
            q.estado,
          ]);
          for (const pg of q.pagos) {
            filasMonto.push(aoa.length);
            aoa.push(['', '', '', `   Pago ${fmtFecha(pg.fecha)}${pg.numero ? ` · ${pg.numero}` : ''}${pg.medio ? ` · ${pg.medio}` : ''}`, '', '', '', '', pg.monto, '', '']);
          }
        }
      }
      filasMonto.push(aoa.length);
      aoa.push(['', '', '', `Total ${p.razon_social}`, '', '', '', '', 'Vencido', p.vencido, '']);
      filasMonto.push(aoa.length);
      aoa.push(['', '', '', '', '', '', '', '', 'Saldo', p.saldo, '']);
      aoa.push([]);
    }

    for (const t of data.totales) {
      filasMonto.push(aoa.length);
      aoa.push(['', '', '', `TOTAL GENERAL${data.totales.length > 1 ? ` (${t.moneda})` : ''}`, '', '', '', '', 'Vencido', t.vencido, '']);
      filasMonto.push(aoa.length);
      aoa.push(['', '', '', '', '', '', '', '', 'Saldo', t.saldo, '']);
    }

    const ws = XLSX.utils.aoa_to_sheet(aoa);
    ws['!merges'] = merges;
    ws['!cols'] = [{ wch: 11 }, { wch: 18 }, { wch: 11 }, { wch: 44 }, { wch: 7 }, { wch: 11 }, { wch: 6 }, { wch: 14 }, { wch: 14 }, { wch: 14 }, { wch: 11 }];
    this.formatoMiles(ws, filasMonto, [7, 8, 9]);
    XLSX.utils.book_append_sheet(wb, ws, 'Detalle');

    // Hoja 2: antigüedad de la deuda por proveedor.
    const res: unknown[][] = [
      ['Proveedor', 'RUC', 'Moneda', 'Saldo', 'Vencido', 'Al día', '1-30 días', '31-60 días', '61-90 días', 'Más de 90'],
    ];
    for (const p of data.proveedores) {
      const a = p.antiguedad;
      res.push([p.razon_social, p.ruc, p.moneda, p.saldo, p.vencido, a.al_dia, a.d1_30, a.d31_60, a.d61_90, a.mas_90]);
    }
    for (const t of data.totales) {
      const a = t.antiguedad;
      res.push([`TOTAL${data.totales.length > 1 ? ` ${t.moneda}` : ''}`, '', t.moneda, t.saldo, t.vencido, a.al_dia, a.d1_30, a.d31_60, a.d61_90, a.mas_90]);
    }
    const wsRes = XLSX.utils.aoa_to_sheet(res);
    wsRes['!cols'] = [{ wch: 40 }, { wch: 14 }, { wch: 8 }, ...Array(7).fill({ wch: 14 })];
    this.formatoMiles(wsRes, res.map((_, i) => i).slice(1), [3, 4, 5, 6, 7, 8, 9]);
    XLSX.utils.book_append_sheet(wb, wsRes, 'Antigüedad');

    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }

  // ── Plan de pagos por mes ─────────────────────────────────────────────

  async planPagos(empresaId: string, usuarioId: string | undefined, opts: { meses?: number; proveedorIds?: string[] } = {}) {
    const meses = Math.min(Math.max(opts.meses ?? 3, 1), 12);
    const det = await this.detalle(empresaId, usuarioId, { proveedorIds: opts.proveedorIds });
    const hoy = det.fecha_corte;

    // Columnas: Vencido, el mes en curso y los siguientes, y "Después".
    const columnas: { clave: string; titulo: string }[] = [{ clave: 'vencido', titulo: 'Vencido' }];
    for (let i = 0; i < meses; i++) {
      const d = new Date(Date.UTC(hoy.getUTCFullYear(), hoy.getUTCMonth() + i, 1));
      columnas.push({ clave: `${d.getUTCFullYear()}-${d.getUTCMonth()}`, titulo: `${MESES[d.getUTCMonth()]} ${d.getUTCFullYear()}` });
    }
    columnas.push({ clave: 'despues', titulo: 'Después' });

    const columnaDe = (venc: Date | null, dias: number) => {
      if (dias > 0) return 'vencido';
      if (!venc) return columnas[1].clave;
      const clave = `${venc.getUTCFullYear()}-${venc.getUTCMonth()}`;
      return columnas.some((c) => c.clave === clave) ? clave : 'despues';
    };

    const filas: {
      proveedor: string;
      ruc: string;
      moneda: string;
      comprobante: string;
      concepto: string;
      cuota: string;
      vencimiento: Date | null;
      columna: string;
      monto: number;
    }[] = [];
    const totales = new Map<string, Record<string, number>>(); // por moneda

    for (const p of det.proveedores) {
      for (const d of p.documentos) {
        for (const q of d.cuotas) {
          const columna = columnaDe(q.vencimiento, q.dias);
          filas.push({
            proveedor: p.razon_social,
            ruc: p.ruc,
            moneda: p.moneda,
            comprobante: d.numero,
            concepto: d.concepto,
            cuota: `${q.nro}/${q.de}`,
            vencimiento: q.vencimiento,
            columna,
            monto: q.saldo,
          });
          const t = totales.get(p.moneda) ?? Object.fromEntries(columnas.map((c) => [c.clave, 0]));
          t[columna] = r2(t[columna] + q.saldo);
          totales.set(p.moneda, t);
        }
      }
    }

    return {
      fecha_corte: hoy,
      columnas,
      filas,
      totales: [...totales.entries()].map(([moneda, porColumna]) => ({ moneda, por_columna: porColumna })),
    };
  }

  async planPagosXlsx(empresaId: string, usuarioId: string | undefined, opts: { meses?: number; proveedorIds?: string[] } = {}): Promise<Buffer> {
    const plan = await this.planPagos(empresaId, usuarioId, opts);
    const empresa = await this.prisma.empresas.findUnique({ where: { id: empresaId }, select: { razon_social: true } });
    const fijas = ['Proveedor', 'Comprobante', 'Concepto', 'Cuota', 'Vence'];
    const nCols = fijas.length + plan.columnas.length;

    const aoa: unknown[][] = [
      [`${empresa?.razon_social ?? ''} — Plan de pagos a proveedores`],
      [`Saldos al ${fmtFecha(plan.fecha_corte)} · cada cuota en el mes en que vence`],
      [],
      [...fijas, ...plan.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;

    let previo = '';
    for (const f of plan.filas) {
      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),
        ...plan.columnas.map((c) => (c.clave === f.columna ? f.monto : '')),
      ]);
      previo = nombre;
    }
    aoa.push([]);
    for (const t of plan.totales) {
      filasMonto.push(aoa.length);
      aoa.push([`TOTAL A PAGAR${multiMoneda ? ` (${t.moneda})` : ''}`, '', '', '', '', ...plan.columnas.map((c) => t.por_columna[c.clave] ?? 0)]);
    }

    const ws = XLSX.utils.aoa_to_sheet(aoa);
    ws['!merges'] = merges;
    ws['!cols'] = [{ wch: 36 }, { wch: 18 }, { wch: 34 }, { wch: 7 }, { wch: 11 }, ...plan.columnas.map(() => ({ wch: 14 }))];
    this.formatoMiles(ws, filasMonto, plan.columnas.map((_, i) => fijas.length + i));
    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;
  }

  /** Separador de miles en las celdas numéricas de esas filas y columnas. */
  private formatoMiles(ws: XLSX.WorkSheet, filas: number[], columnas: number[]) {
    for (const r of filas) {
      for (const c of columnas) {
        const cell = ws[XLSX.utils.encode_cell({ r, c })];
        if (cell && cell.t === 'n') cell.z = '#,##0';
      }
    }
  }
}
