import { BadRequestException, HttpException, HttpStatus, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import * as XLSX from 'xlsx';
import { envs } from '../config/envs';
import { MailService } from '../mail/mail.service';
import { PrismaService } from '../prisma/prisma.service';
import { alcanceSucursalUsuario } from '../utils/alcance-sucursal';

/** Devuelve "YYYY-MM-DD" en hora local de Paraguay. */
function todayPYString(): string {
  const fmt = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/Asuncion',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  });
  return fmt.format(new Date());
}

/** Suma `days` a una fecha YYYY-MM-DD en PY y devuelve YYYY-MM-DD. */
function addDaysPYString(base: string, days: number): string {
  const [y, m, d] = base.split('-').map(Number);
  const dt = new Date(Date.UTC(y, m - 1, d));
  dt.setUTCDate(dt.getUTCDate() + days);
  return dt.toISOString().substring(0, 10);
}

export interface VencidosRow {
  lote_id: string;
  numero_lote: string;
  fecha_vencimiento: Date | null;
  producto_id: string;
  producto_codigo: string | null;
  producto_descripcion: string;
  marca: string;
  deposito: string | null;
  stock_actual: number;
  costo_unitario: number;
  costo_total: number;
}

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly mail: MailService,
  ) {}

  private async getProveedorOrFail(proveedor_id: string, empresa_id: string) {
    const proveedor = await this.prisma.proveedores.findFirst({
      where: { id: proveedor_id, empresa_id },
      include: { personas: true },
    });
    if (!proveedor) throw new NotFoundException('Proveedor no encontrado');
    return proveedor;
  }

  /**
   * Vencidos por proveedor. Lista todos los lotes vencidos en un rango
   * cuya marca está vinculada al proveedor (activo) y aún tienen stock.
   */
  /**
   * Fragmento SQL con el alcance del usuario, para pegar dentro de las
   * consultas crudas de este módulo.
   *
   * Va en SQL y no en Prisma porque estos reportes no se pueden expresar con el
   * cliente: son JOINs de 4 y 5 tablas con COUNT(DISTINCT ...), SUM(a * b) y
   * GROUP BY sobre columnas de tablas unidas. `groupBy` de Prisma agrupa sólo
   * por campos escalares de UN modelo y `_sum` no acepta expresiones, así que
   * el SQL crudo acá es necesario, no una optimización.
   *
   * Empuja el parámetro a `queryArgs` para no interpolar valores en el texto.
   */
  private async filtroAlcanceSql(
    empresa_id: string,
    usuarioId: string | undefined,
    queryArgs: string[],
    campo: 'deposito' | 'sucursal' | 'dest',
    alias: string,
  ): Promise<string> {
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresa_id);
    if (!alcance) return '';
    const valores =
      campo === 'deposito'
        ? alcance.depositoIds
        : campo === 'sucursal'
          ? alcance.sucursalIds
          : alcance.puntosEstablecimiento;
    // Sin valores, el usuario no alcanza nada: se fuerza un resultado vacío.
    if (valores.length === 0) return 'AND false';
    queryArgs.push(`{${valores.join(',')}}`);
    const cast = campo === 'dest' ? 'text[]' : 'uuid[]';
    const columna = campo === 'deposito' ? 'deposito_id' : campo === 'sucursal' ? 'sucursal_id' : 'dest';
    return `AND ${alias}.${columna} = ANY($${queryArgs.length}::${cast})`;
  }

  /**
   * Igual que `filtroAlcanceSql` pero para las consultas con template tag,
   * donde los parámetros se vinculan con `Prisma.sql` en vez de `queryArgs`.
   */
  private async filtroAlcancePrisma(
    empresa_id: string,
    usuarioId: string | undefined,
    campo: 'sucursal' | 'dest',
    alias: string,
  ): Promise<Prisma.Sql> {
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresa_id);
    if (!alcance) return Prisma.empty;
    const valores = campo === 'sucursal' ? alcance.sucursalIds : alcance.puntosEstablecimiento;
    if (valores.length === 0) return Prisma.sql`AND false`;
    const columna = Prisma.raw(`${alias}.${campo === 'sucursal' ? 'sucursal_id' : 'dest'}`);
    return campo === 'sucursal'
      ? Prisma.sql`AND ${columna} = ANY(ARRAY[${Prisma.join(valores)}]::uuid[])`
      : Prisma.sql`AND ${columna} = ANY(ARRAY[${Prisma.join(valores)}]::text[])`;
  }

  async vencidos(
    empresa_id: string,
    proveedor_id: string,
    params: { desde?: string; hasta?: string; deposito_id?: string },
    usuarioId?: string,
  ): Promise<VencidosRow[]> {
    await this.getProveedorOrFail(proveedor_id, empresa_id);

    const desde = params.desde ?? '1900-01-01';
    const hasta = params.hasta ?? todayPYString();

    // Parámetros vinculados (evita SQL injection vía deposito_id).
    const queryArgs: string[] = [empresa_id, proveedor_id, desde, hasta];
    let depositoFilter = '';
    if (params.deposito_id) {
      queryArgs.push(params.deposito_id);
      depositoFilter = `AND sl.deposito_id = $${queryArgs.length}::uuid`;
    }
    const alcanceFilter = await this.filtroAlcanceSql(empresa_id, usuarioId, queryArgs, 'deposito', 'sl');

    const rows: any[] = await this.prisma.$queryRawUnsafe(
      `
      SELECT lp.id::text AS lote_id,
             lp.numero_lote,
             lp.fecha_vencimiento,
             p.id::text AS producto_id,
             p.cod_producto AS producto_codigo,
             p.descripcion AS producto_descripcion,
             m.descripcion AS marca,
             d.descripcion AS deposito,
             SUM(sl.cantidad_disponible)::float AS stock_actual,
             lp.costo_unitario::float AS costo_unitario,
             (SUM(sl.cantidad_disponible) * lp.costo_unitario)::float AS costo_total
        FROM lotes_producto lp
        JOIN productos p ON p.id = lp.producto_id
        JOIN marcas m   ON m.id = p.marca_id
        JOIN proveedor_marca pm
          ON pm.marca_id = m.id
         AND pm.empresa_id = lp.empresa_id
         AND pm.activo = true
        JOIN stock_lote sl ON sl.lote_id = lp.id
        LEFT JOIN depositos d ON d.id = sl.deposito_id
       WHERE lp.empresa_id = $1::uuid
         AND pm.proveedor_id = $2::uuid
         AND lp.fecha_vencimiento BETWEEN $3::date AND $4::date
         AND sl.cantidad_disponible > 0
         ${depositoFilter}
         ${alcanceFilter}
       GROUP BY lp.id, p.id, m.descripcion, d.descripcion
       ORDER BY lp.fecha_vencimiento ASC
      `,
      ...queryArgs,
    );

    return rows as VencidosRow[];
  }

  /**
   * Próximos a vencer en 30/60/90 días, agrupado en buckets.
   */
  async proximosVencer(
    empresa_id: string,
    proveedor_id: string,
    params: { dias?: 30 | 60 | 90; deposito_id?: string },
  ) {
    await this.getProveedorOrFail(proveedor_id, empresa_id);
    const dias = params.dias ?? 90;
    if (![30, 60, 90].includes(dias)) {
      throw new BadRequestException('dias debe ser 30, 60 o 90');
    }

    const hoyStr = todayPYString();
    const hastaStr = addDaysPYString(hoyStr, dias);
    const hoy = new Date(`${hoyStr}T00:00:00`);

    const rows = await this.vencidos(empresa_id, proveedor_id, {
      desde: hoyStr,
      hasta: hastaStr,
      deposito_id: params.deposito_id,
    });

    const buckets = { '0-30': [] as VencidosRow[], '31-60': [] as VencidosRow[], '61-90': [] as VencidosRow[] };
    const totales = { '0-30': 0, '31-60': 0, '61-90': 0 };

    for (const r of rows) {
      if (!r.fecha_vencimiento) continue;
      const dif = Math.ceil(
        (new Date(r.fecha_vencimiento).getTime() - hoy.getTime()) / (1000 * 60 * 60 * 24),
      );
      let key: '0-30' | '31-60' | '61-90' | null = null;
      if (dif <= 30) key = '0-30';
      else if (dif <= 60) key = '31-60';
      else if (dif <= 90) key = '61-90';
      if (!key) continue;
      buckets[key].push(r);
      totales[key] += r.costo_total ?? 0;
    }

    return { dias, buckets, totales, total_general: totales['0-30'] + totales['31-60'] + totales['61-90'] };
  }

  /**
   * Stock total de las marcas que el proveedor provee.
   */
  async stockMarcas(empresa_id: string, proveedor_id: string, params: { deposito_id?: string }, usuarioId?: string) {
    await this.getProveedorOrFail(proveedor_id, empresa_id);

    // Parámetros vinculados (evita SQL injection vía deposito_id).
    const queryArgs: string[] = [empresa_id, proveedor_id];
    let depositoFilter = '';
    if (params.deposito_id) {
      queryArgs.push(params.deposito_id);
      depositoFilter = `AND sl.deposito_id = $${queryArgs.length}::uuid`;
    }
    const alcanceFilter = await this.filtroAlcanceSql(empresa_id, usuarioId, queryArgs, 'deposito', 'sl');

    const rows: any[] = await this.prisma.$queryRawUnsafe(
      `
      SELECT m.id::text AS marca_id,
             m.descripcion AS marca,
             COUNT(DISTINCT p.id)::int AS cantidad_productos,
             COALESCE(SUM(sl.cantidad_disponible), 0)::float AS stock_total,
             COALESCE(SUM(sl.cantidad_disponible * lp.costo_unitario), 0)::float AS valor_total
        FROM marcas m
        JOIN proveedor_marca pm
          ON pm.marca_id = m.id
         AND pm.empresa_id = m.empresa_id
         AND pm.activo = true
         AND pm.proveedor_id = $2::uuid
        LEFT JOIN productos p ON p.marca_id = m.id
        LEFT JOIN lotes_producto lp ON lp.producto_id = p.id AND lp.activo = true
        LEFT JOIN stock_lote sl ON sl.lote_id = lp.id ${depositoFilter} ${alcanceFilter}
       WHERE m.empresa_id = $1::uuid
       GROUP BY m.id, m.descripcion
       ORDER BY m.descripcion ASC
      `,
      ...queryArgs,
    );

    return rows;
  }

  /**
   * Resumen ejecutivo del proveedor.
   */
  async resumen(empresa_id: string, proveedor_id: string) {
    await this.getProveedorOrFail(proveedor_id, empresa_id);

    const [marcasCount, vencidosHoy, proximos30] = await Promise.all([
      this.prisma.proveedor_marca.count({
        where: { empresa_id, proveedor_id, activo: true },
      }),
      this.vencidos(empresa_id, proveedor_id, {
        desde: '1900-01-01',
        hasta: todayPYString(),
      }),
      this.proximosVencer(empresa_id, proveedor_id, { dias: 30 }),
    ]);

    const valorVencido = vencidosHoy.reduce((acc, r) => acc + (r.costo_total ?? 0), 0);

    return {
      marcas_que_provee: marcasCount,
      lotes_vencidos: vencidosHoy.length,
      valor_lotes_vencidos: valorVencido,
      lotes_proximos_30: proximos30.buckets['0-30'].length,
      valor_proximos_30: proximos30.totales['0-30'],
    };
  }

  /**
   * Marcas compradas a un proveedor en un rango: ranking por monto y cantidad.
   * Cruza compra_cab + compra_det + productos para mostrar también marcas aún no vinculadas.
   */
  async marcasCompradas(
    empresa_id: string,
    proveedor_id: string,
    params: { desde?: string; hasta?: string },
    usuarioId?: string,
  ) {
    await this.getProveedorOrFail(proveedor_id, empresa_id);
    const desde = params.desde ?? '1900-01-01';
    const hasta = params.hasta ?? todayPYString();

    const rows: any[] = await this.prisma.$queryRaw`
      SELECT m.id::text AS marca_id,
             COALESCE(m.descripcion, 'SIN MARCA') AS marca,
             COUNT(DISTINCT cc.id)::int AS cantidad_compras,
             COUNT(DISTINCT p.id)::int AS cantidad_productos,
             COALESCE(SUM(cd.cantidad), 0)::float AS unidades,
             COALESCE(SUM(cd.subtotal), 0)::float AS monto_total,
             MAX(cc.fecha_emision) AS ultima_compra,
             MIN(cc.fecha_emision) AS primera_compra,
             (SELECT pm.id IS NOT NULL FROM proveedor_marca pm
                WHERE pm.empresa_id = ${empresa_id}::uuid
                  AND pm.proveedor_id = ${proveedor_id}::uuid
                  AND pm.marca_id = m.id
                  AND pm.activo = true
                LIMIT 1) AS vinculado
        FROM compra_cab cc
        JOIN compra_det cd ON cd.compra_id = cc.id
        JOIN productos p   ON p.id = cd.producto_id
   LEFT JOIN marcas m      ON m.id = p.marca_id
       WHERE cc.empresa_id = ${empresa_id}::uuid
         ${await this.filtroAlcancePrisma(empresa_id, usuarioId, 'sucursal', 'cc')}
         AND cc.proveedor_id = ${proveedor_id}::uuid
         AND COALESCE(cc.anulado, false) = false
         AND cc.fecha_emision BETWEEN ${desde}::date AND ${hasta}::date
       GROUP BY m.id, m.descripcion
       ORDER BY monto_total DESC
    `;

    return rows.map((r) => ({
      ...r,
      vinculado: r.vinculado === true || r.vinculado === 't',
    }));
  }

  /**
   * Ventas por marca atribuibles a un proveedor.
   * - modo='vinculo' (default, no requiere lotes): agrupa ventas de marcas vinculadas al proveedor.
   *   Opcional: solo preferentes para evitar doble conteo cuando hay varios proveedores por marca.
   * - modo='lote': cruza factura_det_lote → lotes_producto → compra_cab.proveedor_id. Trazabilidad real.
   */
  async ventasPorMarca(
    empresa_id: string,
    proveedor_id: string,
    params: {
      desde?: string;
      hasta?: string;
      modo?: 'vinculo' | 'lote';
      soloPreferente?: boolean;
    },
    usuarioId?: string,
  ) {
    await this.getProveedorOrFail(proveedor_id, empresa_id);
    const desde = params.desde ?? '1900-01-01';
    const hasta = params.hasta ?? todayPYString();
    const modo = params.modo ?? 'vinculo';

    if (modo === 'lote') {
      const rows: any[] = await this.prisma.$queryRaw`
        SELECT m.id::text AS marca_id,
               COALESCE(m.descripcion, 'SIN MARCA') AS marca,
               p.id::text AS producto_id,
               p.cod_producto AS producto_codigo,
               p.descripcion AS producto_descripcion,
               COUNT(DISTINCT fc.id)::int AS cantidad_facturas,
               COALESCE(SUM(fdl.cantidad), 0)::float AS unidades,
               COALESCE(SUM(fdl.costo_total), 0)::float AS costo_total,
               COALESCE(SUM(fdl.cantidad * fd.duniproser), 0)::float AS monto_vendido
          FROM factura_det_lote fdl
          JOIN factura_det fd ON fd.id = fdl.factura_det_id
          JOIN factura_cab fc ON fc.id = fd.factura_cab_id
          JOIN lotes_producto lp ON lp.id = fdl.lote_id
          JOIN compra_cab cc ON cc.id = lp.compra_cab_id
          JOIN productos p   ON p.id = fd.producto_id
     LEFT JOIN marcas m      ON m.id = p.marca_id
         WHERE fc.empresa_id = ${empresa_id}::uuid
           AND cc.proveedor_id = ${proveedor_id}::uuid
           AND COALESCE(fc.evento_aplicado, '') NOT IN ('ECAN','EINU','EINO')
           ${await this.filtroAlcancePrisma(empresa_id, usuarioId, 'dest', 'fc')}
           AND (fc.dfeemide AT TIME ZONE 'UTC' AT TIME ZONE 'America/Asuncion')::date BETWEEN ${desde}::date AND ${hasta}::date
         GROUP BY m.id, m.descripcion, p.id, p.cod_producto, p.descripcion
         ORDER BY m.descripcion ASC, monto_vendido DESC
      `;
      return { modo, soloPreferente: false, rows };
    }

    const preferenteFilter = params.soloPreferente
      ? `AND pm.es_proveedor_preferente = true`
      : '';

    const queryArgs: string[] = [empresa_id, proveedor_id, desde, hasta];
    const alcanceFilter = await this.filtroAlcanceSql(empresa_id, usuarioId, queryArgs, 'dest', 'fc');

    const rows: any[] = await this.prisma.$queryRawUnsafe(
      `
      SELECT m.id::text AS marca_id,
             m.descripcion AS marca,
             p.id::text AS producto_id,
             p.cod_producto AS producto_codigo,
             p.descripcion AS producto_descripcion,
             pm.es_proveedor_preferente,
             pm.es_distribuidor_oficial,
             COUNT(DISTINCT fc.id)::int AS cantidad_facturas,
             COALESCE(SUM(fd.dcantproser), 0)::float AS unidades,
             COALESCE(SUM(fd.dtotopeitem), 0)::float AS monto_vendido,
             COALESCE(SUM(fd.costo_total_venta), 0)::float AS costo_total
        FROM proveedor_marca pm
        JOIN marcas m       ON m.id = pm.marca_id
        JOIN productos p    ON p.marca_id = m.id AND p.empresa_id = pm.empresa_id
        JOIN factura_det fd ON fd.producto_id = p.id
        JOIN factura_cab fc ON fc.id = fd.factura_cab_id
       WHERE pm.empresa_id = $1::uuid
         AND pm.proveedor_id = $2::uuid
         AND pm.activo = true
         AND fc.empresa_id = $1::uuid
         AND COALESCE(fc.evento_aplicado, '') NOT IN ('ECAN','EINU','EINO')
         AND (fc.dfeemide AT TIME ZONE 'UTC' AT TIME ZONE 'America/Asuncion')::date BETWEEN $3::date AND $4::date
         ${preferenteFilter}
         ${alcanceFilter}
       GROUP BY m.id, m.descripcion, p.id, p.cod_producto, p.descripcion, pm.es_proveedor_preferente, pm.es_distribuidor_oficial
       ORDER BY m.descripcion ASC, monto_vendido DESC
      `,
      ...queryArgs,
    );

    return { modo, soloPreferente: !!params.soloPreferente, rows };
  }

  /**
   * Indica si la empresa registra trazabilidad por lote en sus ventas.
   * Sirve para que el frontend habilite el modo "por lote" sólo si hay datos.
   */
  async capabilities(empresa_id: string, proveedor_id: string, usuarioId?: string) {
    await this.getProveedorOrFail(proveedor_id, empresa_id);
    const row: any[] = await this.prisma.$queryRaw`
      SELECT COUNT(*)::int AS total
        FROM factura_det_lote fdl
        JOIN factura_det fd ON fd.id = fdl.factura_det_id
        JOIN factura_cab fc ON fc.id = fd.factura_cab_id
       WHERE fc.empresa_id = ${empresa_id}::uuid
         ${await this.filtroAlcancePrisma(empresa_id, usuarioId, 'dest', 'fc')}
       LIMIT 1
    `;
    const tieneLotesEnVentas = (row?.[0]?.total ?? 0) > 0;
    return { tieneLotesEnVentas };
  }

  /**
   * Genera un buffer Excel del reporte de vencidos.
   */
  async exportarVencidosExcel(
    empresa_id: string,
    proveedor_id: string,
    params: { desde?: string; hasta?: string; deposito_id?: string },
  ): Promise<Buffer> {
    const rows = await this.vencidos(empresa_id, proveedor_id, params);
    const wb = XLSX.utils.book_new();
    const data = rows.map((r) => ({
      'Lote': r.numero_lote,
      'Vencimiento': r.fecha_vencimiento
        ? new Date(r.fecha_vencimiento).toISOString().substring(0, 10)
        : '',
      'Código': r.producto_codigo ?? '',
      'Producto': r.producto_descripcion,
      'Marca': r.marca,
      'Depósito': r.deposito ?? '',
      'Stock': r.stock_actual,
      'Costo Unit.': r.costo_unitario,
      'Costo Total': r.costo_total,
    }));
    const ws = XLSX.utils.json_to_sheet(data);
    XLSX.utils.book_append_sheet(wb, ws, 'Vencidos');
    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }

  /**
   * Genera PDF del reporte de vencidos llamando a msv-kude.
   */
  async exportarVencidosPDF(
    empresa_id: string,
    proveedor_id: string,
    params: { desde?: string; hasta?: string; deposito_id?: string },
  ): Promise<Buffer> {
    const proveedor = await this.getProveedorOrFail(proveedor_id, empresa_id);
    const empresa: any = await this.prisma.empresas.findUnique({
      where: { id: empresa_id },
    });
    const rows = await this.vencidos(empresa_id, proveedor_id, params);

    const body = {
      tipo: 'view',
      empresa: {
        razon_social: empresa?.razon_social ?? empresa?.nombre_fantasia ?? '',
        // Nombre comercial arriba y razón social debajo en el PDF (criterio de la factura).
        nombre_fantasia: empresa?.nombre_fantasia ?? null,
        ruc: empresa?.ruc ?? '',
        dv: empresa?.dv ?? '0',
        direccion: empresa?.direccion ?? '',
        telefono: empresa?.telefono ?? '',
        email: empresa?.email ?? '',
        logo_url: empresa?.logo_url ?? empresa?.logo ?? '',
      },
      proveedor: {
        razon_social: proveedor.personas?.razon_social ?? 'Proveedor',
        ruc: proveedor.personas?.ruc ?? '',
        email: proveedor.personas?.email ?? '',
      },
      desde: params.desde ?? null,
      hasta: params.hasta ?? null,
      generado_en: new Date(),
      moneda: 'PYG',
      items: rows,
    };

    const url = `${envs.apiGeneradorPDF}/api/vencidos-proveedor/generate-pdf`;
    this.logger.log(`Calling msv-kude vencidos-proveedor: ${url} items=${rows.length}`);

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!response.ok) {
        const errText = await response.text();
        this.logger.error(`msv-kude responded ${response.status}: ${errText}`);
        throw new HttpException(
          { message: 'Error al generar PDF', detail: errText },
          HttpStatus.BAD_GATEWAY,
        );
      }
      const ab = await response.arrayBuffer();
      return Buffer.from(ab);
    } catch (e) {
      if (e instanceof HttpException) throw e;
      const msg = e instanceof Error ? e.message : 'Error desconocido';
      throw new HttpException(
        { message: 'No se pudo conectar con msv-kude', detail: msg },
        HttpStatus.BAD_GATEWAY,
      );
    }
  }

  // ===== Excel/PDF para marcas-compradas y ventas-por-marca =====

  private async buildEmpresaProveedorPayload(empresa_id: string, proveedor_id: string) {
    const proveedor = await this.getProveedorOrFail(proveedor_id, empresa_id);
    const empresa: any = await this.prisma.empresas.findUnique({ where: { id: empresa_id } });
    return {
      empresa: {
        razon_social: empresa?.razon_social ?? empresa?.nombre_fantasia ?? '',
        // Nombre comercial arriba y razón social debajo en el PDF (criterio de la factura).
        nombre_fantasia: empresa?.nombre_fantasia ?? null,
        ruc: empresa?.ruc ?? '',
        dv: empresa?.dv ?? '0',
        direccion: empresa?.direccion ?? '',
        telefono: empresa?.telefono ?? '',
        email: empresa?.email ?? '',
        logo_url: empresa?.logo_url ?? empresa?.logo ?? '',
      },
      proveedor: {
        razon_social: proveedor.personas?.razon_social ?? 'Proveedor',
        ruc: proveedor.personas?.ruc ?? '',
        email: proveedor.personas?.email ?? '',
      },
    };
  }

  private async callMsvKudeTabla(payload: Record<string, unknown>): Promise<Buffer> {
    const url = `${envs.apiGeneradorPDF}/api/reporte-tabla/generate-pdf`;
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    if (!response.ok) {
      const errText = await response.text();
      this.logger.error(`msv-kude reporte-tabla ${response.status}: ${errText}`);
      throw new HttpException(
        { message: 'Error al generar PDF', detail: errText },
        HttpStatus.BAD_GATEWAY,
      );
    }
    const ab = await response.arrayBuffer();
    return Buffer.from(ab);
  }

  async exportarMarcasCompradasExcel(
    empresa_id: string,
    proveedor_id: string,
    params: { desde?: string; hasta?: string },
  ): Promise<Buffer> {
    const rows = await this.marcasCompradas(empresa_id, proveedor_id, params);
    const wb = XLSX.utils.book_new();
    const data = rows.map((r: any) => ({
      Marca: r.marca,
      Compras: r.cantidad_compras,
      Productos: r.cantidad_productos,
      Unidades: r.unidades,
      'Monto Total': r.monto_total,
      'Primera compra': r.primera_compra
        ? new Date(r.primera_compra).toISOString().substring(0, 10)
        : '',
      'Última compra': r.ultima_compra
        ? new Date(r.ultima_compra).toISOString().substring(0, 10)
        : '',
      Vinculado: r.vinculado ? 'Sí' : 'No',
    }));
    const ws = XLSX.utils.json_to_sheet(data);
    XLSX.utils.book_append_sheet(wb, ws, 'Marcas compradas');
    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }

  async exportarMarcasCompradasPDF(
    empresa_id: string,
    proveedor_id: string,
    params: { desde?: string; hasta?: string },
  ): Promise<Buffer> {
    const base = await this.buildEmpresaProveedorPayload(empresa_id, proveedor_id);
    const rows = await this.marcasCompradas(empresa_id, proveedor_id, params);
    const totalMonto = rows.reduce((acc: number, r: any) => acc + Number(r.monto_total || 0), 0);
    return this.callMsvKudeTabla({
      tipo: 'view',
      titulo: 'Marcas compradas',
      ...base,
      desde: params.desde ?? null,
      hasta: params.hasta ?? null,
      generado_en: new Date(),
      moneda: 'PYG',
      columns: [
        { key: 'marca', label: 'Marca', weight: 2.4 },
        { key: 'cantidad_compras', label: 'Compras', type: 'number', weight: 0.8 },
        { key: 'cantidad_productos', label: 'Productos', type: 'number', weight: 0.9 },
        { key: 'unidades', label: 'Unidades', type: 'number', weight: 0.9 },
        { key: 'monto_total', label: 'Monto Total', type: 'currency', weight: 1.4 },
        { key: 'ultima_compra', label: 'Última', type: 'date', weight: 0.9 },
        { key: 'vinculado', label: 'Vinc.', weight: 0.6 },
      ],
      rows: rows.map((r: any) => ({ ...r, vinculado: r.vinculado ? 'Sí' : 'No' })),
      totales: [{ label: 'Total compras', value: totalMonto, type: 'currency' }],
    });
  }

  async exportarVentasPorMarcaExcel(
    empresa_id: string,
    proveedor_id: string,
    params: {
      desde?: string;
      hasta?: string;
      modo?: 'vinculo' | 'lote';
      soloPreferente?: boolean;
    },
  ): Promise<Buffer> {
    const { rows } = await this.ventasPorMarca(empresa_id, proveedor_id, params);
    const wb = XLSX.utils.book_new();
    const data = rows.map((r: any) => ({
      Marca: r.marca,
      Código: r.producto_codigo ?? '',
      Producto: r.producto_descripcion ?? '',
      Facturas: r.cantidad_facturas,
      Unidades: r.unidades,
      'Monto Vendido': r.monto_vendido,
      Costo: r.costo_total,
      Preferente: r.es_proveedor_preferente ? 'Sí' : '',
      Distribuidor: r.es_distribuidor_oficial ? 'Sí' : '',
    }));
    const ws = XLSX.utils.json_to_sheet(data);
    XLSX.utils.book_append_sheet(wb, ws, 'Ventas por marca');
    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }

  async exportarVentasPorMarcaPDF(
    empresa_id: string,
    proveedor_id: string,
    params: {
      desde?: string;
      hasta?: string;
      modo?: 'vinculo' | 'lote';
      soloPreferente?: boolean;
    },
  ): Promise<Buffer> {
    const base = await this.buildEmpresaProveedorPayload(empresa_id, proveedor_id);
    const { rows, modo } = await this.ventasPorMarca(empresa_id, proveedor_id, params);
    const totalMonto = rows.reduce((acc: number, r: any) => acc + Number(r.monto_vendido || 0), 0);
    return this.callMsvKudeTabla({
      tipo: 'view',
      titulo: `Ventas por marca (${modo === 'lote' ? 'por lote' : 'por vínculo'})`,
      ...base,
      desde: params.desde ?? null,
      hasta: params.hasta ?? null,
      generado_en: new Date(),
      moneda: 'PYG',
      columns: [
        { key: 'marca', label: 'Marca', weight: 1.5 },
        { key: 'producto_codigo', label: 'Código', weight: 1.0 },
        { key: 'producto_descripcion', label: 'Producto', weight: 3.2 },
        { key: 'cantidad_facturas', label: 'Facturas', type: 'number', weight: 0.8 },
        { key: 'unidades', label: 'Unidades', type: 'number', weight: 0.9 },
        { key: 'monto_vendido', label: 'Monto Vendido', type: 'currency', weight: 1.5 },
      ],
      rows,
      totales: [
        { label: 'Total vendido', value: totalMonto, type: 'currency' },
      ],
    });
  }

  /**
   * Envía el reporte de vencidos al proveedor por email.
   */
  async enviarVencidosEmail(
    empresa_id: string,
    proveedor_id: string,
    params: { desde?: string; hasta?: string; deposito_id?: string; emailOverride?: string },
  ) {
    const proveedor = await this.getProveedorOrFail(proveedor_id, empresa_id);
    const empresa = await this.prisma.empresas.findUnique({ where: { id: empresa_id } });

    const destino = params.emailOverride ?? proveedor.personas?.email;
    if (!destino) {
      throw new BadRequestException(
        'El proveedor no tiene email registrado. Indique uno o configure el email del proveedor.',
      );
    }

    const rows = await this.vencidos(empresa_id, proveedor_id, params);
    if (rows.length === 0) {
      throw new BadRequestException('No hay vencidos en el rango indicado');
    }

    const excelBuffer = await this.exportarVencidosExcel(empresa_id, proveedor_id, params);
    const fileName = `vencidos-${proveedor.personas?.razon_social ?? 'proveedor'}-${new Date()
      .toISOString()
      .substring(0, 10)}.xlsx`;

    const totalCosto = rows.reduce((acc, r) => acc + (r.costo_total ?? 0), 0);
    const empresaNombre = (empresa as any)?.nombre_fantasia ?? (empresa as any)?.razon_social ?? 'Empresa';

    await this.mail.sendProveedorVencidosReport({
      to: destino,
      empresaNombre,
      proveedorNombre: proveedor.personas?.razon_social ?? 'Proveedor',
      totalLotes: rows.length,
      totalCosto: totalCosto.toLocaleString('es-PY'),
      excelBuffer,
      fileName,
    });

    this.logger.log(
      `Reporte vencidos enviado a ${destino} (proveedor=${proveedor_id}, empresa=${empresa_id}, lotes=${rows.length})`,
    );

    return {
      message: 'Reporte enviado',
      destino,
      lotes: rows.length,
      total_costo: totalCosto,
    };
  }
}
