import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { AuditService } from '../audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import { TwilioService } from '../twilio/twilio.service';
import { AutorizacionesService } from './autorizaciones.service';
import { GestionMoraService } from './gestion-mora.service';
import { InteresesMoratoriosService } from './intereses-moratorios.service';
import { PromesasService } from './promesas.service';

/**
 * Claves de advisory lock de Postgres, una por job. Arbitrarias pero fijas: la
 * misma en todas las instancias del backend.
 */
const LOCK_AUTO_ABRIR_GESTIONES_MORA = 7_201_001;
const LOCK_EXPIRAR_AUTORIZACIONES = 7_201_002;
const LOCK_PROCESAR_PROMESAS = 7_201_003;
const LOCK_REINTENTAR_INTERESES = 7_201_004;

/** Tope de la transacción que sostiene el lock (la corrida inicial de mora abrió miles). */
const LOCK_TIMEOUT_MS = 30 * 60 * 1000;

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

  constructor(
    private readonly promesasService: PromesasService,
    private readonly autorizacionesService: AutorizacionesService,
    private readonly interesesMoratoriosService: InteresesMoratoriosService,
    private readonly gestionMoraService: GestionMoraService,
    private readonly twilioService: TwilioService,
    private readonly auditService: AuditService,
    private readonly prisma: PrismaService,
  ) {}

  /**
   * Corre `fn` sólo si esta instancia obtiene el advisory lock `clave` en la DB.
   *
   * Con más de una instancia del backend (pm2 cluster, watch que dejó un proceso
   * viejo vivo) cada @Cron dispara en todas a la vez: la auto-apertura de mora
   * abría gestiones duplicadas con milisegundos de diferencia, y los otros jobs
   * mandarían SMS o emitirían facturas dos veces. El árbitro es Postgres, así
   * que también cubre instancias en hosts distintos. El lock vive mientras dure
   * la transacción que lo sostiene; `fn` trabaja con `this.prisma` normalmente.
   */
  private async conLockExclusivo(clave: number, nombreJob: string, fn: () => Promise<unknown>) {
    await this.prisma.$transaction(
      async (tx) => {
        const [{ locked }] = await tx.$queryRaw<{ locked: boolean }[]>`
          SELECT pg_try_advisory_xact_lock(${clave}) AS locked
        `;
        if (!locked) {
          this.logger.warn(`Job ${nombreJob}: otra instancia ya lo está corriendo, se omite.`);
          return;
        }
        await fn();
      },
      { timeout: LOCK_TIMEOUT_MS },
    );
  }

  /**
   * Diariamente a las 06:00: abre GESTION_INTERNA para clientes con cuotas
   * vencidas por más del umbral (default 30 días o `clientes.dias_mora_maximo`).
   * Idempotente: omite clientes que ya tienen una gestión activa.
   */
  @Cron('0 0 6 * * *')
  async autoAbrirGestionesMora() {
    try {
      await this.conLockExclusivo(LOCK_AUTO_ABRIR_GESTIONES_MORA, 'autoAbrirGestionesMora', async () => {
        const r = await this.gestionMoraService.autoAbrirGestionesVencidas();
        this.logger.log(`Job autoAbrirGestionesMora: ${r.abiertas} gestiones abiertas, ${r.errores.length} errores`);
      });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      this.logger.error(`Error en job autoAbrirGestionesMora: ${msg}`, err instanceof Error ? err.stack : undefined);
    }
  }

  /** Cada 15 minutos: expirar autorizaciones de descuento vencidas */
  @Cron('0 */15 * * * *')
  async expirarAutorizaciones() {
    try {
      await this.conLockExclusivo(LOCK_EXPIRAR_AUTORIZACIONES, 'expirarAutorizaciones', () =>
        this.autorizacionesService.expirarVencidas(),
      );
    } catch (err) {
      this.logger.error('Error en job expirarAutorizaciones', err);
    }
  }

  /** Diariamente a las 08:00: marcar promesas incumplidas y enviar recordatorios Twilio */
  @Cron('0 8 * * *')
  async procesarPromesas() {
    try {
      await this.conLockExclusivo(LOCK_PROCESAR_PROMESAS, 'procesarPromesas', () => this.enviarRecordatoriosPromesas());
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      this.logger.error(`Error en job procesarPromesas: ${msg}`, err instanceof Error ? err.stack : undefined);
    }
  }

  private async enviarRecordatoriosPromesas() {
    // 1. Marcar promesas vencidas como incumplidas
    await this.promesasService.marcarIncumplidas();

    // 2. Enviar recordatorios del día
    const promesasHoy = await this.promesasService.getPromesasParaRecordatorio();

    for (const promesa of promesasHoy) {
      const telefono = promesa.cobrador?.telefono;
      if (!telefono) {
        this.logger.warn(
          `Cobrador ${promesa.cobrador_id} sin teléfono — no se envió recordatorio para promesa ${promesa.id}`,
        );
        continue;
      }

      const nombreCliente = promesa.cliente?.nombre_fantasia || promesa.cliente?.personas?.razon_social || 'Cliente';

      const nombreCobrador = `${promesa.cobrador?.nombre || ''} ${promesa.cobrador?.apellido || ''}`.trim();

      const fechaStr = new Date(promesa.fecha_prometida).toLocaleDateString('es-PY', {
        day: '2-digit',
        month: '2-digit',
        year: 'numeric',
      });

      const mensaje =
        `Recordatorio: ${nombreCobrador}, el cliente ${nombreCliente} prometió pagar mañana ${fechaStr}` +
        (promesa.monto_prometido ? ` por Gs. ${Number(promesa.monto_prometido).toLocaleString('es-PY')}` : '') +
        `. Verifique el cobro. — Novasis PY`;

      try {
        await this.twilioService.sendSMS(telefono, mensaje);
        await this.promesasService.marcarRecordatorioEnviado(promesa.id);
        this.logger.log(`Recordatorio enviado a ${telefono} para promesa ${promesa.id}`);
      } catch (smsErr) {
        this.logger.error(`Error enviando SMS a ${telefono} para promesa ${promesa.id}`, smsErr);
      }
    }

    this.logger.log(`Job procesarPromesas: ${promesasHoy.length} recordatorios procesados`);
  }

  /**
   * Fase A.3.8 — Cada 30 minutos: reintentar emisión SIFEN de facturas por
   * intereses moratorios que quedaron con error.
   * Limita por intento (max 5) para no loopear sobre errores permanentes;
   * cuando se agotan, queda como evidencia para soporte y se requiere
   * intervención manual del operador.
   */
  @Cron('0 */30 * * * *')
  async reintentarEmisionInteresesMoratorios() {
    try {
      await this.conLockExclusivo(LOCK_REINTENTAR_INTERESES, 'reintentarEmisionInteresesMoratorios', () =>
        this.reintentarEmisionIntereses(),
      );
    } catch (err) {
      this.logger.error('Error en job reintentarEmisionInteresesMoratorios', err);
    }
  }

  private async reintentarEmisionIntereses() {
    const pendientes = await this.interesesMoratoriosService.listarRecibosConEmisionPendiente(5, 50);
    if (pendientes.length === 0) return;

    let exitosos = 0;
    let fallidos = 0;
    const detalleFallos: Array<{ recibo_cobro_id: string; error: string }> = [];
    for (const fila of pendientes) {
      try {
        const res = await this.interesesMoratoriosService.emitirFacturaParaRecibo(
          fila.empresa_id,
          fila.recibo_cobro_id,
          null,
        );
        if (res.factura_cab_id) {
          exitosos++;
          await this.auditService.log({
            empresa_id: fila.empresa_id,
            action: 'RETRY_EMIT_OK',
            entity_type: 'cob_interes_cobrado',
            entity_id: fila.recibo_cobro_id,
            new_value: { factura_cab_id: res.factura_cab_id },
            descripcion: `Job retry: emisión SIFEN de factura por intereses moratorios exitosa para recibo ${fila.recibo_cobro_id}`,
          });
        } else {
          fallidos++;
        }
      } catch (err) {
        fallidos++;
        const msg = err instanceof Error ? err.message : String(err);
        detalleFallos.push({ recibo_cobro_id: fila.recibo_cobro_id, error: msg });
        this.logger.error(`Retry emisión intereses falló para recibo ${fila.recibo_cobro_id}: ${msg}`);
        await this.auditService.log({
          empresa_id: fila.empresa_id,
          action: 'RETRY_EMIT_FAIL',
          entity_type: 'cob_interes_cobrado',
          entity_id: fila.recibo_cobro_id,
          new_value: { error: msg },
          descripcion: `Job retry: emisión SIFEN falló para recibo ${fila.recibo_cobro_id}`,
        });
      }
    }
    this.logger.log(
      `Job retry emisión intereses: ${exitosos} OK / ${fallidos} fallidos sobre ${pendientes.length} recibos`,
    );
  }
}
