import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Optional } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from '../audit/audit.service';
import { WebhookService } from '../cartera/webhooks/webhook.service';
import { PrismaService } from '../prisma/prisma.service';
import {
  CancelarPromesaDto,
  CreatePromesaMultiFacturaDto,
  CreatePromesaPagoDto,
  QueryPromesasDto,
  RenegociarPromesaDto,
  UpdatePromesaEstadoDto,
} from './dto/cobranzas.dto';

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
    // Opcional: si el módulo Cartera Inteligente no está cargado o la empresa
    // no tiene webhooks configurados, este servicio sigue funcionando igual.
    // Cualquier fallo en la emisión NO afecta el flujo de promesas.
    @Optional() @Inject(WebhookService) private readonly webhooks?: WebhookService,
  ) {}

  /**
   * Emite un evento de webhook si el servicio está disponible. Es completamente
   * fire-and-forget: si la cola no está, no hay endpoints suscritos, o el job
   * falla, no afecta al caller. Toda la lógica de cobranzas sigue intacta.
   */
  private emitirEvento(empresa_id: string, evento: 'promesa.creada' | 'promesa.cumplida' | 'promesa.incumplida', payload: unknown): void {
    if (!this.webhooks) return;
    try {
      void this.webhooks
        .publicar(empresa_id, evento as never, payload)
        .catch((err) => this.logger.warn(`Webhook ${evento} fallo: ${err?.message ?? err}`));
    } catch (err: any) {
      this.logger.warn(`Webhook ${evento} threw: ${err?.message ?? err}`);
    }
  }

  private calcularFechaRecordatorio(fechaPrometida: Date): Date {
    const r = new Date(fechaPrometida);
    r.setDate(r.getDate() - 1);
    return r;
  }

  async create(dto: CreatePromesaPagoDto, empresa_id: string, user_id: string) {
    const fechaPrometida = new Date(dto.fecha_prometida);
    if (isNaN(fechaPrometida.getTime())) {
      throw new BadRequestException('Fecha prometida inválida');
    }

    const promesa = await this.prisma.promesas_pago.create({
      data: {
        empresa_id,
        factura_cab_id: dto.factura_cab_id,
        cliente_id: dto.cliente_id,
        cobrador_id: dto.cobrador_id ?? null,
        cuota_id: dto.cuota_id ?? null,
        fecha_prometida: fechaPrometida,
        monto_prometido: dto.monto_prometido ?? null,
        estado: 'pendiente',
        notas: dto.notas ?? null,
        fecha_recordatorio: this.calcularFechaRecordatorio(fechaPrometida),
        creado_por: user_id,
        tipo_evidencia: (dto.tipo_evidencia as any) ?? 'VERBAL',
        pagador_tipo: (dto.pagador_tipo as any) ?? 'CLIENTE',
        pagador_cliente_id: dto.pagador_cliente_id ?? null,
      },
      include: {
        cliente: { select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true } } } },
        cobrador: { select: { id: true, nombre: true, apellido: true } },
      },
    });

    this.logger.log(`Promesa de pago creada: ${promesa.id} — Factura ${dto.factura_cab_id}`);
    try {
      await this.auditService.log({
        empresa_id,
        user_id,
        action: 'CREATE',
        entity_type: 'promesas_pago',
        entity_id: promesa.id,
        descripcion: `Promesa de pago | Cliente ${dto.cliente_id} | Factura ${dto.factura_cab_id} | Fecha: ${dto.fecha_prometida}`,
      });
    } catch (e) {
      this.logger.warn(`Audit log fallo: ${(e as Error).message}`);
    }
    this.emitirEvento(empresa_id, 'promesa.creada', {
      promesa_id: promesa.id,
      cliente_id: dto.cliente_id,
      factura_cab_id: dto.factura_cab_id,
      cuota_id: dto.cuota_id ?? null,
      fecha_prometida: fechaPrometida,
      monto_prometido: dto.monto_prometido ?? null,
      pagador_tipo: dto.pagador_tipo ?? 'CLIENTE',
      usuario_id: user_id,
    });
    return promesa;
  }

  async createMultiFactura(dto: CreatePromesaMultiFacturaDto, empresa_id: string, user_id: string) {
    const fechaPrometida = new Date(dto.fecha_prometida);
    if (isNaN(fechaPrometida.getTime())) throw new BadRequestException('Fecha prometida inválida');
    if (!dto.facturas?.length) throw new BadRequestException('Debe indicar al menos una factura');

    // Validar facturas del cliente y empresa (varias cuotas pueden ser de la misma factura)
    const facturaIds = [...new Set(dto.facturas.map((f) => f.factura_cab_id))];
    const facturasDb = await this.prisma.factura_cab.findMany({
      where: { id: { in: facturaIds }, empresa_id, cliente_id: dto.cliente_id },
      select: { id: true },
    });
    if (facturasDb.length !== facturaIds.length) {
      throw new BadRequestException('Una o más facturas no pertenecen al cliente o empresa');
    }

    const montoTotal = dto.facturas.reduce((s, f) => s + Number(f.monto_asignado), 0);
    // Un solo objetivo con cuota → promesa "por cuota" clásica (cuota_id seteado), que es
    // lo que leen el wizard de cobros y el Panel Cobrador. Con varios, cuota_id queda
    // null y el cierre se evalúa por el acumulado sobre los objetivos.
    const cuotaUnica = dto.facturas.length === 1 ? (dto.facturas[0].cuota_id ?? null) : null;

    return this.prisma.$transaction(async (tx) => {
      const promesa = await tx.promesas_pago.create({
        data: {
          empresa_id,
          factura_cab_id: facturaIds[0], // factura principal (compatibilidad legacy)
          cuota_id: cuotaUnica,
          cliente_id: dto.cliente_id,
          cobrador_id: dto.cobrador_id ?? null,
          fecha_prometida: fechaPrometida,
          monto_prometido: new Prisma.Decimal(montoTotal),
          estado: 'pendiente',
          notas: dto.notas ?? null,
          fecha_recordatorio: this.calcularFechaRecordatorio(fechaPrometida),
          creado_por: user_id,
          tipo_evidencia: (dto.tipo_evidencia as any) ?? 'VERBAL',
          pagador_tipo: (dto.pagador_tipo as any) ?? 'CLIENTE',
          pagador_cliente_id: dto.pagador_cliente_id ?? null,
        },
      });

      await tx.cob_promesa_factura.createMany({
        data: dto.facturas.map((f) => ({
          promesa_pago_id: promesa.id,
          factura_cab_id: f.factura_cab_id,
          cuota_id: f.cuota_id ?? null,
          monto_asignado: new Prisma.Decimal(f.monto_asignado),
        })),
      });

      try {
        await this.auditService.log({
          empresa_id,
          user_id,
          action: 'CREATE',
          entity_type: 'promesas_pago',
          entity_id: promesa.id,
          descripcion: `Promesa | Cliente ${dto.cliente_id} | ${dto.facturas.length} objetivos (${facturaIds.length} facturas) | Total: ${montoTotal}`,
        });
      } catch (e) {
        this.logger.warn(`Audit fallo: ${(e as Error).message}`);
      }
      return promesa;
    });
  }

  async findAll(empresa_id: string, query: QueryPromesasDto) {
    const where: any = { empresa_id };

    if (query.factura_cab_id) where.factura_cab_id = query.factura_cab_id;
    if (query.cobrador_id) where.cobrador_id = query.cobrador_id;
    if (query.cliente_id) where.cliente_id = query.cliente_id;
    if (query.estado && query.estado !== 'todas') where.estado = query.estado;
    if (query.fecha_desde || query.fecha_hasta) {
      where.fecha_prometida = {};
      if (query.fecha_desde) where.fecha_prometida.gte = new Date(query.fecha_desde);
      if (query.fecha_hasta) where.fecha_prometida.lte = new Date(query.fecha_hasta);
    }

    const promesas = await this.prisma.promesas_pago.findMany({
      where,
      include: {
        cliente: { select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true } } } },
        cobrador: { select: { id: true, nombre: true, apellido: true } },
        factura: {
          select: {
            id: true,
            dnumdoc: true,
            dest: true,
            dpunexp: true,
            moneda: { select: { codigo: true } },
          },
        },
        facturas_objetivo: {
          select: {
            factura_cab_id: true,
            cuota_id: true,
            monto_asignado: true,
            cuota: { select: { nro_cuota: true } },
            factura: { select: { dnumdoc: true, dest: true, dpunexp: true } },
          },
          orderBy: [{ factura_cab_id: 'asc' }, { cuota: { nro_cuota: 'asc' } }],
        },
      },
      orderBy: [{ estado: 'asc' }, { fecha_prometida: 'asc' }],
    });

    // Enriquecer con cuota (cuota_id es referencia suelta, no relación) y pagador_cliente
    const cuotaIds = promesas.map((p) => p.cuota_id).filter((v): v is string => !!v);
    const pagadorIds = promesas.map((p) => p.pagador_cliente_id).filter((v): v is string => !!v);
    const [cuotas, pagadores] = await Promise.all([
      cuotaIds.length
        ? this.prisma.factura_cuotas.findMany({
            where: { id: { in: cuotaIds } },
            select: { id: true, nro_cuota: true },
          })
        : Promise.resolve([]),
      pagadorIds.length
        ? this.prisma.clientes.findMany({
            where: { id: { in: pagadorIds } },
            select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true } } },
          })
        : Promise.resolve([]),
    ]);
    const cuotaMap = new Map(cuotas.map((c) => [c.id, c]));
    const pagadorMap = new Map(pagadores.map((c) => [c.id, c]));

    return promesas.map((p) => ({
      ...p,
      cuota: p.cuota_id ? cuotaMap.get(p.cuota_id) ?? null : null,
      pagador_cliente: p.pagador_cliente_id ? pagadorMap.get(p.pagador_cliente_id) ?? null : null,
    }));
  }

  async findByFactura(facturaId: string, empresa_id: string) {
    return this.prisma.promesas_pago.findMany({
      where: {
        empresa_id,
        OR: [
          { factura_cab_id: facturaId },
          { facturas_objetivo: { some: { factura_cab_id: facturaId } } },
        ],
      },
      include: {
        cobrador: { select: { id: true, nombre: true, apellido: true } },
        creador: { select: { id: true, nombres: true } },
        facturas_objetivo: { select: { factura_cab_id: true, monto_asignado: true } },
      },
      orderBy: { created_at: 'desc' },
    });
  }

  async updateEstado(id: string, dto: UpdatePromesaEstadoDto, empresa_id: string, user_id?: string) {
    const promesa = await this.prisma.promesas_pago.findFirst({ where: { id, empresa_id } });
    if (!promesa) throw new NotFoundException(`Promesa ${id} no encontrada`);
    // Una incumplida puede cerrarse a mano como cumplida (el cliente pagó tarde por un
    // canal que el hook no ve); cualquier otro cambio exige que siga pendiente.
    const puedeCambiar = promesa.estado === 'pendiente' || (promesa.estado === 'incumplida' && dto.estado === 'cumplida');
    if (!puedeCambiar) {
      throw new BadRequestException(`La promesa ya está en estado "${promesa.estado}"`);
    }

    const ahora = new Date();
    const updated = await this.prisma.promesas_pago.update({
      where: { id },
      data: {
        estado: dto.estado,
        notas: dto.notas ? `${promesa.notas ?? ''}\n[${dto.estado}]: ${dto.notas}`.trim() : promesa.notas,
        ...(dto.estado === 'cumplida'
          ? { fecha_cumplimiento: ahora, monto_cumplido: promesa.monto_cumplido ?? promesa.monto_prometido }
          : {}),
        updated_at: ahora,
      },
    });
    try {
      await this.auditService.log({
        empresa_id,
        user_id: user_id ?? promesa.creado_por,
        action: 'UPDATE',
        entity_type: 'promesas_pago',
        entity_id: id,
        descripcion: `Promesa ${id} → ${dto.estado}`,
      });
    } catch (e) {
      this.logger.warn(`Audit fallo: ${(e as Error).message}`);
    }
    if (dto.estado === 'cumplida' || dto.estado === 'incumplida') {
      this.emitirEvento(empresa_id, `promesa.${dto.estado}` as 'promesa.cumplida' | 'promesa.incumplida', {
        promesa_id: id,
        cliente_id: promesa.cliente_id,
        factura_cab_id: promesa.factura_cab_id,
        cuota_id: promesa.cuota_id,
        fecha_prometida: promesa.fecha_prometida,
        monto_prometido: promesa.monto_prometido,
        notas: dto.notas ?? null,
        usuario_id: user_id ?? promesa.creado_por,
      });
    }
    return updated;
  }

  async cancelar(id: string, dto: CancelarPromesaDto, empresa_id: string, user_id: string) {
    const promesa = await this.prisma.promesas_pago.findFirst({ where: { id, empresa_id } });
    if (!promesa) throw new NotFoundException(`Promesa ${id} no encontrada`);
    if (promesa.estado !== 'pendiente') {
      throw new BadRequestException(`Sólo promesas pendientes pueden cancelarse (actual: ${promesa.estado})`);
    }

    const updated = await this.prisma.promesas_pago.update({
      where: { id },
      data: {
        estado: 'cancelada',
        notas: `${promesa.notas ?? ''}\n[cancelada]: ${dto.motivo}`.trim(),
        updated_at: new Date(),
      },
    });
    try {
      await this.auditService.log({
        empresa_id,
        user_id,
        action: 'CANCEL',
        entity_type: 'promesas_pago',
        entity_id: id,
        descripcion: `Promesa cancelada | Motivo: ${dto.motivo}`,
      });
    } catch (e) {
      this.logger.warn(`Audit fallo: ${(e as Error).message}`);
    }
    return updated;
  }

  async renegociar(id: string, dto: RenegociarPromesaDto, empresa_id: string, user_id: string) {
    const original = await this.prisma.promesas_pago.findFirst({
      where: { id, empresa_id },
      include: { facturas_objetivo: true },
    });
    if (!original) throw new NotFoundException(`Promesa ${id} no encontrada`);
    if (!['pendiente', 'incumplida'].includes(original.estado)) {
      throw new BadRequestException(`Sólo promesas pendientes o incumplidas pueden renegociarse (actual: ${original.estado})`);
    }

    const nuevaFecha = new Date(dto.fecha_prometida);
    if (isNaN(nuevaFecha.getTime())) throw new BadRequestException('Fecha prometida inválida');

    return this.prisma.$transaction(async (tx) => {
      await tx.promesas_pago.update({
        where: { id },
        data: {
          estado: 'renegociada',
          notas: `${original.notas ?? ''}\n[renegociada]: ${dto.notas}`.trim(),
          updated_at: new Date(),
        },
      });

      const nueva = await tx.promesas_pago.create({
        data: {
          empresa_id,
          factura_cab_id: original.factura_cab_id,
          cliente_id: original.cliente_id,
          cobrador_id: original.cobrador_id,
          cuota_id: original.cuota_id,
          fecha_prometida: nuevaFecha,
          monto_prometido: dto.monto_prometido != null ? new Prisma.Decimal(dto.monto_prometido) : original.monto_prometido,
          estado: 'pendiente',
          notas: dto.notas,
          fecha_recordatorio: this.calcularFechaRecordatorio(nuevaFecha),
          creado_por: user_id,
          tipo_evidencia: (dto.tipo_evidencia as any) ?? original.tipo_evidencia,
          pagador_tipo: original.pagador_tipo,
          pagador_cliente_id: original.pagador_cliente_id,
          promesa_origen_id: original.id,
        },
      });

      if (original.facturas_objetivo?.length) {
        await tx.cob_promesa_factura.createMany({
          data: original.facturas_objetivo.map((f) => ({
            promesa_pago_id: nueva.id,
            factura_cab_id: f.factura_cab_id,
            monto_asignado: f.monto_asignado,
          })),
        });
      }

      try {
        await this.auditService.log({
          empresa_id,
          user_id,
          action: 'RENEGOTIATE',
          entity_type: 'promesas_pago',
          entity_id: nueva.id,
          descripcion: `Renegociación de promesa ${original.id} → nueva ${nueva.id} | Nueva fecha: ${dto.fecha_prometida}`,
        });
      } catch (e) {
        this.logger.warn(`Audit fallo: ${(e as Error).message}`);
      }
      return nueva;
    });
  }

  /** Vigentes (pendiente/incumplida) por cliente — para banner en wizard de ventas */
  async getVigentesPorCliente(cliente_id: string, empresa_id: string) {
    const promesas = await this.prisma.promesas_pago.findMany({
      where: { empresa_id, cliente_id, estado: { in: ['pendiente', 'incumplida'] } },
      include: {
        factura: {
          select: {
            id: true,
            dest: true,
            dpunexp: true,
            dnumdoc: true,
            moneda: { select: { codigo: true } },
          },
        },
        facturas_objetivo: { select: { factura_cab_id: true, monto_asignado: true } },
      },
      orderBy: { fecha_prometida: 'asc' },
    });
    const incumplidas = promesas.filter((p) => p.estado === 'incumplida').length;
    const pendientes = promesas.filter((p) => p.estado === 'pendiente').length;
    return { promesas, pendientes, incumplidas, total: promesas.length };
  }

  /** Reporte de cumplimiento por cobrador */
  async getReporteCumplimiento(empresa_id: string, fecha_desde?: string, fecha_hasta?: string) {
    const where: Prisma.promesas_pagoWhereInput = { empresa_id };
    if (fecha_desde || fecha_hasta) {
      where.fecha_prometida = {};
      if (fecha_desde) (where.fecha_prometida as any).gte = new Date(fecha_desde);
      if (fecha_hasta) (where.fecha_prometida as any).lte = new Date(fecha_hasta);
    }

    const promesas = await this.prisma.promesas_pago.findMany({
      where,
      select: {
        id: true,
        estado: true,
        cobrador_id: true,
        monto_prometido: true,
        monto_cumplido: true,
        cobrador: { select: { id: true, nombre: true, apellido: true } },
      },
    });

    const porCobrador = new Map<string | null, any>();
    for (const p of promesas) {
      const key = p.cobrador_id ?? '__sin_cobrador__';
      if (!porCobrador.has(key)) {
        porCobrador.set(key, {
          cobrador_id: p.cobrador_id,
          cobrador_nombre: p.cobrador ? `${p.cobrador.nombre} ${p.cobrador.apellido ?? ''}`.trim() : '— Sin cobrador',
          total: 0,
          cumplidas: 0,
          cumplidas_parciales: 0,
          incumplidas: 0,
          canceladas: 0,
          renegociadas: 0,
          pendientes: 0,
          monto_prometido: 0,
          monto_cumplido: 0,
        });
      }
      const row = porCobrador.get(key);
      row.total += 1;
      row.monto_prometido += Number(p.monto_prometido ?? 0);
      row.monto_cumplido += Number(p.monto_cumplido ?? 0);
      switch (p.estado) {
        case 'cumplida':           row.cumplidas += 1; break;
        case 'cumplida_parcial':   row.cumplidas_parciales += 1; break;
        case 'incumplida':         row.incumplidas += 1; break;
        case 'cancelada':          row.canceladas += 1; break;
        case 'renegociada':        row.renegociadas += 1; break;
        case 'pendiente':          row.pendientes += 1; break;
      }
    }

    const rows = Array.from(porCobrador.values()).map((r) => ({
      ...r,
      tasa_cumplimiento: r.total > 0 ? ((r.cumplidas + r.cumplidas_parciales) / r.total) * 100 : 0,
    }));
    return { total_promesas: promesas.length, por_cobrador: rows };
  }

  /** Dashboard cashflow: promesas pendientes agrupadas por fecha futura */
  async getDashboardCashflow(empresa_id: string) {
    const hoy = new Date();
    hoy.setHours(0, 0, 0, 0);

    const pendientes = await this.prisma.promesas_pago.findMany({
      where: { empresa_id, estado: 'pendiente', fecha_prometida: { gte: hoy } },
      select: { fecha_prometida: true, monto_prometido: true },
      orderBy: { fecha_prometida: 'asc' },
    });

    const porFecha = new Map<string, number>();
    for (const p of pendientes) {
      const key = p.fecha_prometida.toISOString().split('T')[0];
      porFecha.set(key, (porFecha.get(key) ?? 0) + Number(p.monto_prometido ?? 0));
    }
    const proyeccion = Array.from(porFecha.entries()).map(([fecha, monto]) => ({ fecha, monto }));
    const total = proyeccion.reduce((s, r) => s + r.monto, 0);
    return { total_pendiente: total, total_promesas: pendientes.length, proyeccion };
  }

  /**
   * Hook: cuando se registra un cobro, marcar promesas vigentes como cumplidas/parciales.
   *
   * Aplica a promesas `pendiente` **e `incumplida`**: el job de las 08:00 marca
   * incumplida toda promesa vencida, y en mora lo normal es que el cliente pague
   * después de la fecha prometida. Sin esto, un pago tardío nunca cerraba la
   * promesa y quedaba "incumplida" con la cuota saldada. Que fue con atraso se
   * deduce de `fecha_cumplimiento > fecha_prometida`.
   *
   * Reglas:
   * - Si la promesa está atada a una `cuota_id`: se compara el saldo actual de esa
   *   cuota (post-cobro). Saldo 0 → `cumplida`; saldo < monto_prometido original
   *   (algo se cobró) → `cumplida_parcial`; si nada cambió, queda pendiente.
   * - Sin `cuota_id` (promesa con objetivos en `cob_promesa_factura`): se suma lo
   *   cobrado en recibos emitidos sobre las cuotas objetivo desde que se registró
   *   la promesa. Acumulado ≥ prometido → `cumplida`; > 0 → `cumplida_parcial`. Una
   *   parcial se vuelve a evaluar en cada cobro hasta llegar a cumplida.
   */
  async cerrarPorCobro(params: {
    empresa_id: string;
    cliente_id: string;
    factura_cab_ids: string[];
    cuota_ids?: string[];
    recibo_cob_id?: string;
    monto_cobrado: number;
  }) {
    try {
      const { empresa_id, cliente_id, factura_cab_ids, cuota_ids, recibo_cob_id, monto_cobrado } = params;
      if (!factura_cab_ids?.length) return;

      const promesas = await this.prisma.promesas_pago.findMany({
        where: {
          empresa_id,
          cliente_id,
          estado: { in: ['pendiente', 'incumplida', 'cumplida_parcial'] },
          OR: [
            { factura_cab_id: { in: factura_cab_ids } },
            { facturas_objetivo: { some: { factura_cab_id: { in: factura_cab_ids } } } },
          ],
        },
      });
      if (!promesas.length) return;

      // Saldo actual por cuota (post-cobro). Solo necesario para promesas atadas a cuota.
      const cuotaIdsPromesas = promesas.filter((p) => p.cuota_id).map((p) => p.cuota_id as string);
      const saldosCuota = new Map<string, { saldo: number; monto: number }>();
      if (cuotaIdsPromesas.length) {
        const rows = await this.prisma.factura_cuotas.findMany({
          where: { id: { in: cuotaIdsPromesas } },
          select: { id: true, saldo_pendiente: true, dmoncuota: true },
        });
        for (const r of rows) {
          saldosCuota.set(r.id, {
            saldo: Number(r.saldo_pendiente ?? 0),
            monto: Number(r.dmoncuota ?? 0),
          });
        }
      }

      const hoy = new Date();
      let cumplidas = 0;
      let parciales = 0;

      for (const p of promesas) {
        const prometido = Number(p.monto_prometido ?? 0);
        let nuevoEstado: 'cumplida' | 'cumplida_parcial' | null = null;
        let montoCumplido = monto_cobrado;

        if (p.cuota_id) {
          // Una parcial por cuota ya quedó evaluada por saldo; sólo puede subir a cumplida.
          const info = saldosCuota.get(p.cuota_id);
          if (!info) continue;
          if (info.saldo <= 0) {
            nuevoEstado = 'cumplida';
            montoCumplido = info.monto;
          } else if (cuota_ids?.includes(p.cuota_id)) {
            // Se cobró sobre la cuota pero no la canceló del todo
            nuevoEstado = 'cumplida_parcial';
            montoCumplido = Math.max(0, info.monto - info.saldo);
          } else {
            continue; // la cuota no recibió cobro en este recibo
          }
        } else {
          const acumulado = await this.cobradoSobreObjetivos(p.id, p.factura_cab_id, p.created_at);
          if (acumulado <= 0) continue;
          nuevoEstado = prometido > 0 && acumulado < prometido ? 'cumplida_parcial' : 'cumplida';
          montoCumplido = acumulado;
        }
        if (nuevoEstado === p.estado) continue;

        await this.prisma.promesas_pago.update({
          where: { id: p.id },
          data: {
            estado: nuevoEstado,
            monto_cumplido: new Prisma.Decimal(montoCumplido),
            fecha_cumplimiento: hoy,
            cumplida_recibo_id: recibo_cob_id ?? null,
            updated_at: new Date(),
          },
        });
        if (nuevoEstado === 'cumplida') cumplidas += 1;
        else parciales += 1;
        this.logger.log(`Promesa ${p.id} cerrada por cobro → ${nuevoEstado}`);
      }

      if (cumplidas + parciales > 0) {
        try {
          await this.auditService.log({
            empresa_id,
            user_id: null as unknown as string,
            action: 'AUTO_CLOSE',
            entity_type: 'promesas_pago',
            entity_id: recibo_cob_id ?? '',
            descripcion: `Cierre automático por cobro | recibo ${recibo_cob_id} | cumplidas: ${cumplidas} | parciales: ${parciales}`,
          });
        } catch (e) {
          this.logger.warn(`Audit cerrarPorCobro fallo: ${(e as Error).message}`);
        }
      }
    } catch (e) {
      this.logger.warn(`cerrarPorCobro fallo: ${(e as Error).message}`);
    }
  }

  /**
   * Total cobrado (recibos emitidos) sobre las cuotas objetivo de una promesa desde
   * que se registró. Objetivos sin cuota cuentan todas las cuotas de esa factura;
   * una promesa sin objetivos cargados usa su factura principal.
   */
  private async cobradoSobreObjetivos(promesaId: string, facturaPrincipalId: string, desde: Date): Promise<number> {
    const objetivos = await this.prisma.cob_promesa_factura.findMany({
      where: { promesa_pago_id: promesaId },
      select: { factura_cab_id: true, cuota_id: true },
    });
    const cuotaIds = objetivos.map((o) => o.cuota_id).filter((id): id is string => !!id);
    const facturaIds = objetivos.filter((o) => !o.cuota_id).map((o) => o.factura_cab_id);
    if (!objetivos.length) facturaIds.push(facturaPrincipalId);

    const filtroCuota: Prisma.recibo_cobro_detalleWhereInput[] = [];
    if (cuotaIds.length) filtroCuota.push({ factura_cuota_id: { in: cuotaIds } });
    if (facturaIds.length) filtroCuota.push({ factura_cuotas: { factura_cab_id: { in: facturaIds } } });

    const agg = await this.prisma.recibo_cobro_detalle.aggregate({
      _sum: { monto_pagado: true },
      where: {
        ...(filtroCuota.length === 1 ? filtroCuota[0] : { OR: filtroCuota }),
        recibos_cobro: { estado: 'emitido', created_at: { gte: desde } },
      },
    });
    return Number(agg._sum.monto_pagado ?? 0);
  }

  /** Job: Marcar como incumplidas las promesas pendientes cuya fecha ya pasó */
  async marcarIncumplidas(): Promise<number> {
    const hoy = new Date();
    hoy.setHours(0, 0, 0, 0);
    const result = await this.prisma.promesas_pago.updateMany({
      where: { estado: 'pendiente', fecha_prometida: { lt: hoy } },
      data: { estado: 'incumplida', updated_at: new Date() },
    });
    if (result.count > 0) {
      this.logger.log(`${result.count} promesas marcadas como incumplidas (job automático)`);
    }
    return result.count;
  }

  /** Job: Obtener promesas con recordatorio hoy (para enviar Twilio) */
  async getPromesasParaRecordatorio(): Promise<any[]> {
    const hoy = new Date();
    const hoyStr = hoy.toISOString().split('T')[0];
    return this.prisma.promesas_pago.findMany({
      where: {
        estado: 'pendiente',
        recordatorio_enviado: false,
        fecha_recordatorio: new Date(hoyStr),
      },
      include: {
        cobrador: {
          select: {
            id: true,
            nombre: true,
            apellido: true,
            telefono: true,
            usuario: { select: { id: true, nombres: true } },
          },
        },
        cliente: { select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true } } } },
      },
    });
  }

  async marcarRecordatorioEnviado(id: string) {
    return this.prisma.promesas_pago.update({
      where: { id },
      data: { recordatorio_enviado: true, updated_at: new Date() },
    });
  }
}
