import {
  BadRequestException,
  ForbiddenException,
  HttpException,
  HttpStatus,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { CobMoraEstado, Prisma } from '@prisma/client';
import { AuditService } from '../audit/audit.service';
import { envs } from '../config/envs';
import { PrismaService } from '../prisma/prisma.service';
import {
  CreateGestionMoraDto,
  QueryGestionMoraDto,
  TransicionEstadoDto,
} from './dto/gestion-mora.dto';

/** Estados que cuentan como "gestión activa" (no cerrada). */
const ESTADOS_ACTIVOS: CobMoraEstado[] = [
  CobMoraEstado.GESTION_INTERNA,
  CobMoraEstado.INFORMCONF,
  CobMoraEstado.DEMANDA,
];

/** Estados que cuentan como "cerrada" — no se reabren automáticamente. */
const ESTADOS_CERRADOS: CobMoraEstado[] = [
  CobMoraEstado.RECUPERADA,
  CobMoraEstado.REFINANCIADA,
  CobMoraEstado.INCOBRABLE,
  CobMoraEstado.AL_DIA,
];

/** Días vencidos por defecto para abrir gestión automáticamente. */
const DIAS_UMBRAL_GESTION_DEFAULT = 30;

const TRANSICIONES: Record<CobMoraEstado, CobMoraEstado[]> = {
  AL_DIA: [CobMoraEstado.GESTION_INTERNA, CobMoraEstado.REFINANCIADA],
  GESTION_INTERNA: [
    CobMoraEstado.INFORMCONF,
    CobMoraEstado.DEMANDA,
    CobMoraEstado.RECUPERADA,
    CobMoraEstado.REFINANCIADA,
    CobMoraEstado.AL_DIA,
  ],
  INFORMCONF: [
    CobMoraEstado.DEMANDA,
    CobMoraEstado.RECUPERADA,
    CobMoraEstado.REFINANCIADA,
  ],
  DEMANDA: [
    CobMoraEstado.INCOBRABLE,
    CobMoraEstado.RECUPERADA,
    CobMoraEstado.REFINANCIADA,
  ],
  INCOBRABLE: [CobMoraEstado.RECUPERADA],
  RECUPERADA: [CobMoraEstado.AL_DIA, CobMoraEstado.GESTION_INTERNA],
  REFINANCIADA: [CobMoraEstado.AL_DIA, CobMoraEstado.GESTION_INTERNA],
};

const ESTADOS_REQUIEREN_DOCUMENTO: CobMoraEstado[] = [
  CobMoraEstado.DEMANDA,
  CobMoraEstado.INCOBRABLE,
];

const ESTADOS_DOBLE_AUTORIZACION: CobMoraEstado[] = [
  CobMoraEstado.DEMANDA,
  CobMoraEstado.INCOBRABLE,
];

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

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

  async create(dto: CreateGestionMoraDto, empresa_id: string, user_id: string) {
    if (!dto.factura_ids?.length) {
      throw new BadRequestException('Debe incluir al menos una factura afectada');
    }

    const cliente = await this.prisma.clientes.findFirst({
      where: { id: dto.cliente_id },
    });
    if (!cliente) throw new NotFoundException('Cliente no encontrado');

    const facturas = await this.prisma.factura_cab.findMany({
      where: { id: { in: dto.factura_ids }, empresa_id, cliente_id: dto.cliente_id },
      select: { id: true, total_factura: true, saldo_pendiente: true },
    });
    if (facturas.length !== dto.factura_ids.length) {
      throw new BadRequestException('Algunas facturas no existen o no pertenecen al cliente');
    }

    // Bloquear duplicados: facturas ya incluidas en una gestión activa del mismo cliente
    const facturasEnGestionActiva = await this.prisma.cob_gestion_mora_factura.findMany({
      where: {
        factura_cab_id: { in: dto.factura_ids },
        gestion_mora: {
          empresa_id,
          deleted_at: null,
          cliente_id: dto.cliente_id,
          estado_actual: { in: ESTADOS_ACTIVOS },
        },
      },
      select: {
        factura_cab: { select: { dest: true, dpunexp: true, dnumdoc: true } },
      },
    });
    if (facturasEnGestionActiva.length > 0) {
      const numeros = [
        ...new Set(
          facturasEnGestionActiva.map(
            (f) =>
              `${f.factura_cab.dest}-${f.factura_cab.dpunexp}-${f.factura_cab.dnumdoc}`,
          ),
        ),
      ];
      throw new BadRequestException(
        `Las siguientes facturas ya están en una gestión activa: ${numeros.join(', ')}. ` +
          `Cierre la gestión existente o use "Transicionar estado" antes de abrir una nueva.`,
      );
    }

    this.validarRequisitosEstado(dto.estado_inicial, dto.documento_url, null);

    const gestion = await this.prisma.$transaction(async (tx) => {
      const g = await tx.cob_gestion_mora.create({
        data: {
          empresa_id,
          cliente_id: dto.cliente_id,
          estado_actual: dto.estado_inicial,
          monto_involucrado: new Prisma.Decimal(dto.monto_involucrado),
          observacion: dto.observacion ?? null,
          documento_url: dto.documento_url ?? null,
          usuario_id: user_id,
        },
      });

      await tx.cob_gestion_mora_factura.createMany({
        data: facturas.map((f) => ({
          gestion_mora_id: g.id,
          factura_cab_id: f.id,
          saldo_snapshot: f.saldo_pendiente ?? new Prisma.Decimal(0),
        })),
      });

      await tx.cob_gestion_mora_historial.create({
        data: {
          gestion_mora_id: g.id,
          estado_anterior: null,
          estado_nuevo: dto.estado_inicial,
          motivo: dto.observacion ?? 'Apertura de gestión de mora',
          usuario_id: user_id,
          documento_url: dto.documento_url ?? null,
        },
      });

      return g;
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'CREATE',
      entity_type: 'cob_gestion_mora',
      entity_id: gestion.id,
      descripcion:
        `Gestión de mora abierta | Cliente: ${dto.cliente_id} | Estado: ${dto.estado_inicial}` +
        ` | Facturas: ${dto.factura_ids.length} | Monto: ${dto.monto_involucrado}`,
      new_value: {
        cliente_id: dto.cliente_id,
        estado_inicial: dto.estado_inicial,
        monto_involucrado: dto.monto_involucrado,
        factura_ids: dto.factura_ids,
        documento_url: dto.documento_url ?? null,
        observacion: dto.observacion ?? null,
      },
    });

    return gestion;
  }

  private buildWhereFromQuery(
    empresa_id: string,
    query: QueryGestionMoraDto,
  ): Prisma.cob_gestion_moraWhereInput {
    const fechaIngreso: Prisma.DateTimeFilter = {};
    if (query.desde) fechaIngreso.gte = new Date(`${query.desde}T00:00:00`);
    if (query.hasta) fechaIngreso.lte = new Date(`${query.hasta}T23:59:59.999`);

    const search = query.search?.trim();
    const clienteFilter: Prisma.clientesWhereInput | undefined = search
      ? {
          OR: [
            { nombre_fantasia: { contains: search, mode: 'insensitive' } },
            {
              personas: {
                OR: [
                  { razon_social: { contains: search, mode: 'insensitive' } },
                  { ruc: { contains: search, mode: 'insensitive' } },
                  { nro_documento: { contains: search, mode: 'insensitive' } },
                ],
              },
            },
          ],
        }
      : undefined;

    return {
      empresa_id,
      deleted_at: null,
      ...(query.cliente_id ? { cliente_id: query.cliente_id } : {}),
      ...(query.estado ? { estado_actual: query.estado } : {}),
      ...(Object.keys(fechaIngreso).length ? { fecha_ingreso: fechaIngreso } : {}),
      ...(clienteFilter ? { cliente: clienteFilter } : {}),
    };
  }

  async findAll(empresa_id: string, query: QueryGestionMoraDto) {
    const page = Number(query.page) > 0 ? Number(query.page) : 1;
    const limit = Number(query.limit) > 0 ? Number(query.limit) : 50;
    const where = this.buildWhereFromQuery(empresa_id, query);

    const [rows, total] = await Promise.all([
      this.prisma.cob_gestion_mora.findMany({
        where,
        include: {
          cliente: { include: { personas: true } },
          _count: { select: { facturas: true, historial: true } },
        },
        orderBy: { fecha_ingreso: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.cob_gestion_mora.count({ where }),
    ]);

    return { rows, total, page, limit };
  }

  /** Exporta CSV (UTF-8 BOM, Excel-compatible) con los mismos filtros que findAll. */
  async exportCsv(empresa_id: string, query: QueryGestionMoraDto) {
    const where = this.buildWhereFromQuery(empresa_id, query);
    const rows = await this.prisma.cob_gestion_mora.findMany({
      where,
      include: {
        cliente: { include: { personas: true } },
        _count: { select: { facturas: true, historial: true } },
      },
      orderBy: { fecha_ingreso: 'desc' },
    });

    const headers = [
      'Cliente',
      'RUC',
      'Estado',
      'Monto involucrado',
      'Facturas',
      'Ingreso al estado',
      'Observación',
    ];
    const escape = (v: unknown) => {
      const s = v == null ? '' : String(v);
      return /[";\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
    };
    const fmtFecha = (d: Date | null | undefined) =>
      d ? new Date(d).toISOString().slice(0, 10) : '';

    const lineas = rows.map((g) => {
      const p = g.cliente?.personas;
      const cliente =
        g.cliente?.nombre_fantasia ||
        [p?.nombres, p?.apellidos].filter(Boolean).join(' ') ||
        p?.razon_social ||
        '';
      return [
        cliente,
        p?.ruc ?? '',
        g.estado_actual,
        Math.round(Number(g.monto_involucrado) || 0),
        g._count?.facturas ?? 0,
        fmtFecha(g.fecha_ingreso),
        g.observacion ?? '',
      ]
        .map(escape)
        .join(';');
    });

    return '\uFEFF' + [headers.join(';'), ...lineas].join('\n');
  }

  async findOne(id: string, empresa_id: string) {
    const gestion = await this.prisma.cob_gestion_mora.findFirst({
      where: { id, empresa_id, deleted_at: null },
      include: {
        cliente: { include: { personas: true } },
        facturas: {
          include: {
            factura_cab: {
              include: { factura_det: true, moneda: true },
            },
          },
        },
        historial: { orderBy: { fecha: 'desc' } },
      },
    });
    if (!gestion) throw new NotFoundException('Gestión de mora no encontrada');
    return gestion;
  }

  async transicionar(
    id: string,
    dto: TransicionEstadoDto,
    empresa_id: string,
    user_id: string,
  ) {
    const gestion = await this.prisma.cob_gestion_mora.findFirst({
      where: { id, empresa_id, deleted_at: null },
    });
    if (!gestion) throw new NotFoundException('Gestión de mora no encontrada');

    const permitidos = TRANSICIONES[gestion.estado_actual] ?? [];
    if (!permitidos.includes(dto.estado_nuevo)) {
      throw new BadRequestException(
        `Transición no permitida: ${gestion.estado_actual} → ${dto.estado_nuevo}`,
      );
    }

    this.validarRequisitosEstado(dto.estado_nuevo, dto.documento_url, dto.autorizado_por_id);

    const updated = await this.prisma.$transaction(async (tx) => {
      const u = await tx.cob_gestion_mora.update({
        where: { id },
        data: {
          estado_actual: dto.estado_nuevo,
          fecha_ingreso: new Date(),
          documento_url: dto.documento_url ?? gestion.documento_url,
          autorizado_por_id: dto.autorizado_por_id ?? null,
        },
      });

      await tx.cob_gestion_mora_historial.create({
        data: {
          gestion_mora_id: id,
          estado_anterior: gestion.estado_actual,
          estado_nuevo: dto.estado_nuevo,
          motivo: dto.motivo,
          usuario_id: user_id,
          documento_url: dto.documento_url ?? null,
        },
      });

      return u;
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'TRANSITION',
      entity_type: 'cob_gestion_mora',
      entity_id: id,
      descripcion:
        `Gestión de mora: transición ${gestion.estado_actual} → ${dto.estado_nuevo}` +
        ` | Cliente: ${gestion.cliente_id} | Motivo: ${dto.motivo}` +
        (dto.autorizado_por_id ? ` | Autorizado por: ${dto.autorizado_por_id}` : ''),
      old_value: {
        estado_actual: gestion.estado_actual,
        documento_url: gestion.documento_url,
        autorizado_por_id: gestion.autorizado_por_id,
      },
      new_value: {
        estado_actual: dto.estado_nuevo,
        documento_url: dto.documento_url ?? gestion.documento_url,
        autorizado_por_id: dto.autorizado_por_id ?? null,
        motivo: dto.motivo,
      },
    });

    return updated;
  }

  async getHistorial(id: string, empresa_id: string) {
    const gestion = await this.prisma.cob_gestion_mora.findFirst({
      where: { id, empresa_id },
      select: { id: true },
    });
    if (!gestion) throw new NotFoundException('Gestión de mora no encontrada');

    return this.prisma.cob_gestion_mora_historial.findMany({
      where: { gestion_mora_id: id },
      orderBy: { fecha: 'desc' },
    });
  }

  /**
   * Dashboard: totales por estado + monto involucrado para la empresa.
   * Solo cuenta registros vivos (`deleted_at IS NULL`).
   */
  async dashboard(empresa_id: string) {
    const groups = await this.prisma.cob_gestion_mora.groupBy({
      by: ['estado_actual'],
      where: { empresa_id, deleted_at: null },
      _count: { _all: true },
      _sum: { monto_involucrado: true },
    });

    const totalesPorEstado = groups.map((g) => ({
      estado: g.estado_actual,
      cantidad: g._count._all,
      monto: Number(g._sum.monto_involucrado ?? 0),
    }));

    const totalGeneral = totalesPorEstado.reduce(
      (acc, g) => {
        acc.cantidad += g.cantidad;
        acc.monto += g.monto;
        return acc;
      },
      { cantidad: 0, monto: 0 },
    );

    const activos = totalesPorEstado.filter((g) => ESTADOS_ACTIVOS.includes(g.estado));
    const totalActivos = activos.reduce(
      (acc, g) => {
        acc.cantidad += g.cantidad;
        acc.monto += g.monto;
        return acc;
      },
      { cantidad: 0, monto: 0 },
    );

    return {
      por_estado: totalesPorEstado,
      total_general: totalGeneral,
      total_activos: totalActivos,
    };
  }

  /**
   * Devuelve la gestión activa del cliente (si hay) + timeline completo de todas las gestiones del cliente.
   * "Activa" = estado en ESTADOS_ACTIVOS y no soft-deleted.
   */
  async getByCliente(cliente_id: string, empresa_id: string) {
    const gestiones = await this.prisma.cob_gestion_mora.findMany({
      where: { cliente_id, empresa_id, deleted_at: null },
      orderBy: { fecha_ingreso: 'desc' },
      include: {
        historial: { orderBy: { fecha: 'desc' } },
        _count: { select: { facturas: true } },
      },
    });

    const activa = gestiones.find((g) => ESTADOS_ACTIVOS.includes(g.estado_actual)) ?? null;

    return {
      activa,
      historial: gestiones,
      tiene_gestion_activa: !!activa,
    };
  }

  /**
   * Hook: cuando un cliente cancela su deuda (saldo = 0), transiciona todas sus
   * gestiones activas a RECUPERADA. Llamado por cobros.service tras crear recibo.
   *
   * No-throw: si falla, se loguea pero no rompe el flujo de cobro.
   */
  async autoRecuperarPorCliente(cliente_id: string, empresa_id: string, user_id?: string) {
    try {
      const cliente = await this.prisma.clientes.findFirst({
        where: { id: cliente_id },
        select: { saldo_pendiente: true },
      });
      // Solo cierra si el saldo total efectivamente quedó en cero
      if (!cliente || Number(cliente.saldo_pendiente ?? 0) > 0) return { transitioned: 0 };

      const activas = await this.prisma.cob_gestion_mora.findMany({
        where: {
          cliente_id,
          empresa_id,
          deleted_at: null,
          estado_actual: { in: ESTADOS_ACTIVOS },
        },
        select: { id: true, estado_actual: true },
      });
      if (activas.length === 0) return { transitioned: 0 };

      const now = new Date();
      await this.prisma.$transaction(async (tx) => {
        for (const g of activas) {
          await tx.cob_gestion_mora.update({
            where: { id: g.id },
            data: { estado_actual: CobMoraEstado.RECUPERADA, fecha_ingreso: now },
          });
          await tx.cob_gestion_mora_historial.create({
            data: {
              gestion_mora_id: g.id,
              estado_anterior: g.estado_actual,
              estado_nuevo: CobMoraEstado.RECUPERADA,
              motivo: 'Auto-cierre: el cliente canceló la deuda',
              usuario_id: user_id ?? cliente_id,
            },
          });
        }
      });

      // Auditoría agregada (una entrada por transición)
      for (const g of activas) {
        await this.auditService.log({
          empresa_id,
          user_id,
          action: 'AUTO_TRANSITION',
          entity_type: 'cob_gestion_mora',
          entity_id: g.id,
          descripcion: `Auto-cierre RECUPERADA por cancelación de saldo | Cliente: ${cliente_id} | Estado previo: ${g.estado_actual}`,
          old_value: { estado_actual: g.estado_actual },
          new_value: { estado_actual: CobMoraEstado.RECUPERADA, motivo: 'cliente canceló deuda' },
        });
      }

      return { transitioned: activas.length };
    } catch (err) {
      this.logger.error(
        `autoRecuperarPorCliente falló para cliente ${cliente_id}: ${err instanceof Error ? err.message : 'unknown'}`,
      );
      return { transitioned: 0, error: true };
    }
  }

  /**
   * Cron: abre gestión GESTION_INTERNA para clientes con cuotas vencidas hace
   * más de N días (default 30, o `clientes.dias_mora_maximo` si es mayor a 0)
   * que aún no tienen gestión activa.
   *
   * Itera por empresa activa. Idempotente: omite clientes con gestión activa.
   */
  async autoAbrirGestionesVencidas(opts: { dias_umbral?: number } = {}) {
    const umbralDefault = opts.dias_umbral ?? DIAS_UMBRAL_GESTION_DEFAULT;
    const ahora = new Date();
    let abiertas = 0;
    const errores: { cliente_id: string; error: string }[] = [];

    // Solo empresas cuyo plan incluye el submódulo "Gestión de Mora Avanzada" (COB_GMR):
    // tienen el módulo COBRANZAS **y** no lo excluyeron. La apertura automática de
    // gestiones de mora es parte de ese submódulo — sin este filtro el cron abría
    // gestiones para empresas que solo tienen Cobros/Recibos/Cuentas por Cobrar.
    //
    // COB_GMR es un submódulo NO-addon: viene con COBRANZAS salvo que la suscripción lo
    // excluya (fila en suscripcion_submodulos con active=false). Misma lógica que
    // SuscripcionesService.getSubmodulosActivos.
    const [modsCobranzas, exclusionesGmr] = await Promise.all([
      this.prisma.suscripcion_modulos.findMany({
        where: {
          activo: true,
          modulos: { codigo: 'COBRANZAS', active: true },
          suscripciones: { estado: { in: ['Activo', 'EnGracia'] } },
        },
        select: { suscripciones: { select: { empresa_id: true } } },
      }),
      this.prisma.suscripcion_submodulos.findMany({
        where: {
          active: false,
          submodulos: { codigo: 'COB_GMR' },
          suscripciones: { estado: { in: ['Activo', 'EnGracia'] } },
        },
        select: { suscripciones: { select: { empresa_id: true } } },
      }),
    ]);
    const excluidas = new Set(
      exclusionesGmr.map((e) => e.suscripciones?.empresa_id).filter((id): id is string => !!id),
    );
    const empresaIds = [
      ...new Set(
        modsCobranzas
          .map((m) => m.suscripciones?.empresa_id)
          .filter((id): id is string => !!id && !excluidas.has(id)),
      ),
    ];
    if (!empresaIds.length) {
      this.logger.log('autoAbrirGestionesVencidas: no hay empresas con el submódulo Gestión de Mora Avanzada (COB_GMR) activo.');
      return { abiertas: 0, errores: [] as { cliente_id: string; error: string }[] };
    }

    const empresas = await this.prisma.empresas.findMany({
      where: { active: true, id: { in: empresaIds } },
      select: { id: true },
    });

    for (const emp of empresas) {
      // Cuotas vencidas y con saldo > 0, agrupadas por cliente
      const cuotasVencidas = await this.prisma.factura_cuotas.findMany({
        where: {
          estado: 'pendiente',
          saldo_pendiente: { gt: 0 },
          dvenccuo: { lt: ahora },
          factura_cab: { empresa_id: emp.id, estado: { notIn: ['Anulado', 'Anulada'] } },
        },
        select: {
          dvenccuo: true,
          factura_cab_id: true,
          saldo_pendiente: true,
          factura_cab: {
            select: {
              cliente_id: true,
              clientes: { select: { dias_mora_maximo: true } },
            },
          },
        },
      });

      // Agrupar por cliente
      const porCliente = new Map<
        string,
        {
          factura_ids: Set<string>;
          monto: number;
          diasMaxVencido: number;
          umbralCliente: number;
        }
      >();
      for (const c of cuotasVencidas) {
        const clienteId = c.factura_cab?.cliente_id;
        if (!clienteId) continue;
        const dias = Math.floor((ahora.getTime() - new Date(c.dvenccuo!).getTime()) / 86400000);
        const umbralCliente = Number(c.factura_cab?.clientes?.dias_mora_maximo ?? 0) || umbralDefault;
        const entry = porCliente.get(clienteId) ?? {
          factura_ids: new Set<string>(),
          monto: 0,
          diasMaxVencido: 0,
          umbralCliente,
        };
        entry.factura_ids.add(c.factura_cab_id!);
        entry.monto += Number(c.saldo_pendiente ?? 0);
        if (dias > entry.diasMaxVencido) entry.diasMaxVencido = dias;
        porCliente.set(clienteId, entry);
      }

      for (const [cliente_id, info] of porCliente) {
        if (info.diasMaxVencido < info.umbralCliente) continue;

        try {
          // Skip si ya tiene gestión activa
          const yaActiva = await this.prisma.cob_gestion_mora.findFirst({
            where: {
              cliente_id,
              empresa_id: emp.id,
              deleted_at: null,
              estado_actual: { in: ESTADOS_ACTIVOS },
            },
            select: { id: true },
          });
          if (yaActiva) continue;

          await this.prisma.$transaction(async (tx) => {
            const g = await tx.cob_gestion_mora.create({
              data: {
                empresa_id: emp.id,
                cliente_id,
                estado_actual: CobMoraEstado.GESTION_INTERNA,
                monto_involucrado: new Prisma.Decimal(info.monto),
                observacion: `Apertura automática: ${info.diasMaxVencido} días vencido (umbral ${info.umbralCliente})`,
                usuario_id: cliente_id, // placeholder: no hay user humano en el cron
              },
            });
            await tx.cob_gestion_mora_factura.createMany({
              data: Array.from(info.factura_ids).map((fid) => ({
                gestion_mora_id: g.id,
                factura_cab_id: fid,
                saldo_snapshot: new Prisma.Decimal(0),
              })),
            });
            await tx.cob_gestion_mora_historial.create({
              data: {
                gestion_mora_id: g.id,
                estado_anterior: null,
                estado_nuevo: CobMoraEstado.GESTION_INTERNA,
                motivo: `Auto-apertura por cron: ${info.diasMaxVencido} días vencido`,
                usuario_id: cliente_id,
              },
            });

            await this.auditService.log({
              empresa_id: emp.id,
              action: 'AUTO_CREATE',
              entity_type: 'cob_gestion_mora',
              entity_id: g.id,
              descripcion: `Auto-apertura GESTION_INTERNA por cron | Cliente: ${cliente_id} | Días vencido: ${info.diasMaxVencido} | Facturas: ${info.factura_ids.size} | Monto: ${info.monto}`,
              new_value: {
                cliente_id,
                estado_inicial: CobMoraEstado.GESTION_INTERNA,
                dias_vencido: info.diasMaxVencido,
                umbral_aplicado: info.umbralCliente,
                facturas: Array.from(info.factura_ids),
                monto_involucrado: info.monto,
              },
            });
          });
          abiertas++;
        } catch (err) {
          const msg = err instanceof Error ? err.message : 'unknown';
          this.logger.error(`autoAbrirGestionesVencidas cliente ${cliente_id}: ${msg}`);
          errores.push({ cliente_id, error: msg });
        }
      }
    }

    this.logger.log(`autoAbrirGestionesVencidas: ${abiertas} gestiones abiertas, ${errores.length} errores`);
    return { abiertas, errores };
  }

  /**
   * Construye el payload para el PDF del expediente de gestión de mora.
   * Trae datos de empresa, cliente, facturas afectadas (snapshot + saldo actual),
   * timeline completo y el usuario que abrió la gestión.
   */
  async buildExpedientePayload(id: string, empresa_id: string) {
    const gestion = await this.prisma.cob_gestion_mora.findFirst({
      where: { id, empresa_id, deleted_at: null },
      include: {
        cliente: {
          include: {
            personas: {
              select: {
                razon_social: true,
                ruc: true,
                dv: true,
                nro_documento: true,
                telefono: true,
                celular: true,
                email: true,
                direccion: true,
              },
            },
          },
        },
        facturas: {
          include: {
            factura_cab: {
              select: {
                id: true,
                dnumdoc: true,
                dest: true,
                dpunexp: true,
                dfeemide: true,
                dvencpag: true,
                total_factura: true,
                saldo_pendiente: true,
                moneda: { select: { codigo: true } },
              },
            },
          },
        },
        historial: { orderBy: { fecha: 'asc' } },
      },
    });
    if (!gestion) throw new NotFoundException('Gestión de mora no encontrada');

    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresa_id },
      select: {
        razon_social: true,
        ruc: true,
        dv: true,
        celular: true,
        email: true,
        logo: true,
        nombre_fantasia: true,
      },
    });

    // Resolver nombres de usuarios involucrados (apertura + cada transición + autorizado_por)
    const userIds = new Set<string>();
    if (gestion.usuario_id) userIds.add(gestion.usuario_id);
    if (gestion.autorizado_por_id) userIds.add(gestion.autorizado_por_id);
    for (const h of gestion.historial) if (h.usuario_id) userIds.add(h.usuario_id);

    const usuarios = userIds.size
      ? await this.prisma.usuario.findMany({
          where: { id: { in: Array.from(userIds) } },
          select: { id: true, nombres: true, apellidos: true, email: true },
        })
      : [];
    const usuarioMap = new Map(
      usuarios.map((u) => [
        u.id,
        `${u.nombres ?? ''} ${u.apellidos ?? ''}`.trim() || u.email || u.id,
      ]),
    );

    const facturas = gestion.facturas.map((f) => ({
      id: f.factura_cab?.id,
      numero: f.factura_cab
        ? `${String(f.factura_cab.dest).padStart(3, '0')}-${String(f.factura_cab.dpunexp).padStart(3, '0')}-${String(f.factura_cab.dnumdoc).padStart(7, '0')}`
        : null,
      fecha_emision: f.factura_cab?.dfeemide ?? null,
      fecha_vencimiento: f.factura_cab?.dvencpag ?? null,
      moneda: f.factura_cab?.moneda?.codigo ?? 'PYG',
      total: Number(f.factura_cab?.total_factura ?? 0),
      saldo_snapshot: Number(f.saldo_snapshot ?? 0),
      saldo_actual: Number(f.factura_cab?.saldo_pendiente ?? 0),
    }));

    const timeline = gestion.historial.map((h) => ({
      fecha: h.fecha,
      estado_anterior: h.estado_anterior,
      estado_nuevo: h.estado_nuevo,
      motivo: h.motivo,
      usuario: usuarioMap.get(h.usuario_id) ?? h.usuario_id,
      documento_url: h.documento_url,
    }));

    const totalSnapshot = facturas.reduce((acc, f) => acc + f.saldo_snapshot, 0);
    const totalActual = facturas.reduce((acc, f) => acc + f.saldo_actual, 0);

    return {
      empresa: {
        razon_social: empresa?.razon_social,
        ruc: empresa?.ruc,
        dv: empresa?.dv,
        celular: empresa?.celular,
        email: empresa?.email,
        logo_url: empresa?.logo ?? null,
        // Nombre comercial arriba y razón social debajo en el PDF (criterio de la factura).
        nombre_fantasia: empresa?.nombre_fantasia ?? null,
      },
      cliente: {
        id: gestion.cliente_id,
        razon_social: gestion.cliente?.personas?.razon_social,
        ruc: gestion.cliente?.personas?.ruc,
        dv: gestion.cliente?.personas?.dv,
        nro_documento: gestion.cliente?.personas?.nro_documento,
        telefono: gestion.cliente?.personas?.telefono,
        celular: gestion.cliente?.personas?.celular,
        email: gestion.cliente?.personas?.email,
        direccion: gestion.cliente?.personas?.direccion,
      },
      gestion: {
        id: gestion.id,
        estado_actual: gestion.estado_actual,
        fecha_apertura: gestion.created_at,
        fecha_estado: gestion.fecha_ingreso,
        monto_involucrado: Number(gestion.monto_involucrado ?? 0),
        observacion: gestion.observacion,
        documento_url: gestion.documento_url,
        usuario_apertura: usuarioMap.get(gestion.usuario_id) ?? gestion.usuario_id,
        autorizado_por: gestion.autorizado_por_id
          ? (usuarioMap.get(gestion.autorizado_por_id) ?? gestion.autorizado_por_id)
          : null,
      },
      // Marcador para integración futura (Submódulo Garantes — diferido)
      garantes: [],
      facturas,
      totales: {
        cantidad_facturas: facturas.length,
        saldo_snapshot: totalSnapshot,
        saldo_actual: totalActual,
        recuperado: totalSnapshot - totalActual,
      },
      timeline,
      fecha_generacion: new Date(),
    };
  }

  /**
   * Genera el PDF del expediente llamando a msv-kude.
   * tipo_retorno: 'view' devuelve Buffer; 'base64' devuelve string.
   */
  async generateExpedientePdf(
    id: string,
    empresa_id: string,
    options: { tipo_retorno?: 'view' | 'base64' } = {},
  ): Promise<{ pdfBuffer?: Buffer; base64?: string; contentType: string }> {
    const tipo_retorno = options.tipo_retorno ?? 'view';
    const data = await this.buildExpedientePayload(id, empresa_id);
    const payload = { tipo: tipo_retorno, ...data };

    const url = `${envs.apiGeneradorPDF}/api/gestion-mora/generate-pdf`;
    this.logger.log(`Generando PDF expediente gestión mora ${id}`);

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });

      if (!response.ok) {
        const errorBody = await response.text();
        this.logger.error(`msv-kude responded ${response.status}: ${errorBody}`);
        throw new HttpException(
          { message: 'Error al generar PDF de expediente', detail: errorBody },
          HttpStatus.BAD_GATEWAY,
        );
      }

      if (tipo_retorno === 'base64') {
        const json = (await response.json()) as { status: string; pdf_base64?: string; message?: string };
        if (json.status !== 'success') {
          throw new HttpException(
            { message: json.message || 'Error al generar PDF' },
            HttpStatus.BAD_GATEWAY,
          );
        }
        return { base64: json.pdf_base64, contentType: 'application/json' };
      }

      const buf = Buffer.from(await response.arrayBuffer());
      return { pdfBuffer: buf, contentType: 'application/pdf' };
    } catch (err) {
      if (err instanceof HttpException) throw err;
      const message = err instanceof Error ? err.message : 'Error desconocido';
      this.logger.error(`Error al generar PDF expediente: ${message}`);
      throw new HttpException(
        { message: 'No se pudo generar el PDF del expediente', detail: message },
        HttpStatus.BAD_GATEWAY,
      );
    }
  }

  private validarRequisitosEstado(
    estado: CobMoraEstado,
    documento_url: string | null | undefined,
    autorizado_por_id: string | null | undefined,
  ) {
    if (ESTADOS_REQUIEREN_DOCUMENTO.includes(estado) && !documento_url) {
      throw new BadRequestException(
        `El estado ${estado} requiere adjuntar documento_url`,
      );
    }
    if (
      ESTADOS_DOBLE_AUTORIZACION.includes(estado) &&
      !autorizado_por_id
    ) {
      throw new ForbiddenException(
        `El estado ${estado} requiere doble autorización (autorizado_por_id)`,
      );
    }
  }
}
