import { BadGatewayException, BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Decimal } from '@prisma/client/runtime/library';
import { envs } from '../config/envs';
import { formatDateResponse, parseFechaValida } from 'src/utils/utilidades';
import { AuditService } from '../audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import { EstadoFactura, EstadoSifen } from '../facturas/facturas.enums';

@Injectable()
export class AsignacionFacturasService {
  private readonly logger = new Logger(AsignacionFacturasService.name);
  private readonly kudeUrl = envs.apiGeneradorPDF;

  constructor(
    private prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  private async callMsvKude(endpoint: string, payload: Record<string, unknown>): Promise<Buffer> {
    const url = `${this.kudeUrl}/api/contabilidad/${endpoint}`;
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...payload, tipo: 'view' }),
      });
      if (!response.ok) {
        const err = await response.text();
        this.logger.error(`msv-kude ${url} → ${response.status}: ${err}`);
        throw new BadGatewayException('Error generando PDF en msv-kude');
      }
      const buf = await response.arrayBuffer();
      return Buffer.from(buf);
    } catch (e) {
      if (e instanceof BadGatewayException) throw e;
      throw new BadGatewayException(`No se pudo conectar a msv-kude: ${e instanceof Error ? e.message : 'unknown'}`);
    }
  }

  private async getEmpresaHeader(empresaId: string) {
    const e = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { razon_social: true, nombre_fantasia: true, ruc: true, logo: true },
    });
    // nombre_fantasia: el PDF lo pone arriba y la razón social debajo (mismo criterio que la factura).
    return {
      razon_social: e?.razon_social ?? '',
      nombre_fantasia: e?.nombre_fantasia ?? null,
      ruc: e?.ruc ?? '',
      logo_url: e?.logo ?? null,
    };
  }

  private formatPeriodo(desde?: Date, hasta?: Date) {
    const f = (d: Date) => d.toLocaleDateString('es-PY', { day: '2-digit', month: '2-digit', year: 'numeric' });
    if (desde && hasta) return `${f(desde)} — ${f(hasta)}`;
    if (desde) return `Desde ${f(desde)}`;
    if (hasta) return `Hasta ${f(hasta)}`;
    return 'Todos los períodos';
  }

  async comisionesResumenPdf(empresaId: string, fechaDesde?: Date, fechaHasta?: Date, monedaId?: string): Promise<Buffer> {
    const empresa = await this.getEmpresaHeader(empresaId);
    const resumen = await this.getResumenComisiones(empresaId, fechaDesde, fechaHasta, monedaId);
    const moneda = resumen[0]?.moneda?.codigo || 'PYG';

    const filas = resumen.map((r) => ({
      nombre: `${r.nombre ?? ''} ${r.apellido ?? ''}`.trim(),
      tipo: r.tipo,
      ventas: r.ventas,
      cobranzas: r.cobranzas,
      porcentaje: r.comision_porcentaje,
      comision_generada: r.comision_generada,
      estado: r.estado,
    }));

    const totales = filas.reduce(
      (acc, f) => ({
        ventas: acc.ventas + Number(f.ventas || 0),
        cobranzas: acc.cobranzas + Number(f.cobranzas || 0),
        comisiones: acc.comisiones + Number(f.comision_generada || 0),
      }),
      { ventas: 0, cobranzas: 0, comisiones: 0 },
    );

    return this.callMsvKude('comisiones-resumen', {
      empresa,
      titulo: 'Resumen de Comisiones',
      periodo: this.formatPeriodo(fechaDesde, fechaHasta),
      moneda,
      filas,
      totales,
    });
  }

  async comisionesDetallePdf(empresaId: string, fechaDesde?: Date, fechaHasta?: Date, monedaId?: string): Promise<Buffer> {
    const empresa = await this.getEmpresaHeader(empresaId);
    const resumen = await this.getResumenComisiones(empresaId, fechaDesde, fechaHasta, monedaId);
    const moneda = resumen[0]?.moneda?.codigo || 'PYG';

    const grupos: any[] = [];
    let totalBase = 0;
    let totalComision = 0;

    for (const r of resumen) {
      const detalle = await this.getComisionesDetalle(empresaId, r.id, fechaDesde, fechaHasta, monedaId);
      const items = detalle.map((c: any) => {
        const esVenta = c.tipo === 'venta';
        const docCab = esVenta ? c.factura_cab : c.recibos_cobro;
        const numero = esVenta
          ? `${docCab?.dest ?? ''}-${docCab?.dpunexp ?? ''}-${docCab?.dnumdoc ?? ''}`
          : (docCab?.numero_recibo ?? '');
        const fecha = esVenta ? docCab?.dfeemide : docCab?.fecha_emision;
        const cliente = docCab?.clientes?.personas?.razon_social
          ?? docCab?.clientes?.nombre_fantasia
          ?? '';
        return {
          fecha,
          numero_documento: numero,
          cliente,
          tipo_origen: esVenta ? 'Venta' : 'Cobranza',
          monto_base: Number(c.monto_base ?? 0),
          comision: Number(c.monto_comision ?? 0),
        };
      });

      const subtotalBase = items.reduce((s, it) => s + it.monto_base, 0);
      const subtotalCom = items.reduce((s, it) => s + it.comision, 0);
      totalBase += subtotalBase;
      totalComision += subtotalCom;

      grupos.push({
        nombre: `${r.nombre ?? ''} ${r.apellido ?? ''}`.trim(),
        tipo: r.tipo,
        porcentaje: r.comision_porcentaje,
        estado: r.estado,
        items,
        total_base: subtotalBase,
        total_comision: subtotalCom,
      });
    }

    return this.callMsvKude('comisiones-detalle', {
      empresa,
      titulo: 'Detalle de Comisiones',
      periodo: this.formatPeriodo(fechaDesde, fechaHasta),
      moneda,
      grupos,
      totales: { base: totalBase, comision: totalComision },
    });
  }

  private async tieneModuloComisiones(empresaId: string): Promise<boolean> {
    // El módulo se considera activo si:
    //   a) El submódulo COB_COMISIONES está en suscripcion_submodulos (nueva vía).
    //   b) El módulo COMISIONES está en suscripcion_modulos (vía legacy que sigue
    //      vigente para empresas con planes viejos donde el submódulo no fue
    //      poblado individualmente). Sin este fallback, en producción se rompía
    //      generación de comisiones para empresas con plan full que muestran
    //      "Comisiones" incluido en la UI pero sin suscripcion_submodulos.
    const [subActivo, modActivo] = await Promise.all([
      this.prisma.suscripcion_submodulos.findFirst({
        where: {
          active: true,
          submodulos: { codigo: 'COB_COMISIONES' },
          suscripciones: { empresa_id: empresaId },
        },
        select: { id: true },
      }),
      this.prisma.suscripcion_modulos.findFirst({
        where: {
          activo: true,
          modulos: { codigo: 'COMISIONES' },
          suscripciones: { empresa_id: empresaId },
        },
        select: { id: true },
      }),
    ]);
    return !!subActivo || !!modActivo;
  }

  async getFacturasPendientes(
    empresaId: string,
    filters: {
      cobrador_id?: string;
      vendedor_id?: string;
      cliente_id?: string;
      moneda_id?: string;
      sin_asignar?: boolean;
      asignadas?: boolean;
      search?: string;
      fechaDesde?: string;
      fechaHasta?: string;
      page?: number;
      limit?: number;
    },
  ) {
    const page = Math.max(1, filters.page || 1);
    const limit = Math.min(200, filters.limit || 100);
    const skip = (page - 1) * limit;

    const where: any = {
      empresa_id: empresaId,
      estado: { in: ['Pendiente', 'Enviado', 'Aprobado', 'Parcial'] },
    };

    if (filters.moneda_id) where.moneda_id = filters.moneda_id;
    if (filters.cliente_id) where.cliente_id = filters.cliente_id;

    if (filters.fechaDesde || filters.fechaHasta) {
      const gte = parseFechaValida(filters.fechaDesde);
      const lte = parseFechaValida(filters.fechaHasta);
      if (gte || lte) {
        where.dfeemide = {};
        if (gte) where.dfeemide.gte = gte;
        if (lte) {
          if (/^\d{4}-\d{2}-\d{2}$/.test(filters.fechaHasta)) lte.setUTCHours(23, 59, 59, 999);
          where.dfeemide.lte = lte;
        }
      }
    }

    // Filtros de búsqueda en SQL
    if (filters.search) {
      const term = filters.search.trim();
      where.OR = [
        { dnumdoc: { contains: term, mode: 'insensitive' } },
        { clientes: { personas: { razon_social: { contains: term, mode: 'insensitive' } } } },
        { clientes: { personas: { ruc: { contains: term, mode: 'insensitive' } } } },
      ];
    }

    // Filtros de asignación en SQL — combinados con AND para que no se pisen.
    // Cada `some` evalúa AL MENOS una asignación de la factura contra el filtro.
    // Bug histórico: cada `if` reasignaba where.asignacion_facturas con `=`, así el
    // último ganaba (ej. asignadas + vendedor_id => se perdía `asignadas`).
    const asignacionFilters: Array<Record<string, unknown>> = [];
    if (filters.sin_asignar) {
      where.asignacion_facturas = {
        none: { OR: [{ cobrador_id: { not: null } }, { vendedor_id: { not: null } }] },
      };
    } else {
      if (filters.asignadas) {
        asignacionFilters.push({
          OR: [{ cobrador_id: { not: null } }, { vendedor_id: { not: null } }],
        });
      }
      if (filters.cobrador_id) {
        asignacionFilters.push({ cobrador_id: filters.cobrador_id });
      }
      if (filters.vendedor_id) {
        asignacionFilters.push({ vendedor_id: filters.vendedor_id });
      }
      if (asignacionFilters.length > 0) {
        where.asignacion_facturas = { some: { AND: asignacionFilters } };
      }
    }

    const [facturas, total] = await Promise.all([
      this.prisma.factura_cab.findMany({
        where,
        select: {
          id: true,
          dest: true,
          dpunexp: true,
          dnumdoc: true,
          dfeemide: true,
          moneda: { select: { id: true, codigo: true, simbolo: true, descripcion: true } },
          clientes: { select: { id: true, personas: { select: { razon_social: true, ruc: true } } } },
          asignacion_facturas: {
            select: {
              id: true,
              cobrador_id: true,
              vendedor_id: true,
              cobrador: { select: { id: true, nombre: true, apellido: true } },
              vendedor: { select: { id: true, nombre: true, apellido: true } },
            },
          },
          factura_subtotales: { select: { dtotope: true }, take: 1 },
          cuentas_cobrar: { select: { saldo_pendiente: true }, take: 1 },
        },
        orderBy: { dfeemide: 'desc' },
        take: limit,
        skip,
      }),
      this.prisma.factura_cab.count({ where }),
    ]);

    const data = facturas.map((f) => {
      const totalFactura = f.factura_subtotales?.[0]?.dtotope ? Number(f.factura_subtotales[0].dtotope) : 0;
      const saldoPendiente = f.cuentas_cobrar?.[0]?.saldo_pendiente
        ? Number(f.cuentas_cobrar[0].saldo_pendiente)
        : totalFactura;
      return {
        id: f.id,
        numero_factura: `${f.dest}-${f.dpunexp}-${f.dnumdoc}`,
        fecha_emision: formatDateResponse(f.dfeemide),
        total: totalFactura,
        saldo_pendiente: saldoPendiente > 0 ? saldoPendiente : 0,
        moneda: f.moneda ?? { id: null, codigo: 'PYG', simbolo: '₲', descripcion: 'Guaraní' },
        cliente: { id: f.clientes?.id, nombre: f.clientes?.personas?.razon_social, ruc: f.clientes?.personas?.ruc },
        cobrador_asignado: f.asignacion_facturas?.find((a) => a.cobrador_id)?.cobrador ?? null,
        vendedor: f.asignacion_facturas?.find((a) => a.vendedor_id)?.vendedor ?? null,
        asignacion_id: f.asignacion_facturas?.[0]?.id ?? null,
      };
    });

    return { data, total, page, limit, pages: Math.ceil(total / limit) };
  }

  async asignarCobrador(empresaId: string, facturaId: string, cobradorId: string, usuarioId: string, notas?: string) {
    const factura = await this.prisma.factura_cab.findFirst({ where: { id: facturaId, empresa_id: empresaId } });
    if (!factura) throw new NotFoundException('Factura no encontrada');
    const cobrador = await this.prisma.vendedores_cobradores.findFirst({
      where: { id: cobradorId, empresa_id: empresaId, active: true },
    });
    if (!cobrador) throw new NotFoundException('Cobrador no encontrado');
    const existente = await this.prisma.asignacion_facturas.findFirst({ where: { factura_id: facturaId } });
    const [asignacion] = await this.prisma.$transaction([
      existente
        ? this.prisma.asignacion_facturas.update({
            where: { id: existente.id },
            data: { cobrador_id: cobradorId, fecha_asignacion: new Date(), asignado_por: usuarioId, notas },
            include: { cobrador: true, factura_cab: true },
          })
        : this.prisma.asignacion_facturas.create({
            data: {
              empresa_id: empresaId,
              factura_id: facturaId,
              cobrador_id: cobradorId,
              fecha_asignacion: new Date(),
              asignado_por: usuarioId,
              estado: 'pendiente',
              notas,
            },
            include: { cobrador: true, factura_cab: true },
          }),
      this.prisma.factura_cab.update({
        where: { id: facturaId },
        data: { cobrador_id: cobradorId },
      }),
    ]);
    return asignacion;
  }

  async asignarCobradorMasivo(
    empresaId: string,
    facturaIds: string[],
    cobradorId: string,
    usuarioId: string,
    notas?: string,
  ) {
    const cobrador = await this.prisma.vendedores_cobradores.findFirst({
      where: { id: cobradorId, empresa_id: empresaId, active: true },
    });
    if (!cobrador) throw new NotFoundException('Cobrador no encontrado');

    const facturas = await this.prisma.factura_cab.findMany({
      where: { id: { in: facturaIds }, empresa_id: empresaId },
      select: { id: true },
    });
    if (facturas.length !== facturaIds.length) throw new BadRequestException('Algunas facturas no fueron encontradas');

    // Obtener asignaciones existentes en un solo query
    const existentes = await this.prisma.asignacion_facturas.findMany({
      where: { factura_id: { in: facturaIds } },
      select: { id: true, factura_id: true },
    });
    const existentesMap = new Map(existentes.map((e) => [e.factura_id, e.id]));

    const toUpdate = facturaIds.filter((id) => existentesMap.has(id));
    const toCreate = facturaIds.filter((id) => !existentesMap.has(id));

    await this.prisma.$transaction([
      ...toUpdate.map((facturaId) =>
        this.prisma.asignacion_facturas.update({
          where: { id: existentesMap.get(facturaId)! },
          data: { cobrador_id: cobradorId, fecha_asignacion: new Date(), asignado_por: usuarioId, notas },
        }),
      ),
      this.prisma.asignacion_facturas.createMany({
        data: toCreate.map((facturaId) => ({
          empresa_id: empresaId,
          factura_id: facturaId,
          cobrador_id: cobradorId,
          fecha_asignacion: new Date(),
          asignado_por: usuarioId,
          estado: 'pendiente',
          notas,
        })),
        skipDuplicates: true,
      }),
      this.prisma.factura_cab.updateMany({
        where: { id: { in: facturaIds }, empresa_id: empresaId },
        data: { cobrador_id: cobradorId },
      }),
    ]);

    return { asignadas: facturaIds.length, cobrador: { id: cobrador.id, nombre: cobrador.nombre } };
  }

  async getAsignacionesCobrador(empresaId: string, cobradorId: string, estado?: string) {
    const where: any = { empresa_id: empresaId, cobrador_id: cobradorId };
    if (estado) where.estado = estado;
    return this.prisma.asignacion_facturas.findMany({
      where,
      include: {
        factura_cab: {
          include: { clientes: { include: { personas: true } }, factura_subtotales: true, cuentas_cobrar: true },
        },
      },
      orderBy: { fecha_asignacion: 'desc' },
    });
  }

  async updateEstadoAsignacion(empresaId: string, asignacionId: string, estado: string, notas?: string) {
    const asignacion = await this.prisma.asignacion_facturas.findFirst({
      where: { id: asignacionId, empresa_id: empresaId },
    });
    if (!asignacion) throw new NotFoundException('Asignacion no encontrada');
    return this.prisma.asignacion_facturas.update({
      where: { id: asignacionId },
      data: { estado, notas: notas ? (asignacion.notas || '') + '\n' + notas : asignacion.notas },
    });
  }

  async getResumenCobradores(empresaId: string) {
    // Un solo query: todas las asignaciones pendientes con el monto de la factura
    const asignaciones = await this.prisma.asignacion_facturas.findMany({
      where: { empresa_id: empresaId, estado: 'pendiente', cobrador_id: { not: null } },
      select: {
        cobrador_id: true,
        cobrador: { select: { id: true, nombre: true, apellido: true, codigo: true } },
        factura_cab: {
          select: {
            cuentas_cobrar: { select: { saldo_pendiente: true }, take: 1 },
            factura_subtotales: { select: { dtotope: true }, take: 1 },
          },
        },
      },
    });

    // Agrupar en memoria (mucho más rápido que N+1 queries)
    const map = new Map<string, { cobrador: any; count: number; monto: number }>();
    for (const a of asignaciones) {
      if (!a.cobrador_id || !a.cobrador) continue;
      const saldo = a.factura_cab?.cuentas_cobrar?.[0]?.saldo_pendiente;
      const total = a.factura_cab?.factura_subtotales?.[0]?.dtotope;
      const monto = saldo ? Number(saldo) : total ? Number(total) : 0;
      const entry = map.get(a.cobrador_id);
      if (entry) {
        entry.count++;
        entry.monto += monto;
      } else {
        map.set(a.cobrador_id, { cobrador: a.cobrador, count: 1, monto });
      }
    }

    return Array.from(map.values()).map(({ cobrador, count, monto }) => ({
      id: cobrador.id,
      nombre: cobrador.nombre,
      apellido: cobrador.apellido,
      codigo: cobrador.codigo,
      facturas_asignadas: count,
      monto_total_pendiente: monto,
    }));
  }

  async asignarVendedor(empresaId: string, facturaId: string, vendedorId: string, usuarioId: string) {
    const factura = await this.prisma.factura_cab.findFirst({
      where: { id: facturaId, empresa_id: empresaId },
      include: { factura_subtotales: true },
    });
    if (!factura) throw new NotFoundException('Factura no encontrada');
    const vendedor = await this.prisma.vendedores_cobradores.findFirst({
      where: { id: vendedorId, empresa_id: empresaId, tipo: { in: ['vendedor', 'ambos'] }, active: true },
    });
    if (!vendedor) throw new NotFoundException('Vendedor no encontrado');

    const existente = await this.prisma.asignacion_facturas.findFirst({ where: { factura_id: facturaId } });
    let asignacion;
    if (existente) {
      asignacion = await this.prisma.asignacion_facturas.update({
        where: { id: existente.id },
        data: { vendedor_id: vendedorId },
        include: { vendedor: true },
      });
    } else {
      asignacion = await this.prisma.asignacion_facturas.create({
        data: {
          empresa_id: empresaId,
          factura_id: facturaId,
          vendedor_id: vendedorId,
          asignado_por: usuarioId,
          estado: 'pendiente',
        },
        include: { vendedor: true },
      });
    }
    await this.prisma.factura_cab.update({
      where: { id: facturaId },
      data: { vendedor_id: vendedorId },
    });

    if (!(await this.tieneModuloComisiones(empresaId))) return asignacion;

    const comisionExistente = await this.prisma.comisiones.findFirst({
      where: { factura_id: facturaId, vendedor_cobrador_id: vendedorId, tipo: 'venta' },
    });

    const porcentajeVendedor = vendedor.comision_venta
      ? Number(vendedor.comision_venta)
      : vendedor.comision_porcentaje
        ? Number(vendedor.comision_porcentaje)
        : 0;
    const montoFijoVendedor = vendedor.comision_fija ? Number(vendedor.comision_fija) : 0;

    if (!comisionExistente && (porcentajeVendedor > 0 || montoFijoVendedor > 0)) {
      const montoBase = factura.factura_subtotales?.[0]?.dtotope ? Number(factura.factura_subtotales[0].dtotope) : 0;
      const montoComision = (montoBase * porcentajeVendedor) / 100 + montoFijoVendedor;
      if (montoComision > 0) {
        await this.prisma.comisiones.create({
          data: {
            empresa_id: empresaId,
            vendedor_cobrador_id: vendedorId,
            factura_id: facturaId,
            tipo: 'venta',
            monto_base: new Decimal(montoBase),
            porcentaje_aplicado: porcentajeVendedor > 0 ? new Decimal(porcentajeVendedor) : null,
            monto_fijo_aplicado: montoFijoVendedor > 0 ? new Decimal(montoFijoVendedor) : null,
            monto_comision: new Decimal(montoComision),
            estado: 'pendiente',
          },
        });
      }
    }

    return asignacion;
  }

  async generarComisionesFacturasExistentes(empresaId: string) {
    if (!(await this.tieneModuloComisiones(empresaId))) {
      return { mensaje: 'Módulo COMISIONES no activo', comisionesGeneradas: 0, detalles: [] };
    }
    // Fuente de verdad: factura_cab.vendedor_id (el vendedor que EMITIÓ la venta).
    // Antes iterábamos asignacion_facturas — pero esa tabla es del módulo de
    // cobranza (puede tener cobrador_id sin vendedor_id, o venir NULL cuando la
    // factura fue emitida directo sin pasar por Asignación). Las comisiones de
    // VENTA tienen que basarse en quién emitió la factura, no en cobranza.
    const facturas = await this.prisma.factura_cab.findMany({
      where: {
        empresa_id: empresaId,
        vendedor_id: { not: null },
        estado: { notIn: [EstadoFactura.ANULADA, 'Anulada'] },
        OR: [
          { estado_sifen: null },
          { estado_sifen: { not: EstadoSifen.CANCELADO } },
        ],
      },
      include: { factura_subtotales: true, vendedor: true },
    });
    let comisionesGeneradas = 0;
    const detalles: string[] = [];

    for (const factura of facturas) {
      if (!factura.vendedor_id || !factura.vendedor) {
        detalles.push(`Factura ${factura.dnumdoc || factura.id}: sin vendedor`);
        continue;
      }
      // Comisión existente en cualquier estado ≠ cancelada/anulada (idempotencia).
      const comisionExistente = await this.prisma.comisiones.findFirst({
        where: {
          factura_id: factura.id,
          vendedor_cobrador_id: factura.vendedor_id,
          tipo: 'venta',
          estado: { notIn: ['cancelada', 'anulada'] },
        },
      });
      if (comisionExistente) {
        detalles.push(`Factura ${factura.dnumdoc || factura.id}: ya tiene comisión (${comisionExistente.estado})`);
        continue;
      }

      // Fix del bug de Decimal(0) truthy: usar Number(x ?? 0) > 0 como guard,
      // no el ternario `x ? Number(x) : ...` que evaluaba objetos Decimal como
      // truthy aunque el valor fuera 0.
      const vendedor = factura.vendedor;
      const pctVenta = Number(vendedor.comision_venta ?? 0);
      const pctGenerico = Number(vendedor.comision_porcentaje ?? 0);
      const porcentaje = pctVenta > 0 ? pctVenta : pctGenerico;
      const montoFijo = Number(vendedor.comision_fija ?? 0);
      if (porcentaje === 0 && montoFijo === 0) {
        detalles.push(`Factura ${factura.dnumdoc || factura.id}: vendedor sin % ni monto fijo cargado`);
        continue;
      }

      const montoBase = factura.factura_subtotales?.[0]?.dtotope
        ? Number(factura.factura_subtotales[0].dtotope) : 0;
      if (montoBase <= 0) {
        detalles.push(`Factura ${factura.dnumdoc || factura.id}: falta factura_subtotales.dtotope`);
        continue;
      }

      const montoComision = (montoBase * porcentaje) / 100 + montoFijo;
      if (montoComision > 0) {
        await this.prisma.comisiones.create({
          data: {
            empresa_id: empresaId,
            vendedor_cobrador_id: factura.vendedor_id,
            factura_id: factura.id,
            tipo: 'venta',
            monto_base: new Decimal(montoBase),
            porcentaje_aplicado: porcentaje > 0 ? new Decimal(porcentaje) : null,
            monto_fijo_aplicado: montoFijo > 0 ? new Decimal(montoFijo) : null,
            monto_comision: new Decimal(montoComision),
            estado: 'pendiente',
          },
        });
        comisionesGeneradas++;
      }
    }
    return { mensaje: `Se generaron ${comisionesGeneradas} comisiones`, comisionesGeneradas, detalles };
  }

  async getResumenComisiones(empresaId: string, fechaDesde?: Date, fechaHasta?: Date, monedaId?: string) {
    // Fuente de verdad: tabla `comisiones`. El resumen suma las comisiones realmente generadas,
    // pero el filtro temporal se aplica sobre la **fecha del documento origen**:
    //   - venta:    factura_cab.dfeemide
    //   - cobranza: recibos_cobro.fecha_emision
    // De esta forma coincide con el panel de Supervisores (que también usa fechas de emisión)
    // y no depende de cuándo se corrió la generación de comisiones (created_at).
    const whereComision: any = {
      empresa_id: empresaId,
      // Excluir comisiones canceladas/anuladas para que el resumen no cuente
      // ventas cuya factura origen fue anulada (o recibos anulados).
      estado: { notIn: ['cancelada', 'anulada'] },
      OR: [
        // Ventas filtradas por fecha de emisión de la factura + factura no anulada
        {
          tipo: 'venta',
          factura_cab: {
            // 'Anulada' se mantiene por defensivo (registros legacy previos al
            // alineamiento del enum a 'Anulado').
            // Nota: NO filtramos por estado_sifen='Rechazado' porque puede ser
            // un estado transitorio (ej: rechazo previo al reenvío exitoso).
            // Solo excluimos 'Cancelado' que sí es terminal.
            estado: { notIn: [EstadoFactura.ANULADA, 'Anulada'] },
            OR: [
              { estado_sifen: null },
              { estado_sifen: { not: EstadoSifen.CANCELADO } },
            ],
            ...(fechaDesde || fechaHasta
              ? { dfeemide: { ...(fechaDesde ? { gte: fechaDesde } : {}), ...(fechaHasta ? { lte: fechaHasta } : {}) } }
              : {}),
          },
        },
        // Cobranzas filtradas por fecha de emisión del recibo + recibo no anulado
        {
          tipo: 'cobranza',
          recibos_cobro: {
            estado: { not: 'anulado' },
            ...(fechaDesde || fechaHasta
              ? { fecha_emision: { ...(fechaDesde ? { gte: fechaDesde } : {}), ...(fechaHasta ? { lte: fechaHasta } : {}) } }
              : {}),
          },
        },
      ],
    };

    const comisiones = await this.prisma.comisiones.findMany({
      where: whereComision,
      include: {
        factura_cab: { select: { moneda_id: true, moneda: true, dfeemide: true } },
        recibos_cobro: { select: { moneda_id: true, moneda: true, fecha_emision: true } },
        vendedor_cobrador: { select: { id: true, nombre: true, apellido: true, tipo: true, comision_porcentaje: true, comision_venta: true, comision_cobranza: true, comision_fija: true } },
      },
    });

    let monedaInfo: { id: string; codigo: string; simbolo: string; descripcion: string } | null = null;
    if (monedaId) {
      const moneda = await this.prisma.moneda.findUnique({ where: { id: monedaId } });
      if (moneda) monedaInfo = { id: moneda.id, codigo: moneda.codigo || 'PYG', simbolo: moneda.simbolo || '₲', descripcion: moneda.descripcion || 'Guaraní' };
    }

    const monedaDeComision = (c: typeof comisiones[number]) =>
      c.tipo === 'venta' ? c.factura_cab?.moneda_id : c.recibos_cobro?.moneda_id ?? c.factura_cab?.moneda_id;

    const filtradas = monedaId ? comisiones.filter((c) => monedaDeComision(c) === monedaId) : comisiones;

    const grupos = new Map<string, { ventas: number; cobranzas: number; comision_generada: number; vc: any }>();
    for (const c of filtradas) {
      let g = grupos.get(c.vendedor_cobrador_id);
      if (!g) {
        g = { ventas: 0, cobranzas: 0, comision_generada: 0, vc: c.vendedor_cobrador };
        grupos.set(c.vendedor_cobrador_id, g);
      }
      const base = Number(c.monto_base ?? 0);
      const com = Number(c.monto_comision ?? 0);
      if (c.tipo === 'venta') g.ventas += base;
      else if (c.tipo === 'cobranza') g.cobranzas += base;
      g.comision_generada += com;

      if (!monedaInfo) {
        const mon = c.tipo === 'venta' ? c.factura_cab?.moneda : c.recibos_cobro?.moneda ?? c.factura_cab?.moneda;
        if (mon) monedaInfo = { id: mon.id, codigo: mon.codigo || 'PYG', simbolo: mon.simbolo || '₲', descripcion: mon.descripcion || 'Guaraní' };
      }
    }

    const resumen: any[] = [];
    for (const [vcId, g] of grupos) {
      const vc = g.vc;
      const pctVenta = vc?.comision_venta ? Number(vc.comision_venta) : vc?.comision_porcentaje ? Number(vc.comision_porcentaje) : 0;
      const pctCobranza = vc?.comision_cobranza ? Number(vc.comision_cobranza) : vc?.comision_porcentaje ? Number(vc.comision_porcentaje) : 0;
      const comisionFija = vc?.comision_fija ? Number(vc.comision_fija) : 0;
      resumen.push({
        id: vcId, nombre: vc?.nombre, apellido: vc?.apellido, tipo: vc?.tipo,
        comision_porcentaje: pctVenta || pctCobranza, comision_fija: comisionFija,
        ventas: g.ventas, cobranzas: g.cobranzas, comision_generada: g.comision_generada,
        estado: 'pendiente',
        moneda: monedaInfo || { id: null, codigo: 'PYG', simbolo: '₲', descripcion: 'Guaraní' },
      });
    }
    return resumen;
  }

  async cancelarComisionesPorFactura(facturaId: string, empresaId: string): Promise<void> {
    await this.prisma.comisiones.updateMany({
      where: { factura_id: facturaId, empresa_id: empresaId, estado: { in: ['pendiente', 'aprobada'] } },
      data: { estado: 'cancelada', observaciones: 'Cancelada automáticamente por anulación de factura', updated_at: new Date() },
    });
  }

  async cancelarComisionesPorRecibo(reciboId: string, empresaId: string): Promise<void> {
    await this.prisma.comisiones.updateMany({
      where: { recibo_id: reciboId, empresa_id: empresaId, estado: { in: ['pendiente', 'aprobada'] } },
      data: { estado: 'cancelada', observaciones: 'Cancelada automáticamente por anulación de recibo', updated_at: new Date() },
    });
  }

  /**
   * Recalcula el monto_base y monto_comision de las comisiones de venta asociadas
   * a una factura cuando cambia `factura_cab.total_notas_credito` (aplicación o
   * reversión de NC). Solo toca comisiones en estado 'pendiente' o 'aprobada'
   * (nunca liquidada/pagada, para no romper cierres previos).
   *
   * - Si el neto (bruto - total_nc) queda <= 0 → la comisión se cancela.
   * - Si estaba cancelada por NC previa (observaciones marca eso) y ahora hay
   *   base positiva → se reactiva a 'pendiente' con los nuevos montos.
   */
  async recalcularComisionesPorNC(facturaId: string, empresaId: string): Promise<void> {
    // Si la empresa no tiene contratado el submódulo COB_COMISIONES, no
    // hay comisiones que mantener — evita queries innecesarias.
    if (!(await this.tieneModuloComisiones(empresaId))) return;

    const factura = await this.prisma.factura_cab.findFirst({
      where: { id: facturaId, empresa_id: empresaId },
      include: { factura_subtotales: true },
    });
    if (!factura) return;

    const bruto = factura.factura_subtotales?.[0]?.dtotope
      ? Number(factura.factura_subtotales[0].dtotope)
      : 0;
    const totalNc = Number((factura as any).total_notas_credito ?? 0);
    const nuevaBase = Math.max(0, bruto - totalNc);

    const comisiones = await this.prisma.comisiones.findMany({
      where: {
        factura_id: facturaId,
        empresa_id: empresaId,
        tipo: 'venta',
        estado: { in: ['pendiente', 'aprobada', 'cancelada'] },
      },
    });

    for (const c of comisiones) {
      const canceladaPorNC =
        c.estado === 'cancelada' &&
        (c.observaciones ?? '').toLowerCase().includes('nota de crédito');
      if (c.estado === 'cancelada' && !canceladaPorNC) continue;

      const pct = c.porcentaje_aplicado ? Number(c.porcentaje_aplicado) : 0;
      const fijo = c.monto_fijo_aplicado ? Number(c.monto_fijo_aplicado) : 0;

      if (nuevaBase <= 0) {
        if (c.estado !== 'cancelada') {
          await this.prisma.comisiones.update({
            where: { id: c.id },
            data: {
              estado: 'cancelada',
              monto_base: new Decimal(0),
              monto_comision: new Decimal(0),
              observaciones:
                'Cancelada automáticamente: nota de crédito consumió el total de la factura',
              updated_at: new Date(),
            },
          });
        }
        continue;
      }

      const nuevaComision = (nuevaBase * pct) / 100 + fijo;
      await this.prisma.comisiones.update({
        where: { id: c.id },
        data: {
          estado: canceladaPorNC ? 'pendiente' : c.estado ?? 'pendiente',
          monto_base: new Decimal(nuevaBase),
          monto_comision: new Decimal(nuevaComision),
          observaciones: canceladaPorNC
            ? 'Reactivada: reversión de nota de crédito'
            : `Recalculada por nota de crédito aplicada (NC: ${totalNc})`,
          updated_at: new Date(),
        },
      });
    }

    // Caso borde: no había comisión (bruto original fue 0 o error) y ahora hay base.
    // No hay `vendedor_id` seguro para crearla acá → confiar en flujo de generación
    // manual. Log informativo si detectamos esto.
    if (comisiones.length === 0 && nuevaBase > 0 && factura.vendedor_id) {
      this.logger.log(
        `Factura ${facturaId} sin comisión pero con base ${nuevaBase} tras NC — considerar generarla`,
        'AsignacionFacturasService',
      );
    }
  }

  async generarComisionVentaPorFactura(facturaId: string, vendedorId: string, empresaId: string): Promise<void> {
    if (!(await this.tieneModuloComisiones(empresaId))) return;
    const factura = await this.prisma.factura_cab.findFirst({ where: { id: facturaId, empresa_id: empresaId }, include: { factura_subtotales: true } });
    if (!factura) return;
    const vendedor = await this.prisma.vendedores_cobradores.findFirst({ where: { id: vendedorId, empresa_id: empresaId, active: true } });
    if (!vendedor) return;
    const existe = await this.prisma.comisiones.findFirst({ where: { factura_id: facturaId, vendedor_cobrador_id: vendedorId, tipo: 'venta' } });
    if (existe) return;
    const pctVenta = vendedor.comision_venta ? Number(vendedor.comision_venta) : vendedor.comision_porcentaje ? Number(vendedor.comision_porcentaje) : 0;
    const montoFijo = vendedor.comision_fija ? Number(vendedor.comision_fija) : 0;
    if (pctVenta === 0 && montoFijo === 0) return;
    const montoBruto = factura.factura_subtotales?.[0]?.dtotope ? Number(factura.factura_subtotales[0].dtotope) : 0;
    if (montoBruto <= 0) return;
    const totalNc = Number((factura as any).total_notas_credito ?? 0);
    const montoBase = montoBruto - totalNc;
    if (montoBase <= 0) return;
    const montoComision = (montoBase * pctVenta) / 100 + montoFijo;
    if (montoComision <= 0) return;
    await this.prisma.comisiones.create({
      data: {
        empresa_id: empresaId, vendedor_cobrador_id: vendedorId, factura_id: facturaId,
        tipo: 'venta', monto_base: new Decimal(montoBase),
        porcentaje_aplicado: pctVenta > 0 ? new Decimal(pctVenta) : null,
        monto_fijo_aplicado: montoFijo > 0 ? new Decimal(montoFijo) : null,
        monto_comision: new Decimal(montoComision), estado: 'pendiente',
      },
    });
  }

  async getComisionesDetalle(
    empresaId: string,
    vendedorCobradorId: string,
    fechaDesde?: Date,
    fechaHasta?: Date,
    monedaId?: string,
  ) {
    const where: any = {
      empresa_id: empresaId,
      vendedor_cobrador_id: vendedorCobradorId,
      // Excluir comisiones canceladas/anuladas (por ejemplo, cuando la factura
      // origen fue anulada, o cuando el recibo asociado fue anulado).
      estado: { notIn: ['cancelada', 'anulada'] },
      // Excluir comisiones cuya factura origen esté anulada o cuyo estado SIFEN
      // sea Cancelado/Rechazado. Esta condición se aplica solo a las de tipo
      // venta; las de cobranza (recibos_cobro) no tienen factura_cab directo.
      OR: [
        { tipo: { not: 'venta' } },
        {
          factura_cab: {
            // 'Anulada' se mantiene por defensivo (registros legacy previos al
            // alineamiento del enum a 'Anulado').
            // Nota: NO filtramos por estado_sifen='Rechazado' porque puede ser
            // un estado transitorio (ej: rechazo previo al reenvío exitoso).
            // Solo excluimos 'Cancelado' que sí es terminal.
            estado: { notIn: [EstadoFactura.ANULADA, 'Anulada'] },
            OR: [
              { estado_sifen: null },
              { estado_sifen: { not: EstadoSifen.CANCELADO } },
            ],
          },
        },
      ],
    };
    if (fechaDesde || fechaHasta) {
      where.created_at = {};
      if (fechaDesde) where.created_at.gte = fechaDesde;
      if (fechaHasta) where.created_at.lte = fechaHasta;
    }
    const comisiones = await this.prisma.comisiones.findMany({
      where,
      orderBy: { created_at: 'desc' },
      include: {
        factura_cab: {
          select: {
            id: true, dnumdoc: true, dest: true, dpunexp: true, dfeemide: true, moneda_id: true,
            vendedor_id: true, total_notas_credito: true, total_factura: true, saldo_disponible: true,
            moneda: { select: { id: true, codigo: true, simbolo: true } },
            clientes: { select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true, ruc: true, nro_documento: true } } } },
            cliente_direccion: {
              select: {
                id: true,
                label: true,
                supervisor_id: true,
                supervisor: { select: { id: true, nombre: true, apellido: true } },
              },
            },
          },
        },
        recibos_cobro: {
          select: {
            id: true, numero_recibo: true, fecha_emision: true, moneda_id: true,
            moneda: { select: { id: true, codigo: true, simbolo: true } },
            clientes: { select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true, ruc: true, nro_documento: true } } } },
          },
        },
      },
    });

    const monedaDe = (c: typeof comisiones[number]) =>
      c.tipo === 'venta' ? c.factura_cab?.moneda_id : c.recibos_cobro?.moneda_id ?? c.factura_cab?.moneda_id;

    // Fallback supervisor: para cada fila se intenta resolver el supervisor por el vendedor de la factura
    // (factura_cab.vendedor_id) y, como respaldo final, por el vendedor de la comisión.
    const vendedorIds = new Set<string>([vendedorCobradorId]);
    for (const c of comisiones) {
      if (c.factura_cab?.vendedor_id) vendedorIds.add(c.factura_cab.vendedor_id);
      if (c.vendedor_cobrador_id) vendedorIds.add(c.vendedor_cobrador_id);
    }
    const equipoLinks = await this.prisma.supervisor_vendedores_cobradores.findMany({
      where: { vendedor_cobrador_id: { in: Array.from(vendedorIds) } },
      include: { supervisor: { select: { id: true, nombre: true, apellido: true } } },
    });
    const supervisorPorVendedor = new Map<string, { id: string; nombre: string; apellido: string | null }>();
    for (const link of equipoLinks) {
      if (link.supervisor) supervisorPorVendedor.set(link.vendedor_cobrador_id, link.supervisor);
    }

    const enriched = comisiones.map((c) => {
      const supFactura = c.factura_cab?.vendedor_id
        ? supervisorPorVendedor.get(c.factura_cab.vendedor_id)
        : undefined;
      const supComision = supervisorPorVendedor.get(c.vendedor_cobrador_id);
      const sup = supFactura ?? supComision ?? null;
      // Clonar por fila: evita "[Circular]" en el serializador cuando el mismo objeto
      // se referencia desde varias filas del array.
      const supClone = sup ? { id: sup.id, nombre: sup.nombre, apellido: sup.apellido } : null;
      return { ...c, supervisor_equipo: supClone };
    });

    return monedaId ? enriched.filter((c) => monedaDe(c) === monedaId) : enriched;
  }

  async generarComisionCobranzaPorRecibo(
    reciboId: string,
    empresaId: string,
  ): Promise<{ created: boolean; reason?: string; comision_id?: string; monto_comision?: number }> {
    if (!(await this.tieneModuloComisiones(empresaId))) {
      return { created: false, reason: 'modulo_comisiones_inactivo' };
    }
    const recibo = await this.prisma.recibos_cobro.findUnique({ where: { id: reciboId }, select: { id: true, cobrador_id: true, monto_total: true } });
    if (!recibo?.cobrador_id) return { created: false, reason: 'recibo_sin_cobrador' };
    if (!recibo.monto_total) return { created: false, reason: 'recibo_sin_monto' };
    const cobrador = await this.prisma.vendedores_cobradores.findFirst({ where: { id: recibo.cobrador_id, empresa_id: empresaId, active: true } });
    if (!cobrador) return { created: false, reason: 'cobrador_inactivo_o_no_encontrado' };
    const existe = await this.prisma.comisiones.findFirst({ where: { recibo_id: reciboId, vendedor_cobrador_id: recibo.cobrador_id } });
    if (existe) return { created: false, reason: 'comision_ya_existente', comision_id: existe.id };
    const pctCobranza = cobrador.comision_cobranza ? Number(cobrador.comision_cobranza) : cobrador.comision_porcentaje ? Number(cobrador.comision_porcentaje) : 0;
    const montoFijo = cobrador.comision_fija ? Number(cobrador.comision_fija) : 0;
    if (pctCobranza === 0 && montoFijo === 0) return { created: false, reason: 'cobrador_sin_porcentaje_ni_monto_fijo' };
    const montoBase = Number(recibo.monto_total);
    const montoComision = (montoBase * pctCobranza) / 100 + montoFijo;
    if (montoComision <= 0) return { created: false, reason: 'monto_comision_calculado_cero' };
    const comision = await this.prisma.comisiones.create({
      data: {
        empresa_id: empresaId, vendedor_cobrador_id: recibo.cobrador_id, recibo_id: reciboId,
        tipo: 'cobranza', monto_base: new Decimal(montoBase),
        porcentaje_aplicado: pctCobranza > 0 ? new Decimal(pctCobranza) : null,
        monto_fijo_aplicado: montoFijo > 0 ? new Decimal(montoFijo) : null,
        monto_comision: new Decimal(montoComision), estado: 'pendiente',
      },
    });
    return { created: true, comision_id: comision.id, monto_comision: montoComision };
  }

  async eliminarComision(
    empresaId: string,
    comisionId: string,
    userId?: string,
    meta?: { ip?: string; userAgent?: string },
  ) {
    const comision = await this.prisma.comisiones.findFirst({
      where: { id: comisionId, empresa_id: empresaId },
    });
    if (!comision) throw new NotFoundException('Comisión no encontrada');
    if (comision.estado === 'liquidada' || comision.liquidacion_id) {
      throw new BadRequestException(
        'No se puede eliminar una comisión liquidada. Anulá la liquidación primero.',
      );
    }
    await this.prisma.comisiones.delete({ where: { id: comisionId } });

    await this.auditService.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'DELETE',
      entity_type: 'comision',
      entity_id: comisionId,
      descripcion: `Comisión ${comision.tipo} eliminada (estado: ${comision.estado ?? 'pendiente'})`,
      old_value: {
        vendedor_cobrador_id: comision.vendedor_cobrador_id,
        factura_id: comision.factura_id,
        recibo_id: comision.recibo_id,
        tipo: comision.tipo,
        monto_base: comision.monto_base?.toString(),
        monto_comision: comision.monto_comision?.toString(),
        estado: comision.estado,
      },
      ip_address: meta?.ip,
      user_agent: meta?.userAgent,
    });

    return { ok: true };
  }
}
