import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { FacturasService } from '../facturas/facturas.service';
import { CreateFacturaDto } from '../facturas/dto/create-factura.dto';
import { PrismaService } from '../prisma/prisma.service';
import { ConfigMoraService } from './config-mora.service';
import {
  RegistrarInteresCobradoDto,
  UpsertConfigInteresesDto,
} from './dto/intereses-moratorios.dto';

export interface InteresCalculo {
  factura_cuota_id: string;
  dias_mora: number;
  tasa_aplicada: number;
  base_calculo: number;
  monto_interes_bruto: number;
  monto_exonerado: number;
  monto_interes_neto: number;
  iva_porcentaje: number;
  monto_iva: number;
  total_a_cobrar: number;
  // política
  pausado: boolean;
  motivo_pausa?: string;
  tipo_comprobante: 'FACTURA' | 'NOTA_DEBITO' | 'NINGUNO';
}

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly configMoraService: ConfigMoraService,
    private readonly facturasService: FacturasService,
  ) {}

  async getConfig(empresa_id: string) {
    const existing = await this.prisma.cob_config_intereses.findUnique({
      where: { empresa_id },
    });
    if (existing) return existing;
    return this.prisma.cob_config_intereses.create({
      data: { empresa_id },
    });
  }

  async upsertConfig(empresa_id: string, dto: UpsertConfigInteresesDto) {
    if (dto.tipo_comprobante === 'FACTURA') {
      if (!dto.producto_servicio_id) {
        throw new BadRequestException(
          'tipo_comprobante=FACTURA requiere producto_servicio_id',
        );
      }
      if (!dto.numeracion_id) {
        throw new BadRequestException(
          'tipo_comprobante=FACTURA requiere numeracion_id (timbrado / punto de expedición que emitirá la factura de intereses)',
        );
      }
      // Validar que la numeración pertenezca a la empresa y esté activa
      const numeracion = await this.prisma.numeraciones_documento.findUnique({
        where: { id: dto.numeracion_id },
        select: { id: true, empresa_id: true, active: true },
      });
      if (!numeracion || numeracion.empresa_id !== empresa_id) {
        throw new BadRequestException(
          'La numeración no existe o no pertenece a la empresa',
        );
      }
      if (!numeracion.active) {
        throw new BadRequestException('La numeración seleccionada está inactiva');
      }
    }
    return this.prisma.cob_config_intereses.upsert({
      where: { empresa_id },
      create: {
        empresa_id,
        tipo_comprobante: dto.tipo_comprobante,
        iva_porcentaje: new Prisma.Decimal(dto.iva_porcentaje),
        iva_modo: dto.iva_modo,
        producto_servicio_id: dto.producto_servicio_id ?? null,
        numeracion_id: dto.numeracion_id ?? null,
        pausar_en_demanda: dto.pausar_en_demanda,
        pausar_en_incobrable: dto.pausar_en_incobrable,
        imputacion_orden: dto.imputacion_orden,
      },
      update: {
        tipo_comprobante: dto.tipo_comprobante,
        iva_porcentaje: new Prisma.Decimal(dto.iva_porcentaje),
        iva_modo: dto.iva_modo,
        producto_servicio_id: dto.producto_servicio_id ?? null,
        numeracion_id: dto.numeracion_id ?? null,
        pausar_en_demanda: dto.pausar_en_demanda,
        pausar_en_incobrable: dto.pausar_en_incobrable,
        imputacion_orden: dto.imputacion_orden,
        updated_at: new Date(),
      },
    });
  }

  /**
   * Calcula el interés moratorio para una cuota.
   * Considera políticas de pausa (gestión en DEMANDA/INCOBRABLE).
   */
  async previewParaCuota(
    empresa_id: string,
    factura_cuota_id: string,
    monto_exonerado = 0,
  ): Promise<InteresCalculo> {
    const cuota = await this.prisma.factura_cuotas.findUnique({
      where: { id: factura_cuota_id },
      include: {
        factura_cab: { select: { empresa_id: true, cliente_id: true } },
      },
    });
    if (!cuota) throw new NotFoundException('Cuota no encontrada');
    if (cuota.factura_cab.empresa_id !== empresa_id) {
      throw new NotFoundException('Cuota no pertenece a la empresa');
    }

    const [configMora, configIntereses] = await Promise.all([
      this.configMoraService.getOrCreate(empresa_id),
      this.getConfig(empresa_id),
    ]);

    const pausa = await this.evaluarPausaGestion(
      empresa_id,
      cuota.factura_cab.cliente_id,
      configIntereses.pausar_en_demanda,
      configIntereses.pausar_en_incobrable,
    );

    const saldo = Number(cuota.saldo_pendiente ?? cuota.dmoncuota);
    const montoCuota = Number(cuota.dmoncuota);

    const bruto = pausa.pausado
      ? 0
      : this.configMoraService.calcularMora(
          saldo,
          montoCuota,
          new Date(cuota.dvenccuo),
          {
            activo: configMora.activo,
            tipo_calculo: configMora.tipo_calculo,
            tasa: Number(configMora.tasa),
            base_calculo: configMora.base_calculo,
            periodo_gracia: configMora.periodo_gracia,
            monto_minimo_mora: Number(configMora.monto_minimo_mora),
            monto_maximo_mora: configMora.monto_maximo_mora
              ? Number(configMora.monto_maximo_mora)
              : null,
          },
        );

    // Modo IVA configurado por empresa (decisión contractual):
    //   INCLUIDO  → el monto cobrado al cliente YA contiene IVA. La factura
    //               desglosa neto + IVA por dentro. Cliente paga el bruto.
    //   POR_FUERA → la tasa de mora es neta. El IVA se suma encima y el
    //               cliente paga base + IVA.
    const exonerado = Math.max(0, Math.min(monto_exonerado, bruto));
    const montoTrasExo = bruto - exonerado;
    const ivaPct = Number(configIntereses.iva_porcentaje);
    const ivaModo = (configIntereses as any).iva_modo === 'POR_FUERA' ? 'POR_FUERA' : 'INCLUIDO';
    let neto: number;
    let iva: number;
    let total: number;
    if (ivaModo === 'POR_FUERA') {
      neto = montoTrasExo;
      iva = ivaPct > 0 ? Math.round((neto * ivaPct) / 100) : 0;
      total = neto + iva;
    } else {
      total = montoTrasExo;
      iva = ivaPct > 0 ? Math.round((total * ivaPct) / (100 + ivaPct)) : 0;
      neto = total - iva;
    }

    const diasMora = this.diasVencido(cuota.dvenccuo);

    return {
      factura_cuota_id,
      dias_mora: diasMora,
      tasa_aplicada: Number(configMora.tasa),
      base_calculo:
        configMora.base_calculo === 'cuota_completa' ? montoCuota : saldo,
      monto_interes_bruto: bruto,
      monto_exonerado: exonerado,
      monto_interes_neto: neto,
      iva_porcentaje: ivaPct,
      monto_iva: iva,
      total_a_cobrar: total,
      pausado: pausa.pausado,
      motivo_pausa: pausa.motivo,
      tipo_comprobante:
        configIntereses.tipo_comprobante as InteresCalculo['tipo_comprobante'],
    };
  }

  /**
   * Persiste un cobro de interés. Esperado: llamarse dentro de la TX de cobros.
   * El factura_cab_id (comprobante emitido) se setea desde el caller en chunk 3.
   */
  async registrarCobro(
    tx: Prisma.TransactionClient,
    empresa_id: string,
    user_id: string | null,
    calc: InteresCalculo,
    dto: Pick<
      RegistrarInteresCobradoDto,
      'recibo_cobro_id' | 'autorizacion_descuento_id'
    >,
    factura_cab_id: string | null,
  ) {
    return tx.cob_interes_cobrado.create({
      data: {
        empresa_id,
        factura_cuota_id: calc.factura_cuota_id,
        recibo_cobro_id: dto.recibo_cobro_id,
        dias_mora: calc.dias_mora,
        tasa_aplicada: new Prisma.Decimal(calc.tasa_aplicada),
        base_calculo: new Prisma.Decimal(calc.base_calculo),
        monto_interes_bruto: new Prisma.Decimal(calc.monto_interes_bruto),
        monto_exonerado: new Prisma.Decimal(calc.monto_exonerado),
        monto_interes_cobrado: new Prisma.Decimal(calc.monto_interes_neto),
        iva_porcentaje: new Prisma.Decimal(calc.iva_porcentaje),
        monto_iva: new Prisma.Decimal(calc.monto_iva),
        tipo_comprobante: calc.tipo_comprobante,
        factura_cab_id,
        autorizacion_descuento_id: dto.autorizacion_descuento_id ?? null,
        usuario_id: user_id,
      },
    });
  }

  async listarPorRecibo(empresa_id: string, recibo_cobro_id: string) {
    return this.prisma.cob_interes_cobrado.findMany({
      where: { empresa_id, recibo_cobro_id },
      orderBy: { created_at: 'asc' },
    });
  }

  /**
   * Fase A.3.8 — Job retry: devuelve los recibos cuya emisión SIFEN quedó
   * pendiente (factura_cab_id=null + error_emision presente).
   * Devuelve agrupado por recibo_cobro_id porque la emisión se hace por recibo.
   * `maxIntentos` previene loops infinitos sobre errores permanentes.
   */
  async listarRecibosConEmisionPendiente(maxIntentos = 5, limit = 50) {
    const filas = await this.prisma.cob_interes_cobrado.findMany({
      where: {
        factura_cab_id: null,
        error_emision: { not: null },
        intento_emision: { lt: maxIntentos },
        tipo_comprobante: 'FACTURA',
      },
      select: { empresa_id: true, recibo_cobro_id: true },
      distinct: ['recibo_cobro_id'],
      take: limit,
      orderBy: { created_at: 'asc' },
    });
    return filas;
  }

  /**
   * Fase A.3.6 — emisión post-TX de la factura electrónica por intereses moratorios
   * cobrados en un recibo. Una línea (factura_det) por cuota cobrada con mora, para
   * preservar la trazabilidad ante disputas del cliente: cada línea referencia su
   * cuota origen, días de mora, tasa aplicada y exoneración aplicada (si corresponde).
   *
   * Idempotente por recibo+cuota: solo procesa filas de cob_interes_cobrado con
   * tipo_comprobante='FACTURA' que aún no tengan factura_cab_id.
   * Errores se persisten en error_emision/intento_emision para reintento por job.
   */
  async emitirFacturaParaRecibo(
    empresa_id: string,
    recibo_cobro_id: string,
    user_id: string | null,
  ): Promise<{ factura_cab_id?: string; emitidos: number; saltados: number; error?: string }> {
    const pendientes = await this.prisma.cob_interes_cobrado.findMany({
      where: {
        empresa_id,
        recibo_cobro_id,
        factura_cab_id: null,
        tipo_comprobante: 'FACTURA',
        monto_interes_cobrado: { gt: 0 },
      },
      orderBy: { created_at: 'asc' },
    });
    if (pendientes.length === 0) return { emitidos: 0, saltados: 0 };

    try {
      const recibo = await this.prisma.recibos_cobro.findUnique({
        where: { id: recibo_cobro_id },
        select: { id: true, cliente_id: true, empresa_id: true, numero_recibo: true },
      });
      if (!recibo || recibo.empresa_id !== empresa_id) {
        throw new Error('Recibo no encontrado o no pertenece a la empresa');
      }

      const config = await this.getConfig(empresa_id);
      if (config.tipo_comprobante !== 'FACTURA') {
        return { emitidos: 0, saltados: pendientes.length };
      }
      if (!config.producto_servicio_id) {
        throw new Error('cob_config_intereses.producto_servicio_id no configurado');
      }
      if (!config.numeracion_id) {
        throw new Error(
          'cob_config_intereses.numeracion_id no configurado — el supervisor debe elegir qué timbrado/punto de expedición emite las facturas de intereses',
        );
      }

      // Maestros requeridos por el DTO de FacturasService.create()
      const [moneda, indicador, tipoImpuesto, tipoTransaccion, condicion, medioPago, productoServicio] =
        await Promise.all([
          this.prisma.moneda.findFirst({ where: { codigo: 'PYG' } }),
          this.prisma.indicador_presencia.findFirst({ where: { codigo: 1 } }),
          this.prisma.tipo_impuesto.findFirst({ where: { codigo: 1 } }),
          this.prisma.tipo_transaccion.findFirst({ where: { codigo: 1 } }), // 1 = Venta de mercadería/servicio
          this.prisma.condicion_operacion.findFirst({ where: { codigo: 1 } }), // 1 = Contado
          this.prisma.medio_pago.findFirst({ where: { codigo: 1 } }),         // 1 = Efectivo
          this.prisma.productos.findUnique({ where: { id: config.producto_servicio_id } }),
        ]);
      if (!moneda || !indicador || !tipoImpuesto || !tipoTransaccion || !condicion || !medioPago) {
        throw new Error('Faltan parámetros maestros (moneda/indicador/tipos/condicion/medio_pago)');
      }
      if (!productoServicio) {
        throw new Error('Producto servicio de intereses no existe');
      }

      // Cuotas + factura origen para descripción autoexplicativa por línea
      const cuotaIds = pendientes.map((p) => p.factura_cuota_id);
      const cuotas = await this.prisma.factura_cuotas.findMany({
        where: { id: { in: cuotaIds } },
        select: {
          id: true,
          nro_cuota: true,
          dvenccuo: true,
          factura_cab: { select: { dest: true, dpunexp: true, dnumdoc: true } },
        },
      });
      const cuotaMap = new Map(cuotas.map((c) => [c.id, c]));

      const ivaPct = Number(config.iva_porcentaje);
      // IVA incluido: duniproser = neto + iva ; dliqivaitem = iva ; dbasgraviva = neto
      const lineasComputadas = pendientes.map((row) => {
        const neto = Number(row.monto_interes_cobrado);
        const iva = Number(row.monto_iva);
        const total = neto + iva;
        const cuota = cuotaMap.get(row.factura_cuota_id);
        const numFactOrigen = cuota?.factura_cab
          ? `${cuota.factura_cab.dest}-${cuota.factura_cab.dpunexp}-${cuota.factura_cab.dnumdoc}`
          : 'fact. origen N/D';
        const desc =
          `Interés mora cuota N°${cuota?.nro_cuota ?? '?'} fact. ${numFactOrigen}` +
          ` — ${row.dias_mora} días @ ${Number(row.tasa_aplicada)}%` +
          (Number(row.monto_exonerado) > 0
            ? ` (exonerado Gs ${Number(row.monto_exonerado).toLocaleString('es-PY')})`
            : '');
        return { row, neto, iva, total, descripcion: desc };
      });

      const totalNeto = lineasComputadas.reduce((acc, l) => acc + l.neto, 0);
      const totalIva = lineasComputadas.reduce((acc, l) => acc + l.iva, 0);
      const totalFactura = totalNeto + totalIva;
      const baseGrav10 = ivaPct === 10 ? totalNeto : 0;
      const baseGrav5 = ivaPct === 5 ? totalNeto : 0;
      const liqIva10 = ivaPct === 10 ? totalIva : 0;
      const liqIva5 = ivaPct === 5 ? totalIva : 0;
      const subExe = ivaPct === 0 ? totalNeto : 0;

      const fechaIsoSinTz = new Date().toISOString().slice(0, 19).replace('T', ' ');

      const dto: CreateFacturaDto = {
        cabecera: {
          empresa_id,
          cliente_id: recibo.cliente_id,
          tipo_transaccion_id: tipoTransaccion.id,
          tipo_impuesto_id: tipoImpuesto.id,
          moneda_id: moneda.id,
          indicador_presencia_id: indicador.id,
          condicion_operacion_id: condicion.id,
          numeracion_id: config.numeracion_id, // ← timbrado/punto expedición elegido por el supervisor
          dfeemide: fechaIsoSinTz,
          dinfadic: `Factura por intereses moratorios — Recibo ${recibo.numero_recibo}`.slice(0, 5000),
        } as any,
        items: lineasComputadas.map((l) => ({
          producto_id: productoServicio.id,
          unidad_medida: 'UNI',
          descripcion: l.descripcion.slice(0, 2000),
          cantidad: 1,
          precio_unitario: l.total,
          total_bruto: l.total,
          descuento: 0,
          porcentaje_descuento: 0,
          descuento_global: 0,
          total_operacion: l.total,
          total_gs: l.total,
          // SET: 1=Gravado, 2=Exonerado (por decreto), 3=Exento (no alcanzado), 4=Gravado parcial
          // Intereses sin IVA configurado → Exento (3). 'Exonerado' (2) sólo aplica
          // cuando hay un decreto específico que exonera el servicio.
          afectacion_iva: ivaPct === 0 ? 3 : 1,
          porcentaje_iva: ivaPct,
          proporcion_iva: 100,
          base_gravada: ivaPct === 0 ? 0 : l.neto,
          liquidacion_iva: l.iva,
          base_gravada_exenta: ivaPct === 0 ? l.neto : 0,
        })),
        pagos: [
          {
            medio_pago_id: medioPago.id,
            moneda_id: moneda.id,
            monto_pago: totalFactura,
          } as any,
        ],
        subtotal: {
          subtotal_exento: subExe,
          subtotal_exonerado: 0,
          subtotal_5: baseGrav5 + liqIva5,
          subtotal_10: baseGrav10 + liqIva10,
          liquidacion_iva_5: liqIva5,
          liquidacion_iva_10: liqIva10,
          liquidacion_redondeo_5: 0,
          liquidacion_redondeo_10: 0,
          liquidacion_iva_comision: 0,
          total_iva: totalIva,
          total_operacion: totalFactura,
          total_descuento: 0,
          total_descuento_global_item: 0,
          total_descuento_particular_item: 0,
          total_bruto_operacion: totalFactura,
          total_anticipo_item: 0,
          total_anticipo_global_item: 0,
          porcentaje_descuento_total: 0,
          total_anticipo_operacion: 0,
          total_redondeo: 0,
          total_comision: 0,
          total_general_operacion: totalFactura,
          total_base_gravada_5: baseGrav5,
          total_base_gravada_10: baseGrav10,
          total_base_gravada: baseGrav5 + baseGrav10,
          total_gs: totalFactura,
        } as any,
      };

      // FacturasService.create() reserva el número con SELECT FOR UPDATE,
      // genera CDC, valida cliente, persiste y encola SIFEN según config sucursal.
      const result = await this.facturasService.create(
        dto,
        empresa_id,
        user_id ?? undefined,
      );
      const facturaCabId: string | undefined = (result as any)?.data?.id;
      if (!facturaCabId) {
        throw new Error('FacturasService.create() no devolvió id de factura');
      }

      // Mapear cada factura_det creado con su cob_interes_cobrado por orden de
      // inserción: pasamos `items` en el mismo orden que `lineasComputadas`, y
      // facturas.service.ts persiste los det en ese mismo orden. Esto evita tener
      // que poner un marker en dinfitem (que se imprime en la factura).
      const dets = await this.prisma.factura_det.findMany({
        where: { factura_cab_id: facturaCabId },
        select: { id: true },
        orderBy: { created_at: 'asc' },
      });
      const ahora = new Date();
      for (let i = 0; i < lineasComputadas.length; i++) {
        const l = lineasComputadas[i];
        const det = dets[i];
        await this.prisma.cob_interes_cobrado.update({
          where: { id: l.row.id },
          data: {
            factura_cab_id: facturaCabId,
            factura_det_id: det?.id ?? null,
            emitido_at: ahora,
            intento_emision: { increment: 1 },
            error_emision: null,
          },
        });
      }

      this.logger.log(
        `Factura intereses ${facturaCabId} emitida (${pendientes.length} líneas) recibo=${recibo_cobro_id}`,
        'InteresesMoratoriosService',
      );
      return {
        factura_cab_id: facturaCabId,
        emitidos: pendientes.length,
        saltados: 0,
      };
    } catch (err) {
      // BadRequestException de FacturasService trae detalle en getResponse().errores.
      // Sin esto, error_emision solo guardaba el título y perdíamos el motivo real
      // (p.ej. lista de campos faltantes del cliente).
      let msg = err instanceof Error ? err.message : String(err);
      const resp =
        err && typeof (err as any).getResponse === 'function'
          ? (err as any).getResponse()
          : null;
      if (resp && typeof resp === 'object') {
        const baseMsg = (resp as any).message ?? msg;
        const errores = (resp as any).errores;
        if (Array.isArray(errores) && errores.length > 0) {
          const detalle = errores
            .map((e: any) =>
              typeof e === 'string' ? e : e?.mensaje ?? JSON.stringify(e),
            )
            .join(' | ');
          msg = `${baseMsg} ${detalle}`;
        } else {
          msg = baseMsg;
        }
      }
      await this.prisma.cob_interes_cobrado.updateMany({
        where: { id: { in: pendientes.map((p) => p.id) } },
        data: {
          intento_emision: { increment: 1 },
          error_emision: msg.slice(0, 1000),
        },
      });
      this.logger.error(
        `Emisión de factura de intereses falló recibo=${recibo_cobro_id}: ${msg}`,
        'InteresesMoratoriosService',
      );
      return { emitidos: 0, saltados: pendientes.length, error: msg };
    }
  }

  /**
   * Desglose para soporte/disputas: dado un factura_cab_id emitido por intereses,
   * devuelve la traza completa por línea (factura origen, cuota, días, tasa,
   * exoneración con autorización).
   */
  async desgloseFacturaIntereses(empresa_id: string, factura_cab_id: string) {
    const factura = await this.prisma.factura_cab.findFirst({
      where: { id: factura_cab_id, empresa_id },
      select: {
        id: true,
        dest: true,
        dpunexp: true,
        dnumdoc: true,
        dfeemide: true,
        cliente_id: true,
        total_factura: true,
      },
    });
    if (!factura) throw new NotFoundException('Factura no encontrada');

    const filas = await this.prisma.cob_interes_cobrado.findMany({
      where: { empresa_id, factura_cab_id },
      orderBy: { created_at: 'asc' },
    });
    if (filas.length === 0) {
      throw new NotFoundException(
        'La factura no corresponde a una emisión de intereses moratorios',
      );
    }

    const cuotaIds = filas.map((f) => f.factura_cuota_id);
    const autorizIds = filas
      .map((f) => f.autorizacion_descuento_id)
      .filter((v): v is string => !!v);

    const [cuotas, autorizaciones] = await Promise.all([
      this.prisma.factura_cuotas.findMany({
        where: { id: { in: cuotaIds } },
        select: {
          id: true,
          nro_cuota: true,
          dvenccuo: true,
          dmoncuota: true,
          factura_cab: {
            select: { id: true, dest: true, dpunexp: true, dnumdoc: true, cliente_id: true },
          },
        },
      }),
      autorizIds.length > 0
        ? this.prisma.autorizaciones_descuento.findMany({
            where: { id: { in: autorizIds } },
            select: { id: true, motivo: true, aprobado_por: true, created_at: true },
          })
        : Promise.resolve([] as any[]),
    ]);
    const cuotaMap = new Map(cuotas.map((c) => [c.id, c]));
    const autorizMap = new Map(autorizaciones.map((a: any) => [a.id, a]));

    const lineas = filas.map((f) => {
      const c = cuotaMap.get(f.factura_cuota_id);
      const factOrigen = c?.factura_cab
        ? `${c.factura_cab.dest}-${c.factura_cab.dpunexp}-${c.factura_cab.dnumdoc}`
        : null;
      return {
        cob_interes_cobrado_id: f.id,
        factura_det_id: f.factura_det_id,
        cuota: {
          id: f.factura_cuota_id,
          nro: c?.nro_cuota ?? null,
          vencimiento: c?.dvenccuo ?? null,
          monto_cuota: c?.dmoncuota ?? null,
          factura_origen_id: c?.factura_cab?.id ?? null,
          factura_origen_numero: factOrigen,
        },
        dias_mora: f.dias_mora,
        tasa_aplicada: Number(f.tasa_aplicada),
        base_calculo: Number(f.base_calculo),
        monto_interes_bruto: Number(f.monto_interes_bruto),
        monto_exonerado: Number(f.monto_exonerado),
        monto_interes_cobrado: Number(f.monto_interes_cobrado),
        iva_porcentaje: Number(f.iva_porcentaje),
        monto_iva: Number(f.monto_iva),
        total_linea: Number(f.monto_interes_cobrado) + Number(f.monto_iva),
        autorizacion_exoneracion: f.autorizacion_descuento_id
          ? autorizMap.get(f.autorizacion_descuento_id) ?? null
          : null,
        recibo_cobro_id: f.recibo_cobro_id,
        emitido_at: f.emitido_at,
      };
    });

    return {
      factura: {
        id: factura.id,
        numero: `${factura.dest}-${factura.dpunexp}-${factura.dnumdoc}`,
        fecha: factura.dfeemide,
        cliente_id: factura.cliente_id,
        total: Number(factura.total_factura ?? 0),
      },
      lineas,
    };
  }

  // ──────────────── helpers ────────────────

  private diasVencido(fecha: Date | string): number {
    const d = new Date(fecha);
    if (Number.isNaN(d.getTime())) return 0;
    d.setHours(0, 0, 0, 0);
    const hoy = new Date();
    hoy.setHours(0, 0, 0, 0);
    return Math.max(
      0,
      Math.floor((hoy.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)),
    );
  }

  private async evaluarPausaGestion(
    empresa_id: string,
    cliente_id: string,
    pausar_demanda: boolean,
    pausar_incobrable: boolean,
  ): Promise<{ pausado: boolean; motivo?: string }> {
    if (!pausar_demanda && !pausar_incobrable) return { pausado: false };
    const estadosPausa: string[] = [];
    if (pausar_demanda) estadosPausa.push('DEMANDA');
    if (pausar_incobrable) estadosPausa.push('INCOBRABLE');

    const gestion = await this.prisma.cob_gestion_mora.findFirst({
      where: {
        empresa_id,
        cliente_id,
        deleted_at: null,
        estado_actual: { in: estadosPausa as any },
      },
      select: { estado_actual: true },
    });
    if (!gestion) return { pausado: false };
    return {
      pausado: true,
      motivo: `Cliente en estado ${gestion.estado_actual} — devengamiento pausado por configuración`,
    };
  }
}
