import { Injectable, Logger, NotFoundException, BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { envs } from 'src/config/envs';
import { PrismaService } from 'src/prisma/prisma.service';
import { alcanceSucursalUsuario } from 'src/utils/alcance-sucursal';
import { CuentasPagarReportesService } from './cuentas-pagar-reportes.service';

/**
 * Extracto de cuenta corriente de un proveedor (Reportes → Cuentas por Pagar →
 * Estado de cuenta → Extracto): cada movimiento con su débito, crédito, el saldo
 * de la factura afectada y el saldo acumulado. Es lo que el contador compara contra
 * el estado de cuenta que manda el proveedor.
 *
 * Fuentes:
 *  - Débito: cada documento (compra, gasto, importación) que generó cuentas por
 *    pagar, por la suma de sus cuotas (`monto_original`), a su fecha de emisión.
 *  - Crédito: pagos (`pagos_proveedor`, uno por OP/recibo y documento), gastos de
 *    contado (se pagan al registrarse y no dejan fila en `pagos_proveedor`), notas
 *    de crédito de compra registradas y compensaciones en recibos de cobro.
 *  - Facturas con el mismo número y monto se marcan `posible_duplicado` (p. ej.
 *    la misma factura cargada como gasto y como compra).
 *
 * El sistema no guarda todo lo que movió un saldo (p. ej. el sobrante de una NC
 * mayor a la deuda no queda registrado). Por eso el extracto se CUADRA contra la
 * deuda real: si los movimientos no explican el saldo pendiente de hoy, se agrega
 * una línea visible «Diferencia no explicada por movimientos».
 */

export type TipoMovimiento = 'factura' | 'pago' | 'nota_credito' | 'compensacion' | 'diferencia';

export interface MovimientoExtracto {
  fecha: string;
  tipo: TipoMovimiento;
  comprobante: string;
  /** Documento al que se aplica (el propio, si es una factura). */
  documento: string | null;
  concepto: string;
  debito: number;
  credito: number;
  /** Saldo de la factura afectada después de este movimiento. */
  saldo_documento: number | null;
  saldo: number;
  /** Otra factura del período tiene el mismo número y monto. */
  posible_duplicado?: boolean;
}

export interface BloqueMoneda {
  moneda: string;
  saldo_anterior: number;
  movimientos: MovimientoExtracto[];
  total_debito: number;
  total_credito: number;
  saldo_final: number;
  /** Saldo pendiente real de hoy (suma de cuotas). */
  saldo_pendiente_hoy: number;
  /** Diferencia hoy entre la deuda real y los movimientos; 0 si cuadra. */
  diferencia: number;
}

interface Crudo {
  fecha: string;
  tipo: TipoMovimiento;
  comprobante: string;
  docKey: string | null;
  concepto: string;
  debito: number;
  credito: number;
  moneda: string;
  orden: number;
  posibleDuplicado?: boolean;
}

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;
};
/** Fecha calendario de una columna DATE o TIMESTAMP sin zona (Prisma la entrega como UTC). */
const ymd = (d: Date | null | undefined) => (d ? new Date(d).toISOString().slice(0, 10) : '');
/** Fecha calendario en Paraguay de una columna TIMESTAMPTZ. */
const ymdPy = (d: Date | null | undefined) => (d ? new Date(d).toLocaleDateString('en-CA', { timeZone: 'America/Asuncion' }) : '');
const fmtFecha = (iso: string | null) => (iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}` : '');
const YMD = /^\d{4}-\d{2}-\d{2}$/;
/** Por debajo de esto la diferencia es redondeo, no un movimiento faltante. */
const TOLERANCIA = 1;

const ETIQUETA: Record<TipoMovimiento, string> = {
  factura: 'Factura',
  pago: 'Pago',
  nota_credito: 'Nota de crédito',
  compensacion: 'Compensación',
  diferencia: 'Diferencia',
};

@Injectable()
export class ExtractoProveedorService {
  private readonly logger = new Logger(ExtractoProveedorService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly reportes: CuentasPagarReportesService,
  ) {}

  private hoy(): string {
    return new Date().toLocaleDateString('en-CA', { timeZone: 'America/Asuncion' });
  }

  async extracto(empresaId: string, proveedorId: string, usuarioId: string | undefined, opts: { desde?: string; hasta?: string } = {}) {
    const hoy = this.hoy();
    const desde = opts.desde && YMD.test(opts.desde) ? opts.desde : `${hoy.slice(0, 4)}-01-01`;
    const hasta = opts.hasta && YMD.test(opts.hasta) ? opts.hasta : hoy;
    if (desde > hasta) throw new BadRequestException('La fecha «desde» no puede ser posterior a «hasta»');

    const proveedor = await this.prisma.proveedores.findFirst({
      where: { id: proveedorId, empresa_id: empresaId },
      select: { id: true, personas: { select: { razon_social: true, ruc: true, dv: true, email: true } } },
    });
    if (!proveedor) throw new NotFoundException('Proveedor no encontrado');

    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresaId);
    const cuentas = await this.prisma.cuentas_pagar.findMany({
      where: { empresa_id: empresaId, proveedor_id: proveedorId, activo: true, estado: { not: 'anulada' } },
      include: {
        compra_cab: { select: { establecimiento: true, punto_expedicion: true, numero_factura: true, fecha_emision: true, sucursal_id: true, observaciones: true } },
        moneda: { select: { codigo: true } },
      },
    });

    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,
            forma_pago: true,
            medio_pago: { select: { 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_id ? gastoPorId.get(c.origen_id)?.sucursal_id : null) ?? null;
    const visibles = alcance ? cuentas.filter((c) => !!sucursalDe(c) && alcance.sucursalIds.includes(sucursalDe(c))) : cuentas;

    const crudos: Crudo[] = [];
    const docKeyDe = (c: (typeof cuentas)[number]) => c.compra_id || c.origen_id || c.id;
    const docPorKey = new Map<string, { numero: string; moneda: string }>();
    const docKeyPorCuota = new Map<string, string>();
    const pendienteHoy = new Map<string, number>();

    // Débitos: un documento por compra/gasto, por la suma de sus cuotas.
    const debitos = new Map<string, Crudo>();
    const pagadoPorDoc = new Map<string, number>();
    for (const c of visibles) {
      const key = docKeyDe(c);
      const moneda = c.moneda?.codigo ?? 'PYG';
      docKeyPorCuota.set(c.id, key);
      pendienteHoy.set(moneda, r2((pendienteHoy.get(moneda) ?? 0) + num(c.saldo_pendiente)));
      let d = debitos.get(key);
      if (!d) {
        const doc = this.reportes.describirDocumento(c, c.origen_id ? gastoPorId.get(c.origen_id) : undefined);
        d = { fecha: ymd(doc.fecha_emision), tipo: 'factura', comprobante: doc.numero, docKey: key, concepto: doc.concepto, debito: 0, credito: 0, moneda, orden: 0 };
        debitos.set(key, d);
        docPorKey.set(key, { numero: doc.numero, moneda });
      }
      d.debito = r2(d.debito + num(c.monto_original));
      pagadoPorDoc.set(key, r2((pagadoPorDoc.get(key) ?? 0) + num(c.monto_pagado)));
    }
    crudos.push(...debitos.values());

    // Misma factura cargada dos veces (mismo número y monto): se avisa, no se corrige.
    const digitos = (n: string) => n.replace(/\D/g, '').slice(-7);
    const facturas = [...debitos.values()];
    for (const a of facturas) {
      a.posibleDuplicado = facturas.some(
        (b) => b !== a && b.moneda === a.moneda && digitos(b.comprobante) && digitos(b.comprobante) === digitos(a.comprobante) && Math.abs(b.debito - a.debito) <= TOLERANCIA,
      );
    }

    // Pagos: una línea por recibo/OP y documento.
    const pagos = visibles.length
      ? await this.prisma.pagos_proveedor.findMany({
          where: { empresa_id: empresaId, cuenta_pagar_id: { in: visibles.map((c) => c.id) }, activo: true, OR: [{ anulado: false }, { anulado: null }] },
          select: { cuenta_pagar_id: true, fecha_pago: true, numero_recibo: true, monto: true, referencia: true, numero_cheque: true, medio_pago: { select: { descripcion: true } } },
        })
      : [];
    const pagosAgrupados = new Map<string, Crudo>();
    for (const p of pagos) {
      const key = docKeyPorCuota.get(p.cuenta_pagar_id);
      const doc = key ? docPorKey.get(key) : undefined;
      const fecha = ymd(p.fecha_pago);
      const grupo = `${p.numero_recibo ?? ''}|${fecha}|${key}`;
      let g = pagosAgrupados.get(grupo);
      if (!g) {
        const medio = [p.medio_pago?.descripcion, p.numero_cheque ? `cheque ${p.numero_cheque}` : null, p.referencia].filter(Boolean).join(' · ');
        g = { fecha, tipo: 'pago', comprobante: p.numero_recibo || 'Pago', docKey: key ?? null, concepto: medio ? `Pago · ${medio}` : 'Pago', debito: 0, credito: 0, moneda: doc?.moneda ?? 'PYG', orden: 1 };
        pagosAgrupados.set(grupo, g);
      }
      g.credito = r2(g.credito + num(p.monto));
    }
    crudos.push(...pagosAgrupados.values());

    // Notas de crédito de compra registradas del proveedor.
    const ncs = await this.prisma.nota_credito_compra_cab.findMany({
      where: { empresa_id: empresaId, proveedor_id: proveedorId, anulado: false, estado: { not: 'anulada' } },
      select: { compra_id: true, gasto_id: true, establecimiento: true, punto_expedicion: true, numero: true, fecha_emision: true, motivo: true, total: true, moneda: { select: { codigo: true } } },
    });
    for (const nc of ncs) {
      const key = nc.compra_id ?? nc.gasto_id ?? null;
      const doc = key ? docPorKey.get(key) : undefined;
      // Con alcance por sucursal, una NC de un documento fuera de alcance no se muestra.
      if (alcance && key && !doc) continue;
      crudos.push({
        fecha: ymd(nc.fecha_emision),
        tipo: 'nota_credito',
        comprobante: [nc.establecimiento, nc.punto_expedicion, nc.numero].filter(Boolean).join('-') || 'NC',
        docKey: doc ? key : null,
        concepto: `Nota de crédito${nc.motivo ? ` · ${nc.motivo}` : ''}`,
        debito: 0,
        credito: num(nc.total),
        moneda: nc.moneda?.codigo ?? doc?.moneda ?? 'PYG',
        orden: 2,
      });
    }

    // Gastos de contado: se pagan al registrarse y no dejan fila en pagos_proveedor.
    // El crédito es lo pagado que no explican los pagos ni las NC de ese gasto (así,
    // si el contado se mandó a orden de pago, no se cuenta dos veces).
    const creditoPorDoc = new Map<string, number>();
    for (const c of crudos) if (c.docKey && c.credito) creditoPorDoc.set(c.docKey, r2((creditoPorDoc.get(c.docKey) ?? 0) + c.credito));
    for (const g of gastos) {
      if (!g.forma_pago || g.forma_pago.toUpperCase() === 'CREDITO' || !debitos.has(g.id)) continue;
      const residuo = r2((pagadoPorDoc.get(g.id) ?? 0) - (creditoPorDoc.get(g.id) ?? 0));
      if (residuo < TOLERANCIA) continue;
      const forma = g.forma_pago.toLowerCase();
      crudos.push({
        fecha: ymd(g.fecha_emision),
        tipo: 'pago',
        comprobante: g.numero_factura || 'Gasto',
        docKey: g.id,
        concepto: `Pagado al registrar el gasto (${forma})${g.medio_pago?.descripcion ? ` · ${g.medio_pago.descripcion}` : ''}`,
        debito: 0,
        credito: residuo,
        moneda: debitos.get(g.id).moneda,
        orden: 1,
      });
    }

    // Compensaciones en recibos de cobro (cliente que además es proveedor).
    const compraIds = [...new Set(visibles.map((c) => c.compra_id).filter((x): x is string => !!x))];
    const compensaciones = compraIds.length
      ? await this.prisma.recibo_cobro_facturas_compra_aplicadas.findMany({
          where: { compra_cab_id: { in: compraIds }, recibo: { estado: { notIn: ['anulado', 'anulada'] }, fecha_anulacion: null } },
          select: { compra_cab_id: true, monto: true, recibo: { select: { numero_recibo: true, fecha_emision: true, fecha_registro: true } } },
        })
      : [];
    for (const comp of compensaciones) {
      const doc = docPorKey.get(comp.compra_cab_id);
      crudos.push({
        fecha: comp.recibo.fecha_registro ? ymd(comp.recibo.fecha_registro) : ymdPy(comp.recibo.fecha_emision),
        tipo: 'compensacion',
        comprobante: comp.recibo.numero_recibo,
        docKey: comp.compra_cab_id,
        concepto: 'Compensado en recibo de cobro',
        debito: 0,
        credito: num(comp.monto),
        moneda: doc?.moneda ?? 'PYG',
        orden: 3,
      });
    }

    const monedas = [...new Set([...crudos.map((c) => c.moneda), ...pendienteHoy.keys()])].sort((a, b) => (a === 'PYG' ? -1 : b === 'PYG' ? 1 : a.localeCompare(b)));
    const bloques = monedas
      .map((m) => this.armarBloque(m, crudos.filter((c) => c.moneda === m), docPorKey, pendienteHoy.get(m) ?? 0, desde, hasta, hoy))
      .filter((b) => b.movimientos.length || Math.abs(b.saldo_anterior) > 0 || Math.abs(b.saldo_final) > 0);

    const p = proveedor.personas;
    return {
      proveedor: { id: proveedor.id, razon_social: p?.razon_social ?? '(sin nombre)', ruc: p?.ruc ? `${p.ruc}${p.dv != null && p.dv !== '' ? `-${p.dv}` : ''}` : '', email: p?.email ?? '' },
      desde,
      hasta,
      fecha_corte: hoy,
      monedas: bloques,
    };
  }

  private armarBloque(
    moneda: string,
    movs: Crudo[],
    docPorKey: Map<string, { numero: string }>,
    pendienteHoy: number,
    desde: string,
    hasta: string,
    hoy: string,
  ): BloqueMoneda {
    movs.sort((a, b) => a.fecha.localeCompare(b.fecha) || a.orden - b.orden || a.comprobante.localeCompare(b.comprobante));

    // Lo que explican los movimientos hasta hoy vs. la deuda real.
    const explicado = r2(movs.reduce((acc, m) => acc + m.debito - m.credito, 0));
    const diferencia = r2(pendienteHoy - explicado);
    const hayDiferencia = Math.abs(diferencia) >= TOLERANCIA;

    const saldoDoc = new Map<string, number>();
    let saldo = 0;
    let saldoAnterior = 0;
    const movimientos: MovimientoExtracto[] = [];
    let totalDebito = 0;
    let totalCredito = 0;

    for (const m of movs) {
      if (m.fecha > hasta) break;
      saldo = r2(saldo + m.debito - m.credito);
      let sd: number | null = null;
      if (m.docKey) {
        sd = r2((saldoDoc.get(m.docKey) ?? 0) + m.debito - m.credito);
        saldoDoc.set(m.docKey, sd);
      }
      if (m.fecha < desde) {
        saldoAnterior = saldo;
        continue;
      }
      totalDebito = r2(totalDebito + m.debito);
      totalCredito = r2(totalCredito + m.credito);
      movimientos.push({
        fecha: m.fecha,
        tipo: m.tipo,
        comprobante: m.comprobante,
        documento: m.docKey && m.tipo !== 'factura' ? (docPorKey.get(m.docKey)?.numero ?? null) : m.tipo === 'factura' ? m.comprobante : null,
        concepto: m.concepto,
        debito: m.debito,
        credito: m.credito,
        saldo_documento: sd,
        saldo,
        ...(m.posibleDuplicado ? { posible_duplicado: true } : {}),
      });
    }

    // La diferencia es de hoy: solo entra al extracto si el período llega a hoy.
    if (hayDiferencia && hasta >= hoy) {
      saldo = r2(saldo + diferencia);
      if (diferencia > 0) totalDebito = r2(totalDebito + diferencia);
      else totalCredito = r2(totalCredito - diferencia);
      movimientos.push({
        fecha: hoy,
        tipo: 'diferencia',
        comprobante: '—',
        documento: null,
        concepto: 'Diferencia no explicada por movimientos (ajuste para cuadrar con la deuda real)',
        debito: diferencia > 0 ? diferencia : 0,
        credito: diferencia < 0 ? -diferencia : 0,
        saldo_documento: null,
        saldo,
      });
    }

    return {
      moneda,
      saldo_anterior: saldoAnterior,
      movimientos,
      total_debito: totalDebito,
      total_credito: totalCredito,
      saldo_final: saldo,
      saldo_pendiente_hoy: pendienteHoy,
      diferencia: hayDiferencia ? diferencia : 0,
    };
  }

  async extractoXlsx(empresaId: string, proveedorId: string, usuarioId: string | undefined, opts: { desde?: string; hasta?: string } = {}): Promise<Buffer> {
    const ex = await this.extracto(empresaId, proveedorId, usuarioId, opts);
    const empresa = await this.prisma.empresas.findUnique({ where: { id: empresaId }, select: { razon_social: true } });
    const COLS = 9;
    const aoa: unknown[][] = [];
    const merges: XLSX.Range[] = [];
    const filasMonto: number[] = [];
    const titulo = (t: string) => {
      merges.push({ s: { r: aoa.length, c: 0 }, e: { r: aoa.length, c: COLS - 1 } });
      aoa.push([t]);
    };

    titulo(`${empresa?.razon_social ?? ''} — Extracto de cuenta del proveedor`);
    titulo(`${ex.proveedor.razon_social}${ex.proveedor.ruc ? ` · RUC ${ex.proveedor.ruc}` : ''} · del ${fmtFecha(ex.desde)} al ${fmtFecha(ex.hasta)}`);
    aoa.push([]);

    for (const b of ex.monedas) {
      if (ex.monedas.length > 1) titulo(`Moneda: ${b.moneda}`);
      aoa.push(['Fecha', 'Comprobante', 'Tipo', 'Aplica a', 'Concepto', 'Débito', 'Crédito', 'Saldo factura', 'Saldo']);
      filasMonto.push(aoa.length);
      aoa.push(['', '', '', '', `Saldo anterior al ${fmtFecha(ex.desde)}`, '', '', '', b.saldo_anterior]);
      for (const m of b.movimientos) {
        filasMonto.push(aoa.length);
        const concepto = m.posible_duplicado ? `${m.concepto} — ⚠ posible duplicado (mismo número y monto)` : m.concepto;
        aoa.push([fmtFecha(m.fecha), m.comprobante, ETIQUETA[m.tipo], m.tipo === 'factura' ? '' : (m.documento ?? ''), concepto, m.debito || '', m.credito || '', m.saldo_documento ?? '', m.saldo]);
      }
      filasMonto.push(aoa.length);
      aoa.push(['', '', '', '', 'Totales del período', b.total_debito, b.total_credito, '', '']);
      filasMonto.push(aoa.length);
      aoa.push(['', '', '', '', `Saldo al ${fmtFecha(ex.hasta)}`, '', '', '', b.saldo_final]);
      if (b.diferencia && ex.hasta < ex.fecha_corte) {
        aoa.push(['', '', '', '', `Atención: hoy la deuda real no coincide con los movimientos por ${b.diferencia}. Emití el extracto hasta hoy para ver el ajuste.`]);
      }
      aoa.push([]);
    }

    const ws = XLSX.utils.aoa_to_sheet(aoa);
    ws['!merges'] = merges;
    ws['!cols'] = [{ wch: 11 }, { wch: 18 }, { wch: 15 }, { wch: 18 }, { wch: 46 }, { wch: 14 }, { wch: 14 }, { wch: 14 }, { wch: 15 }];
    for (const r of filasMonto) {
      for (const c of [5, 6, 7, 8]) {
        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, 'Extracto');
    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }

  /** PDF vía el reporte-tabla genérico del generador (una moneda por PDF). */
  async extractoPdf(empresaId: string, proveedorId: string, usuarioId: string | undefined, opts: { desde?: string; hasta?: string; moneda?: string } = {}): Promise<Buffer> {
    const ex = await this.extracto(empresaId, proveedorId, usuarioId, opts);
    const bloque = ex.monedas.find((b) => b.moneda === opts.moneda) ?? ex.monedas[0];
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { razon_social: true, nombre_fantasia: true, ruc: true, dv: true, celular: true, email: true, logo: true },
    });
    const moneda = bloque?.moneda ?? 'PYG';

    const rows: Record<string, unknown>[] = [
      { fecha: '', comprobante: '', aplica: '', concepto: `Saldo anterior al ${fmtFecha(ex.desde)}`, debito: '', credito: '', saldo_documento: '', saldo: bloque?.saldo_anterior ?? 0 },
      ...(bloque?.movimientos ?? []).map((m) => ({
        fecha: fmtFecha(m.fecha),
        comprobante: m.comprobante,
        aplica: m.tipo === 'factura' ? '' : (m.documento ?? ''),
        concepto: m.posible_duplicado ? `${m.concepto} (posible duplicado)` : m.concepto,
        debito: m.debito || '',
        credito: m.credito || '',
        saldo_documento: m.saldo_documento ?? '',
        saldo: m.saldo,
      })),
    ];

    const payload = {
      titulo: `Extracto de cuenta del proveedor${ex.monedas.length > 1 ? ` (${moneda})` : ''}`,
      empresa: {
        razon_social: empresa?.razon_social ?? empresa?.nombre_fantasia ?? '',
        nombre_fantasia: empresa?.nombre_fantasia ?? null,
        ruc: empresa?.ruc ?? '',
        dv: empresa?.dv ?? '0',
        direccion: '',
        telefono: empresa?.celular ?? '',
        email: empresa?.email ?? '',
        logo_url: empresa?.logo ?? '',
      },
      proveedor: { razon_social: ex.proveedor.razon_social, ruc: ex.proveedor.ruc, email: ex.proveedor.email },
      // A mediodía: el generador interpreta "YYYY-MM-DD" como medianoche UTC y en
      // Paraguay (UTC−3/−4) lo mostraba un día antes.
      desde: `${ex.desde}T12:00:00`,
      hasta: `${ex.hasta}T12:00:00`,
      generado_en: new Date(),
      moneda,
      columns: [
        { key: 'fecha', label: 'Fecha', weight: 1.1 },
        { key: 'comprobante', label: 'Comprobante', weight: 1.7 },
        { key: 'aplica', label: 'Aplica a', weight: 1.7 },
        { key: 'concepto', label: 'Concepto', weight: 3.4 },
        { key: 'debito', label: 'Débito', type: 'currency', weight: 1.5 },
        { key: 'credito', label: 'Crédito', type: 'currency', weight: 1.5 },
        { key: 'saldo_documento', label: 'Saldo factura', type: 'currency', weight: 1.5 },
        { key: 'saldo', label: 'Saldo', type: 'currency', weight: 1.6 },
      ],
      rows,
      totales: [
        { label: 'Débitos del período', value: bloque?.total_debito ?? 0, type: 'currency' },
        { label: 'Créditos del período', value: bloque?.total_credito ?? 0, type: 'currency' },
        { label: `Saldo al ${fmtFecha(ex.hasta)}`, value: bloque?.saldo_final ?? 0, type: 'currency' },
      ],
      pageSize: 'A4',
      orientation: 'landscape',
      tipo: 'view',
    };

    const response = await fetch(`${envs.apiGeneradorPDF}/api/reporte-tabla/generate-pdf`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    }).catch((e: Error) => {
      throw new HttpException(`No se pudo conectar con el generador de PDF: ${e.message}`, HttpStatus.BAD_GATEWAY);
    });
    if (!response.ok) {
      const errText = await response.text();
      this.logger.error(`generador-pdf reporte-tabla ${response.status}: ${errText}`);
      throw new HttpException('No se pudo generar el PDF del extracto', HttpStatus.BAD_GATEWAY);
    }
    return Buffer.from(await response.arrayBuffer());
  }
}
