import { sincronizarCuentaCobrar, type TxSincronizacion } from 'src/common/utils/sincronizar-cuenta.util';
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createCipheriv, createHash, randomBytes } from 'crypto';
import { AuditService } from 'src/audit/audit.service';
import { envs } from 'src/config';
import { PrismaService } from 'src/prisma/prisma.service';
import { ContabilidadIntegracionService } from 'src/contabilidad/services/integracion.service';
import { CreateMiddlewareSifenDto } from './dto/create-middleware-sifen.dto';
import { EstadoFactura, EstadoSifen } from 'src/facturas/facturas.enums';
import { AsignacionFacturasService } from 'src/vendedores-cobradores/asignacion-facturas.service';
import { LotesService } from 'src/lotes/lotes.service';
import { ReversionCajaService } from 'src/tesoreria/reversion-caja.service';
import { AlertasEmpresaService } from 'src/alertas-empresa/alertas-empresa.service';
import { ReversionRemisionService } from 'src/nota-remision/reversion-remision.service';
import { alertaDeEvento, claveAlertaEvento } from './alerta-evento-sifen';
import { revertirNcDeCuotas } from 'src/nota-creditos/aplicacion-nc';
import {
  clasificarEventoPendiente,
  UMBRAL_EVENTO_MS,
  type CasoEventoPendiente,
  type RespuestaConsultaEvento,
} from './clasificar-evento-pendiente';

export type TipoDocumento = 'factura' | 'nota_credito' | 'nota_remision' | 'nota_debito';
export type TipoEvento = 'inutilizacion' | 'cancelacion' | 'nominacion';

const MENSAJE_CASO_EVENTO: Record<CasoEventoPendiente, string> = {
  respondido: 'SIFEN respondió el evento; el documento quedó actualizado.',
  en_espera: 'El evento sigue esperando respuesta de SIFEN.',
  sin_respuesta: 'El evento está en el middleware pero SIFEN no respondió en más de una hora.',
  no_registrado: 'El middleware no tiene registrado el evento: no llegó a SIFEN.',
  middleware_falla: 'No se pudo consultar el evento en el middleware.',
  incoherente: 'El evento pendiente no es coherente con el estado del documento.',
};

/** Lo que devuelve el middleware por cada evento consultado. */
type RegistroEventoMiddleware = {
  fecha_envio?: string;
  fecha_firma?: string;
  estado_evento?: string;
  retorno_sifen?: {
    estado: string;
    mensaje: string;
    protocolo: string;
    codrespuesta: string;
    fecha_retorno: string;
  } | null;
  fecha_registro?: string;
};

const DOCUMENTO_CONFIG: Record<TipoDocumento, { iTiDE: number; label: string }> = {
  factura: { iTiDE: 1, label: 'Factura' },
  nota_credito: { iTiDE: 5, label: 'Nota de Crédito' },
  nota_remision: { iTiDE: 7, label: 'Nota de Remisión' },
  nota_debito: { iTiDE: 6, label: 'Nota de Débito' },
};

type DocConsultaEvento = { cdc?: string | null; dest?: string | null; dpunexp?: string | null; dnumdoc?: string | null; evento_aplicado?: string | null };

/**
 * Payload de POST /api/maintenance para consultar el estado de un evento
 * (formato real del middleware, captura de producción 2026-09-16). ECAN/EINO
 * identifican el documento por CDC; EINU por rango de numeración (el
 * documento inutilizado no tiene CDC en SIFEN), en ambos casos con el iTiDE
 * del tipo de documento.
 */
function payloadConsultaEvento(doc: DocConsultaEvento, iTiDE: number) {
  const esInutilizacion = doc.evento_aplicado === 'EINU';
  return {
    tipOpe: '2',
    tipo: 'evento',
    fecha_inicial: null,
    fecha_final: null,
    nro_lote: null,
    iTiDE,
    dEst: esInutilizacion ? doc.dest : null,
    dPunExp: esInutilizacion ? doc.dpunexp : null,
    dNumIn: esInutilizacion ? doc.dnumdoc : null,
    dNumFin: esInutilizacion ? doc.dnumdoc : null,
    cdc: esInutilizacion ? null : doc.cdc,
  };
}

/**
 * Claves bajo las que el middleware devuelve el registro en `response`: el CDC
 * para ECAN/EINO y "dEst-dPunExp-dNumIn/dNumFin" para EINU. La forma sin
 * "/dNumFin" se conserva por si alguna versión la usa.
 */
function clavesRegistroEvento(doc: DocConsultaEvento): string[] {
  const rango = `${doc.dest}-${doc.dpunexp}-${doc.dnumdoc}`;
  return [doc.cdc, `${rango}/${doc.dnumdoc}`, rango].filter((k): k is string => !!k);
}

@Injectable()
export class MiddlewareSifenService {
  private readonly logger = new Logger(MiddlewareSifenService.name);
  constructor(
    private readonly configService: ConfigService,
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
    private readonly contabilidadIntegracion: ContabilidadIntegracionService,
    private readonly asignacionFacturas: AsignacionFacturasService,
    // Para devolver los lotes consumidos por FIFO al cancelar (ECAN/EINU).
    private readonly lotesService: LotesService,
    // Para revertir los movimientos de caja de la factura cancelada/inutilizada.
    private readonly reversionCaja: ReversionCajaService,
    // Eventos SIFEN inconsistentes → alertas de empresa (docs/plan-alertas-empresa.md).
    private readonly alertasEmpresa: AlertasEmpresaService,
    // Remisión cancelada/inutilizada: repone stock y libera lo aplicado a facturas.
    private readonly reversionRemision: ReversionRemisionService,
  ) {}

  /**
   * Registra un evento de sincronización SIFEN en audit_log para trazabilidad.
   * Nunca lanza — falla silenciosa para no interrumpir el flujo de sync.
   */
  private async auditSifenEvent(params: {
    empresaId: string;
    tipoDocumento: TipoDocumento;
    documentoId: string;
    numero?: string | null;
    action:
      | 'sifen_sync_documento'
      | 'sifen_sync_evento'
      | 'sifen_integracion_contable'
      | 'sifen_reversion_contable'
      | 'sifen_reversion_nc'
      | 'sifen_reversion_remision';
    resultado: 'OK' | 'ERROR';
    detalle: string;
    extra?: Record<string, unknown>;
  }): Promise<void> {
    try {
      await this.auditService.log({
        empresa_id: params.empresaId,
        action: 'UPDATE',
        entity_type: `${params.tipoDocumento}_${params.action}`,
        entity_id: params.documentoId,
        descripcion: `[${params.resultado}] ${params.detalle}${params.numero ? ` | ${params.numero}` : ''}`,
        new_value: { resultado: params.resultado, detalle: params.detalle, numero: params.numero, ...params.extra },
      });
    } catch (err) {
      this.logger.warn(
        `No se pudo registrar audit SIFEN ${params.action}: ${err instanceof Error ? err.message : err}`,
        'MiddlewareSifenService',
      );
    }
  }

  /**
   * Revierte los efectos de saldo que la NC aplicó sobre su factura asociada.
   * Inline aquí (no se importa NotaCreditosService) para evitar ciclo de módulos:
   * MiddlewareSifen → NotaCreditos → Queues → MiddlewareSifen.
   */
  // ════════════════════════════════════════════════════════════════════════
  // Bypass SIFEN para empresas con `sifen_activo=false`.
  // Marca el documento como Aprobado/Cancelado localmente, sin enviarlo
  // al middleware. Dispara los mismos side-effects post-aprobación que el
  // flujo normal (contabilización).
  // ════════════════════════════════════════════════════════════════════════

  /** Aprueba una factura localmente sin enviarla a SIFEN. */
  async aprobarFacturaSinSifen(facturaId: string): Promise<void> {
    const ahora = new Date();
    await this.prisma.factura_cab.update({
      where: { id: facturaId },
      data: {
        estado: 'Aprobado',
        estado_sifen: 'Aprobado',
        mensaje_sifen: 'Aprobado localmente — empresa sin facturación electrónica',
        tipo_aprobacion: 'MANUAL',
        fecha_envio_sifen: ahora,
        fecha_firma_sifen: ahora,
        fecha_registro_sifen: ahora,
      },
    });

    this.contabilidadIntegracion
      .integrarFacturaVenta(facturaId)
      .catch((err) =>
        this.logger.error(
          `Error contabilizando factura (manual) ${facturaId}: ${err instanceof Error ? err.message : err}`,
          'MiddlewareSifenService',
        ),
      );
  }

  /** Aprueba una NC localmente sin enviarla a SIFEN. */
  async aprobarNCSinSifen(ncId: string): Promise<void> {
    const ahora = new Date();
    await this.prisma.nota_credito_cab.update({
      where: { id: ncId },
      data: {
        estado: 'Aprobado',
        estado_sifen: 'Aprobado',
        mensaje_sifen: 'Aprobado localmente — empresa sin facturación electrónica',
        tipo_aprobacion: 'MANUAL',
        fecha_envio_sifen: ahora,
        fecha_firma_sifen: ahora,
        fecha_registro_sifen: ahora,
      },
    });
    // La aplicación de la NC contra la factura se dispara en el create del módulo
    // de notas de crédito (no es side-effect de la aprobación SIFEN como en facturas).
  }

  /**
   * Resuelve si una empresa opera en modo SIFEN o en modo manual local.
   * Centraliza la consulta del flag para que cada caller no lo duplique.
   */
  async empresaUsaSifen(empresaId: string): Promise<boolean> {
    const e = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { sifen_activo: true },
    });
    return e?.sifen_activo !== false;
  }

  /**
   * Deshace la marca de reversion cuando una NC previamente rechazada termina aprobada.
   *
   * Solo toca NC que siguen marcadas como reversadas: si nunca se reverso, no hace nada.
   * El saldo vuelve a NULL en vez de a un monto fijo porque el sistema lo deriva de los
   * subtotales de la NC cuando esta en NULL -- asi no se congela un valor que despues
   * quede desactualizado.
   *
   * Para NC en modo ESTRICTO la reversion ademas deshizo la aplicacion contra las cuotas
   * de la factura. Esas no se re-aplican solas: se dejan disponibles para aplicarlas
   * desde un recibo, que es explicito y auditable.
   */
  private async restaurarNcTrasAprobacion(notaCreditoId: string): Promise<void> {
    const nc = await this.prisma.nota_credito_cab.findUnique({
      where: { id: notaCreditoId },
      select: { id: true, rec_aplicacion_reversada: true },
    });
    if (!nc?.rec_aplicacion_reversada) return;

    await this.prisma.nota_credito_cab.update({
      where: { id: notaCreditoId },
      data: { rec_saldo_disponible: null, rec_aplicacion_reversada: false } as any,
    });
    this.logger.log(
      `NC ${notaCreditoId}: se limpio la marca de reversion tras aprobacion de SIFEN`,
      'MiddlewareSifenService',
    );
  }

  private async revertirAplicacionNC(notaCreditoId: string): Promise<void> {
    const nc = await this.prisma.nota_credito_cab.findUnique({
      where: { id: notaCreditoId },
      select: { id: true, factura_cab_id: true, rec_aplicacion_reversada: true as any } as any,
    });
    if (!nc || !(nc as any).factura_cab_id) return;
    if ((nc as any).rec_aplicacion_reversada) return;

    const facturaId = (nc as any).factura_cab_id as string;
    const subtotales = await this.prisma.nota_credito_subtotal.findMany({
      where: { nota_credito_cab_id: notaCreditoId },
      select: { dtotgralope: true },
    });
    const montoNC = subtotales.reduce((acc, s) => acc + Number(s.dtotgralope || 0), 0);

    const items = await this.prisma.nota_credito_det.findMany({
      where: { nota_credito_cab_id: notaCreditoId },
      select: { producto_id: true, dcantproser: true },
    });

    await this.prisma.$transaction(async (tx) => {
      const fac = await tx.factura_cab.findUnique({ where: { id: facturaId } });
      if (fac) {
        const totalNCActual = Number((fac as any).total_notas_credito) || 0;
        const totalFactura = Number(fac.total_factura) || 0;
        const nuevoTotalNC = Math.max(0, totalNCActual - montoNC);
        await tx.factura_cab.update({
          where: { id: fac.id },
          data: {
            total_notas_credito: nuevoTotalNC,
            saldo_disponible: totalFactura - nuevoTotalNC,
            updated_at: new Date(),
          } as any,
        });
      }

      for (const item of items) {
        const det = await tx.factura_det.findFirst({
          where: { factura_cab_id: facturaId, producto_id: item.producto_id },
        });
        if (!det) continue;
        const cantidadNCActual = Number((det as any).cantidad_nc_aplicada) || 0;
        const cantidadOriginal = Number(det.dcantproser) || 0;
        const cantidadNC = Number(item.dcantproser) || 0;
        const nuevoNCAplicada = Math.max(0, cantidadNCActual - cantidadNC);
        await tx.factura_det.update({
          where: { id: det.id },
          data: {
            cantidad_nc_aplicada: nuevoNCAplicada,
            cantidad_disponible: cantidadOriginal - nuevoNCAplicada,
            updated_at: new Date(),
          } as any,
        });
      }

      // Lo que la NC había descontado de las cuotas vuelve a la factura. Sin esto
      // la factura quedaba con menos deuda que la real y, si la NC se reenviaba
      // y SIFEN la aprobaba, volvía a ofrecerse entera desde el recibo.
      await revertirNcDeCuotas(tx, notaCreditoId);

      // La deuda volvió a las cuotas: la cuenta y el cliente tienen que reflejarla.
      const ncRevertida = await tx.nota_credito_cab.findUnique({
        where: { id: notaCreditoId },
        select: { factura_cab_id: true },
      });
      if (ncRevertida?.factura_cab_id) {
        await sincronizarCuentaCobrar(tx as unknown as TxSincronizacion, ncRevertida.factura_cab_id);
      }

      await tx.nota_credito_cab.update({
        where: { id: notaCreditoId },
        data: { rec_saldo_disponible: 0, rec_aplicacion_reversada: true } as any,
      });
    });

    // Restaurar comisiones tras la reversión — la factura recuperó neto y las
    // comisiones asociadas deben recalcularse (o reactivarse si estaban en 0).
    const facturaCabId = (nc as any).factura_cab_id as string;
    const empresaId = (
      await this.prisma.factura_cab.findUnique({
        where: { id: facturaCabId },
        select: { empresa_id: true },
      })
    )?.empresa_id;
    if (empresaId) {
      this.asignacionFacturas
        .recalcularComisionesPorNC(facturaCabId, empresaId)
        .catch((err: Error) =>
          this.logger.error?.(
            `Error recalculando comisiones al revertir NC ${notaCreditoId}: ${err.message}`,
          ),
        );
    }

    this.logger.log(
      `NC ${notaCreditoId} aplicación reversada (monto ${montoNC}, items ${items.length})`,
      'MiddlewareSifenService',
    );
  }

  private getPrismaModel(tipoDocumento: TipoDocumento) {
    const models: Record<TipoDocumento, any> = {
      factura: this.prisma.factura_cab,
      nota_credito: this.prisma.nota_credito_cab,
      nota_remision: this.prisma.nota_remision_cab,
      nota_debito: this.prisma.nota_debito_cab,
    };
    return models[tipoDocumento];
  }

  async enviarEvento(
    empresaId: string,
    tipoDocumento: TipoDocumento,
    tipoEvento: TipoEvento,
    documentoId: string,
    motivo?: string,
    datosCliente?: any,
  ): Promise<{ success: boolean; message: string; data?: unknown }> {
    const prismaModel = this.getPrismaModel(tipoDocumento);
    const config = DOCUMENTO_CONFIG[tipoDocumento];

    const documento = await prismaModel.findFirst({
      where: { id: documentoId, empresa_id: empresaId },
    });

    if (!documento) {
      return { success: false, message: `${config.label} no encontrado` };
    }

    // `nota_debito_cab` no tiene columnas de evento: enviarlo al middleware y
    // después fallar al guardar dejaba un evento sin rastro del lado nuestro.
    if (tipoDocumento === 'nota_debito') {
      return { success: false, message: 'Los eventos SIFEN no están soportados para notas de débito' };
    }

    // Un evento a la vez por documento: mientras SIFEN no responda el anterior,
    // mandar otro (el mismo reintentado o uno distinto) duplica el evento en
    // SIFEN. El `Pendiente` lo resuelve el sync de eventos (sifen-sync.processor).
    if (documento.estado_evento === 'Pendiente') {
      return { success: false, message: this.mensajeEventoPendiente(config.label, documento.evento_aplicado) };
    }

    // Validaciones según tipo de evento
    if (tipoEvento === 'inutilizacion' && documento.estado_sifen !== 'Rechazado') {
      return {
        success: false,
        message: 'Solo se pueden inutilizar documentos rechazados',
      };
    }
    if (tipoEvento === 'cancelacion' && documento.estado_sifen !== 'Aprobado') {
      return {
        success: false,
        message: 'Solo se pueden cancelar documentos aprobados',
      };
    }

    // ───────────────────────────────────────────────────────────────────────
    // Bypass para empresas con sifen_activo = false (sin facturación
    // electrónica). Solo soportamos cancelación local; inutilización y
    // nominación no aplican en este modo.
    // ───────────────────────────────────────────────────────────────────────
    const usaSifen = await this.empresaUsaSifen(empresaId);
    if (!usaSifen) {
      if (tipoEvento !== 'cancelacion') {
        return {
          success: false,
          message:
            'La empresa no tiene facturación electrónica activa: solo se admite cancelación, no inutilización ni nominación.',
        };
      }
      try {
        if (tipoDocumento === 'factura') {
          await this.prisma.factura_cab.update({
            where: { id: documentoId },
            data: {
              // Estado interno: Anulado (consistente con el resto del ERP).
              // estado_sifen='Cancelado' refleja el evento ECAN aprobado.
              estado: EstadoFactura.ANULADA,
              estado_sifen: EstadoSifen.CANCELADO,
              evento_aplicado: 'ECAN',
              estado_evento: 'Aprobado',
              mensaje_evento: motivo || 'Cancelación local (sin SIFEN)',
              fecha_evento: new Date(),
            },
          });
          await this.revertirStockFactura(documentoId, empresaId);
          await this.revertirOrdenVentaDesdeFactura(documentoId, empresaId);
          await this.reversionCaja
            .revertirCajaFactura(documentoId, empresaId, motivo || 'Cancelación local (sin SIFEN)')
            .catch((err) =>
              this.logger.error(
                `Error revirtiendo caja factura ${documentoId}: ${err.message}`,
                'MiddlewareSifenService',
              ),
            );
        } else if (tipoDocumento === 'nota_credito') {
          await this.prisma.nota_credito_cab.update({
            where: { id: documentoId },
            data: {
              estado: EstadoFactura.ANULADA,
              estado_sifen: EstadoSifen.CANCELADO,
              evento_aplicado: 'ECAN',
              estado_evento: 'Aprobado',
              mensaje_evento: motivo || 'Cancelación local (sin SIFEN)',
              fecha_evento: new Date(),
            },
          });
          await this.revertirAplicacionNC(documentoId);
          await this.revertirStockNC(documentoId, empresaId);
        } else if (tipoDocumento === 'nota_remision') {
          // `estado: 'Anulado'` lo pone la reversión (idempotente sobre ese campo).
          await this.prisma.nota_remision_cab.update({
            where: { id: documentoId },
            data: {
              estado_sifen: EstadoSifen.CANCELADO,
              evento_aplicado: 'ECAN',
              estado_evento: 'Aprobado',
              mensaje_evento: motivo || 'Cancelación local (sin SIFEN)',
              fecha_evento: new Date(),
            },
          });
          await this.reversionRemision.revertirRemision(documentoId, empresaId);
        } else {
          return {
            success: false,
            message: `Cancelación local no soportada para ${tipoDocumento}`,
          };
        }
        await this.auditService.log({
          empresa_id: empresaId,
          action: 'UPDATE',
          entity_type: tipoDocumento,
          entity_id: documentoId,
          descripcion: `Cancelación local (sin SIFEN) | Empresa con sifen_activo=false${motivo ? ` | Motivo: ${motivo}` : ''}`,
          new_value: { evento: 'cancelacion', eventoCode: 'ECAN', estado_evento: 'Aprobado' },
        });
        this.logger.log(
          `Cancelación local aplicada: ${tipoDocumento}:${documentoId} | Empresa: ${empresaId}`,
          'MiddlewareSifenService',
        );
        return {
          success: true,
          message: 'Cancelación aplicada localmente (empresa sin SIFEN)',
        };
      } catch (err) {
        const msg = err instanceof Error ? err.message : 'Error desconocido';
        this.logger.error(
          `Error en cancelación local ${tipoDocumento}:${documentoId} — ${msg}`,
          'MiddlewareSifenService',
        );
        return { success: false, message: `Error al cancelar localmente: ${msg}` };
      }
    }
    if (tipoEvento === 'nominacion') {
      if (tipoDocumento !== 'factura') {
        return {
          success: false,
          message: 'La nominación solo aplica para facturas',
        };
      }
      if (documento.estado_sifen !== 'Aprobado') {
        return {
          success: false,
          message: 'Solo se pueden nominar documentos aprobados',
        };
      }
    }

    // Mapeo de evento a código SIFEN
    const eventoCode = tipoEvento === 'inutilizacion' ? 'EINU' : tipoEvento === 'cancelacion' ? 'ECAN' : 'EINO';
    const enviarCdc = eventoCode !== 'EINU';

    // La inutilización va por rango de numeración y SIFEN identifica el rango
    // por timbrado: sin dNumTim el evento queda pendiente para siempre.
    let timbrado: number | null = null;
    if (eventoCode === 'EINU') {
      timbrado = await this.timbradoParaEvento(empresaId, documento.cdc as string | null);
      if (!timbrado) {
        return {
          success: false,
          message: 'No se encontró el timbrado del documento ni un timbrado vigente de la empresa: no se puede inutilizar',
        };
      }
    }

    // Enviar evento al middleware usando el método existente
    const payload: Record<string, unknown> = {
      tipOpe: '3',
      nro_lote: String('novasispy-event-' + Date.now()),
      iTiDE: config.iTiDE,
      tipoEve: eventoCode,
      comprobantes: [
        {
          ...(eventoCode === 'EINU' ? { dNumTim: timbrado } : {}),
          dEst: documento.dest ?? null,
          dPunExp: documento.dpunexp ?? null,
          dNumIn: documento.dnumdoc ?? null,
          dNumFin: documento.dnumdoc ?? null,
          cdc: enviarCdc ? documento.cdc : null,
          mOtEve: motivo || 'Sin motivo especificado',
        },
      ],
    };
    // No loguear el payload completo (contiene RUC/datos del cliente).
    this.logger.debug?.(`Evento SIFEN preparado: ${tipoEvento}`);

    if (tipoEvento === 'nominacion' && datosCliente) {
      payload.datosCliente = datosCliente;
    }

    // Reserva atómica ANTES de llamar al middleware: sólo uno de dos envíos
    // simultáneos (doble clic, dos pestañas) consigue marcar el documento como
    // Pendiente; el otro no toca SIFEN. El chequeo de arriba no alcanza solo
    // porque ambos pueden leer el documento antes de que el primero lo marque.
    const reservado = await prismaModel.updateMany({
      where: {
        id: documentoId,
        empresa_id: empresaId,
        OR: [{ estado_evento: null }, { estado_evento: { not: 'Pendiente' } }],
      },
      data: {
        evento_aplicado: eventoCode,
        estado_evento: 'Pendiente',
        mensaje_evento: motivo || null,
        fecha_evento: new Date(),
      },
    });
    if (reservado.count === 0) {
      return { success: false, message: this.mensajeEventoPendiente(config.label, eventoCode) };
    }

    let enviado = false;
    try {
      const response = await this.sendEventoToMiddleware(empresaId, payload);
      enviado = true;
      if (tipoEvento === 'nominacion' && datosCliente) {
        await prismaModel.update({
          where: { id: documentoId },
          data: { cliente_nominado_id: datosCliente.clienteId },
        });
      }
      this.logger.log(
        `Evento SIFEN enviado: ${tipoEvento} | ${tipoDocumento}: ${documentoId} | Empresa: ${empresaId}`,
        'MiddlewareSifenService',
      );
      await this.auditService.log({
        empresa_id: empresaId,
        action: 'UPDATE',
        entity_type: tipoDocumento,
        entity_id: documentoId,
        descripcion: `Evento SIFEN ${tipoEvento} enviado | Código: ${eventoCode}${motivo ? ` | Motivo: ${motivo}` : ''}`,
        new_value: { evento: tipoEvento, eventoCode, estado_evento: 'Pendiente' },
      });

      // Para cancelación/inutilización de facturas: esperar resultado y revertir stock
      if (tipoDocumento === 'factura' && (eventoCode === 'ECAN' || eventoCode === 'EINU')) {
        const resultado = await this.esperarYConsultarEstado(
          empresaId,
          documentoId,
          { cdc: documento.cdc, dest: documento.dest, dpunexp: documento.dpunexp, dnumdoc: documento.dnumdoc },
          eventoCode,
        );
        return {
          success: resultado.estado === 'Aprobado',
          message: resultado.mensaje,
          data: { response, estadoEvento: resultado },
        };
      }

      return {
        success: true,
        message: 'Evento enviado correctamente',
        data: response,
      };
    } catch (error) {
      // Falló el envío al middleware: se libera la reserva devolviendo el
      // documento a como estaba, para que el usuario pueda reintentar. Si el
      // error vino DESPUÉS de que el middleware aceptó el evento, el Pendiente
      // se mantiene: liberarlo permitiría duplicarlo.
      if (!enviado) {
        await prismaModel
          .update({
            where: { id: documentoId },
            data: {
              evento_aplicado: documento.evento_aplicado ?? null,
              estado_evento: documento.estado_evento ?? null,
              mensaje_evento: documento.mensaje_evento ?? null,
              fecha_evento: documento.fecha_evento ?? null,
            },
          })
          .catch((err: unknown) =>
            this.logger.error(
              `No se pudo liberar la reserva de evento ${tipoDocumento}:${documentoId}: ${err instanceof Error ? err.message : err}`,
              'MiddlewareSifenService',
            ),
          );
      }
      return {
        success: false,
        message: error instanceof Error ? error.message : 'Error al enviar evento',
      };
    }
  }

  private mensajeEventoPendiente(labelDocumento: string, eventoAplicado?: string | null) {
    const nombre = { ECAN: 'cancelación', EINU: 'inutilización', EINO: 'nominación' }[eventoAplicado ?? ''] ?? 'SIFEN';
    return `${labelDocumento}: ya hay un evento de ${nombre} pendiente de respuesta de SIFEN. Esperá a que se procese antes de enviar otro.`;
  }

  async sendDocumentToMiddlewareSifen(document: CreateMiddlewareSifenDto): Promise<unknown> {
    // No loguear el documento completo (RUC/datos fiscales del cliente).
    this.logger.debug?.('Enviando documento al middleware SIFEN');
    const url = envs.middlewareSifenUrl;
    if (!url) throw new Error('MIDDLEWARE_SIFEN_URL no está configurado');
    const urlDocumento = `${url}/api/maintenance`;
    const controller = new AbortController();
    const timeoutMs = Number(this.configService.get<string>('MIDDLEWARE_SIFEN_TIMEOUT_MS') ?? 15000);
    const timeout = setTimeout(() => controller.abort(), timeoutMs);
    const responseLogin = await this.login(document.comprobantes?.[0]?.empresa_id?.toString() ?? undefined);
    if (!responseLogin) {
      throw new Error('No se pudo obtener el token de autenticación');
    }
    const tokenMiddleware = (responseLogin as { token?: string })?.token || null;
    return fetch(urlDocumento, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        ...(tokenMiddleware ? { authorization: `Bearer ${tokenMiddleware}` } : {}),
      },
      body: JSON.stringify(document),
      signal: controller.signal,
    })
      .then(async (res) => {
        const text = await res.text();
        if (!res.ok) {
          throw new Error(`Middleware SIFEN error ${res.status}: ${text}`);
        }
        try {
          return text ? (JSON.parse(text) as unknown) : null;
        } catch {
          return text;
        }
      })
      .finally(() => clearTimeout(timeout));
  }

  /**
   * Envía datos de empresa/sucursales/timbrados al middleware panel
   * POST /empresas/create
   */
  async sendEmpresaToMiddleware(payload: Record<string, unknown>): Promise<unknown> {
    const url = envs.apiUrlPanel;
    if (!url) throw new Error('API_URL_PANEL no está configurado');

    const empresaId = payload.id as string | undefined;
    const responseLogin = await this.loginPanel(empresaId);
    const tokenMiddleware = (responseLogin as { token?: string })?.token || null;

    const controller = new AbortController();
    const timeoutMs = Number(this.configService.get<string>('MIDDLEWARE_SIFEN_TIMEOUT_MS') ?? 15000);
    const timeout = setTimeout(() => controller.abort(), timeoutMs);

    const endpoint = `${url}/empresas/create`;
    console.log('sendEmpresaToMiddleware →', endpoint, JSON.stringify(payload).substring(0, 200));

    return fetch(endpoint, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        ...(tokenMiddleware ? { authorization: `Bearer ${tokenMiddleware}` } : {}),
      },
      body: JSON.stringify(payload),
      signal: controller.signal,
    })
      .then(async (res) => {
        const text = await res.text();
        if (!res.ok) {
          throw new Error(`Middleware empresa error ${res.status}: ${text}`);
        }
        try {
          return text ? (JSON.parse(text) as unknown) : null;
        } catch {
          return text;
        }
      })
      .finally(() => clearTimeout(timeout));
  }
  /**
   * Envía configuración de correo de empresa al middleware panel
   * POST /empresas/create-correo
   */
  async sendCorreoToMiddleware(payload: Record<string, unknown>): Promise<unknown> {
    const url = envs.apiUrlPanel;
    if (!url) throw new Error('API_URL_PANEL no está configurado');

    const empresaId = payload.empresa as string | undefined;
    const responseLogin = await this.loginPanel(empresaId);
    const tokenMiddleware = (responseLogin as { token?: string })?.token || null;

    const controller = new AbortController();
    const timeoutMs = Number(this.configService.get<string>('MIDDLEWARE_SIFEN_TIMEOUT_MS') ?? 15000);
    const timeout = setTimeout(() => controller.abort(), timeoutMs);

    const endpoint = `${url}/empresas/create-correo`;
    console.log('sendCorreoToMiddleware →', endpoint, JSON.stringify(payload).substring(0, 200));

    return fetch(endpoint, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        ...(tokenMiddleware ? { authorization: `Bearer ${tokenMiddleware}` } : {}),
      },
      body: JSON.stringify(payload),
      signal: controller.signal,
    })
      .then(async (res) => {
        const text = await res.text();
        if (!res.ok) {
          throw new Error(`Middleware correo error ${res.status}: ${text}`);
        }
        try {
          return text ? (JSON.parse(text) as unknown) : null;
        } catch {
          return text;
        }
      })
      .finally(() => clearTimeout(timeout));
  }

  /**
   * Envía datos de certificado digital al middleware panel
   * POST /certificates/create-certificate
   */
  async sendCertificadoToMiddleware(payload: Record<string, unknown>): Promise<unknown> {
    const url = envs.apiUrlPanel;
    if (!url) throw new Error('API_URL_PANEL no está configurado');

    const empresaId = payload.empresa as string | undefined;
    const responseLogin = await this.loginPanel(empresaId);
    const tokenMiddleware = (responseLogin as { token?: string })?.token || null;

    const controller = new AbortController();
    const timeoutMs = Number(this.configService.get<string>('MIDDLEWARE_SIFEN_TIMEOUT_MS') ?? 15000);
    const timeout = setTimeout(() => controller.abort(), timeoutMs);

    const endpoint = `${url}/certificados/create-certificate`;
    console.log('sendCertificadoToMiddleware →', endpoint, JSON.stringify(payload).substring(0, 200));

    return fetch(endpoint, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        ...(tokenMiddleware ? { authorization: `Bearer ${tokenMiddleware}` } : {}),
      },
      body: JSON.stringify(payload),
      signal: controller.signal,
    })
      .then(async (res) => {
        const text = await res.text();
        if (!res.ok) {
          throw new Error(`Middleware certificado error ${res.status}: ${text}`);
        }
        try {
          return text ? (JSON.parse(text) as unknown) : null;
        } catch {
          return text;
        }
      })
      .finally(() => clearTimeout(timeout));
  }

  async login(empresa_id: string | undefined): Promise<unknown> {
    const usuario_funcional = await this.prisma.usuario_funcional.findFirst({
      where: { empresa: empresa_id },
    });
    if (!usuario_funcional) {
      throw new Error('Usuario funcional no encontrado para la empresa');
    }
    const loginUrl = `${envs.middlewareSifenUrl}/api/auth`;
    const controller = new AbortController();
    const timeoutMs = Number(this.configService.get<string>('MIDDLEWARE_SIFEN_TIMEOUT_MS') ?? 15000);
    const timeout = setTimeout(() => controller.abort(), timeoutMs);

    const response = await fetch(loginUrl, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        ruc: usuario_funcional.username,
        password: `ABC#${usuario_funcional.username}`,
      }),
      signal: controller.signal,
    })
      .then(async (res) => {
        const text = await res.text();
        if (!res.ok) {
          throw new Error(`Middleware SIFEN login error ${res.status}: ${text}`);
        }
        try {
          const data = JSON.parse(text) as { status: string; token: string };
          return data;
        } catch {
          throw new Error(`Middleware SIFEN login error: Invalid response format`);
        }
      })
      .finally(() => clearTimeout(timeout));

    return response;
  }

  async loginPanel(empresa_id: string | undefined): Promise<unknown> {
    const loginUrl = `${envs.apiUrlPanel}/users/login`;
    const key = envs.appKeyPass;
    const controller = new AbortController();
    const timeoutMs = Number(this.configService.get<string>('MIDDLEWARE_SIFEN_TIMEOUT_MS') ?? 15000);
    const timeout = setTimeout(() => controller.abort(), timeoutMs);
    // Credenciales del panel SIFEN desde entorno (nunca hardcodeadas).
    const USER_PANEL = this.configService.get<string>('PANEL_SIFEN_USER');
    const PASS_PANEL = this.configService.get<string>('PANEL_SIFEN_PASS');
    if (!USER_PANEL || !PASS_PANEL) {
      throw new Error('Credenciales del panel SIFEN no configuradas (PANEL_SIFEN_USER / PANEL_SIFEN_PASS)');
    }

    const encryptedUsername = this.encryptAes256(key, USER_PANEL.toLowerCase().trim());
    const encryptedPass = this.encryptAes256(key, PASS_PANEL.trim());
    const response = await fetch(loginUrl, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        username: encryptedUsername,
        pass: encryptedPass,
      }),
      signal: controller.signal,
    })
      .then(async (res) => {
        const text = await res.text();
        if (!res.ok) {
          throw new Error(`PANEL SIFEN login error ${res.status}: ${text}`);
        }
        try {
          const data = JSON.parse(text) as { status: string; token: string };
          return data;
        } catch {
          throw new Error(`PANEL SIFEN login error: Invalid response format`);
        }
      })
      .finally(() => clearTimeout(timeout));
    console.log({ response });
    return response;
  }

  /**
   * Encripta un texto usando AES-256-CTR (compatible con el paquete aes256)
   * - Genera una key de 32 bytes con SHA-256 del passphrase
   * - IV aleatorio de 16 bytes antepuesto al ciphertext
   * - Retorna hex(iv + ciphertext)
   */
  private encryptAes256(passphrase: string, plaintext: string): string {
    const key = createHash('sha256').update(passphrase).digest();
    const iv = randomBytes(16);
    const cipher = createCipheriv('aes-256-ctr', key, iv);
    const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
    return Buffer.concat([iv, encrypted]).toString('hex');
  }

  async uploadCertificadoDigital(params: {
    empresaId: string;
    usuario: string;
    pin: string;
    fileBuffer: Buffer;
    originalFilename: string;
  }): Promise<unknown> {
    const url = envs.apiUrlPanel;
    if (!url) throw new Error('API_URL_PANEL no está configurado');

    const uploadUrl = `${url}/certificados/insert`;
    console.log('uploadUrl:', uploadUrl);
    console.log('params:', params);

    const controller = new AbortController();
    const timeoutMs = Number(this.configService.get<string>('MIDDLEWARE_SIFEN_TIMEOUT_MS') ?? 15000);
    const timeout = setTimeout(() => controller.abort(), timeoutMs);

    const responseLogin = await this.loginPanel(params.empresaId);
    const tokenMiddleware = (responseLogin as { token?: string })?.token || null;
    console.log('tokenMiddleware:', tokenMiddleware);

    const form = new FormData();
    form.append('pin', params.pin);
    form.append('empresa', params.empresaId);
    form.append('usuario', params.usuario);
    form.append('file', new Blob([new Uint8Array(params.fileBuffer)]), params.originalFilename || 'certificado.p12');

    return fetch(uploadUrl, {
      method: 'POST',
      headers: {
        ...(tokenMiddleware ? { authorization: `Bearer ${tokenMiddleware}` } : {}),
      },
      body: form,
      signal: controller.signal,
    })
      .then(async (res) => {
        const text = await res.text();
        if (!res.ok) {
          throw new Error(`Middleware certificado error ${res.status}: ${text}`);
        }
        try {
          return text ? (JSON.parse(text) as unknown) : null;
        } catch (parseError) {
          console.error('Error parsing middleware certificado response:', text);
          console.error('Parse error:', parseError);
          return text;
        }
      })
      .finally(() => clearTimeout(timeout));
  }

  /**
   * Sincroniza el estado SIFEN de todas las empresas con facturas pendientes
   * Este método es llamado por el cron job
   */
  async sincronizarEstadoSifenTodasEmpresas(): Promise<{
    empresasProcesadas: number;
    totalActualizados: number;
    detalles: string[];
  }> {
    // Obtener empresas que tienen facturas pendientes de sincronización
    const empresasConPendientes = await this.prisma.factura_cab.findMany({
      where: {
        nro_lote: { not: null },
        estado_sifen: { in: ['Enviado', 'Pendiente', 'Documento firmado'] },
      },
      select: { empresa_id: true },
      distinct: ['empresa_id'],
    });

    const empresaIds = [...new Set(empresasConPendientes.map((f) => f.empresa_id).filter(Boolean))] as string[];

    if (empresaIds.length === 0) {
      return {
        empresasProcesadas: 0,
        totalActualizados: 0,
        detalles: ['No hay empresas con facturas pendientes de sincronización'],
      };
    }

    let totalActualizados = 0;
    const detalles: string[] = [];

    for (const empresaId of empresaIds) {
      try {
        const resultado = await this.sincronizarSifen(empresaId, 'factura', 'documento');
        totalActualizados += resultado.actualizados;
        detalles.push(`Empresa ${empresaId}: ${resultado.actualizados} actualizadas`);
      } catch (error) {
        const msg = error instanceof Error ? error.message : 'Error desconocido';
        detalles.push(`Error en empresa ${empresaId}: ${msg}`);
      }
    }

    return {
      empresasProcesadas: empresaIds.length,
      totalActualizados,
      detalles,
    };
  }

  /**
   * Sincroniza el estado SIFEN de documentos o eventos pendientes
   * @param empresaId ID de la empresa
   * @param tipoDocumento Tipo de documento
   * @param tipoConsulta 'documento' o 'evento'
   * @param nroLote Número de lote opcional (solo para documentos)
   */
  async sincronizarSifen(
    empresaId: string,
    tipoDocumento: TipoDocumento,
    tipoConsulta: 'documento' | 'evento',
    nroLote?: string,
    /** Reconsulta de un solo documento (acción "Reconsultar"): sólo su evento pendiente. */
    soloDocumentoId?: string,
  ): Promise<{ actualizados: number; detalles: string[]; casosEvento: Record<string, CasoEventoPendiente> }> {
    const url = envs.middlewareSifenUrl;
    if (!url) throw new Error('MIDDLEWARE_SIFEN_URL no está configurado');
    const prismaModel = this.getPrismaModel(tipoDocumento);
    const labelDoc = DOCUMENTO_CONFIG[tipoDocumento].label;
    // Estados locales que representan un documento enviado a SIFEN pero aún sin
    // respuesta final capturada. Debe usarse tanto para DETECTAR pendientes como
    // para MATCHEAR el registro local a actualizar (si difieren, se detectan
    // documentos que luego nunca se actualizan).
    const ESTADOS_SIFEN_PENDIENTES = ['Enviado', 'Pendiente', 'Documento firmado'];
    // Se reconsultan también los 'Rechazado': un rechazo podía ser un 4003 de
    // SIFEN ("el CDC ya tiene ese evento"), que en realidad confirma el evento.
    // Con el filtro viejo esos documentos salían para siempre del set y nadie
    // los volvía a mirar (caso DOBA 001-001-0002374). Mismo criterio que
    // EVENTO_PENDIENTE en sifen-sync.plan.ts, que decide a qué empresas visitar:
    // si difieren, se visitan empresas en las que después no se encuentra nada.
    const whereClauseEvent = {
      empresa_id: empresaId,
      evento_aplicado: { in: ['ECAN', 'EINO', 'EINU'] },
      estado_evento: { in: ['Pendiente', 'Rechazado'] },
      ...(soloDocumentoId ? { id: soloDocumentoId } : {}),
    };
    const whereClauseDocument = {
      empresa_id: empresaId,
      nro_lote: nroLote || { not: null },
      estado_sifen: { in: ESTADOS_SIFEN_PENDIENTES },
    };

    const pendientesDocument = soloDocumentoId
      ? []
      : await prismaModel.findMany({
          where: whereClauseDocument,
        });
    const pendientesEvent = await prismaModel.findMany({
      where: whereClauseEvent,
    });
    const casosEvento: Record<string, CasoEventoPendiente> = {};
    if (pendientesDocument.length === 0 && pendientesEvent.length === 0) {
      return {
        actualizados: 0,
        detalles: [`No hay ${labelDoc} pendientes de sincronización`],
        casosEvento,
      };
    }

    const detalles: string[] = [];
    let actualizados = 0;

    // Con el middleware caído los documentos no se pueden sincronizar, pero los
    // eventos pendientes igual se clasifican: una incoherencia, o un evento que
    // no se puede consultar hace más de una hora, tiene que generar su alerta.
    // Sin eventos pendientes el error se propaga como antes.
    let tokenMiddleware: string | null = null;
    let errorLogin: string | null = null;
    try {
      const responseLogin = await this.login(empresaId);
      tokenMiddleware = (responseLogin as { token?: string })?.token || null;
    } catch (error) {
      if (pendientesEvent.length === 0) throw error;
      errorLogin = error instanceof Error ? error.message : 'Error desconocido';
      detalles.push(`No se pudo iniciar sesión en el middleware: ${errorLogin}`);
    }

    const parseFecha = (fechaStr: string | null): Date | null => {
      if (!fechaStr) return null;
      const [fecha, hora] = fechaStr.split(' ');
      const [dia, mes, anio] = fecha.split('-');
      return new Date(`${anio}-${mes}-${dia}T${hora}`);
    };

    if (pendientesDocument.length > 0 && !errorLogin) {
      const lotes = [...new Set(pendientesDocument.map((p) => p.nro_lote).filter(Boolean))] as string[];
      for (const lote of lotes) {
        try {
          const body = {
            tipOpe: '2',
            tipo: 'documento',
            fecha_inicial: null,
            fecha_final: null,
            nro_lote: lote,
            iTiDE: null,
            dEst: null,
            dPunExp: null,
            dNumDoc: null,
            cdc: null,
          };

          const response = await fetch(`${url}/api/maintenance`, {
            method: 'POST',
            headers: {
              'content-type': 'application/json',
              ...(tokenMiddleware ? { authorization: `Bearer ${tokenMiddleware}` } : {}),
            },
            body: JSON.stringify(body),
          });

          const data = (await response.json()) as {
            status: string;
            response: Array<{
              cdc: string;
              enlaceQR: string;
              nro_lote: string;
              fecha_envio: string;
              fecha_firma: string;
              fecha_registro: string;
              estado_documento: string;
              estado_envio_sifen: string;
              estado_envio_email?: string;
              retorno_sifen?: {
                estado: string;
                mensaje: string;
                protocolo: string | null;
                codrespuesta: string;
                fecha_retorno: string;
              };
            }>;
          };

          if (data.status === 'success' && Array.isArray(data.response)) {
            for (const doc of data.response) {
              const cdc = doc.cdc;
              if (!cdc || cdc.length < 44) continue;
              const dest = cdc.substring(11, 14);
              const dpunexp = cdc.substring(14, 17);
              const dnumdoc = cdc.substring(17, 24);

              const registro = await prismaModel.findFirst({
                where: {
                  empresa_id: empresaId,
                  dest,
                  dpunexp,
                  dnumdoc,
                  estado_sifen: { in: ESTADOS_SIFEN_PENDIENTES },
                },
              });
              if (!registro) {
                detalles.push(`CDC ${cdc}: ${labelDoc} no encontrada (${dest}-${dpunexp}-${dnumdoc})`);
                continue;
              }

              const estadoSifen = doc.retorno_sifen?.estado || doc.estado_documento || 'Desconocido';
              const mensajeSifen = doc.retorno_sifen?.mensaje || null;

              try {
                await prismaModel.update({
                  where: { id: registro.id },
                  data: {
                    cdc: doc.cdc,
                    enlace_qr: doc.enlaceQR,
                    fecha_firma_sifen: parseFecha(doc.fecha_firma),
                    fecha_registro_sifen: parseFecha(doc.fecha_registro),
                    estado_sifen: estadoSifen,
                    mensaje_sifen: mensajeSifen,
                    ...(estadoSifen === 'Aprobado' ? { estado: 'Aprobado' } : {}),
                    ...(estadoSifen === 'Rechazado' ? { estado: 'Rechazado' } : {}),
                    ...(doc.estado_envio_email ? { estado_envio_email: doc.estado_envio_email } : {}),
                  },
                });
                actualizados++;
                detalles.push(`${labelDoc} ${dest}-${dpunexp}-${dnumdoc}: ${estadoSifen}`);
                const numeroDoc = `${dest}-${dpunexp}-${dnumdoc}`;

                // Audit: estado de documento sincronizado con SIFEN
                await this.auditSifenEvent({
                  empresaId,
                  tipoDocumento,
                  documentoId: registro.id,
                  numero: numeroDoc,
                  action: 'sifen_sync_documento',
                  resultado: estadoSifen === 'Aprobado' ? 'OK' : 'ERROR',
                  detalle: `Documento ${estadoSifen}${mensajeSifen ? `: ${mensajeSifen}` : ''}`,
                  extra: { estadoSifen, cdc },
                });

                // Integración contable al aprobar por SIFEN
                if (estadoSifen === 'Aprobado') {
                  if (tipoDocumento === 'nota_credito') {
                    // Una NC rechazada se reversa y queda marcada como tal. Si despues
                    // se corrige el problema y se reenvia, SIFEN la aprueba pero la marca
                    // seguia puesta: la NC quedaba valida y aprobada, pero invisible para
                    // el wizard de cobros -que filtra por rec_aplicacion_reversada y por
                    // rec_saldo_disponible > 0-, y no se podia usar nunca mas.
                    // Al aprobarse se limpia la marca y se devuelve el saldo a NULL, que
                    // es como se calcula desde los subtotales de la propia NC.
                    this.restaurarNcTrasAprobacion(registro.id).catch((err: Error) =>
                      this.logger.error(
                        `Error restaurando NC ${registro.id} tras aprobacion: ${err.message}`,
                        'MiddlewareSifenService',
                      ),
                    );
                    this.contabilidadIntegracion
                      .integrarNotaCredito(registro.id)
                      .then(() =>
                        this.auditSifenEvent({
                          empresaId,
                          tipoDocumento,
                          documentoId: registro.id,
                          numero: numeroDoc,
                          action: 'sifen_integracion_contable',
                          resultado: 'OK',
                          detalle: 'Asiento contable generado al aprobar NC',
                        }),
                      )
                      .catch((err) => {
                        this.logger.error(
                          `Error contabilizando NC ${registro.id}: ${err.message}`,
                          'MiddlewareSifenService',
                        );
                        return this.auditSifenEvent({
                          empresaId,
                          tipoDocumento,
                          documentoId: registro.id,
                          numero: numeroDoc,
                          action: 'sifen_integracion_contable',
                          resultado: 'ERROR',
                          detalle: `Error contabilizando NC: ${err.message}`,
                        });
                      });
                  } else if (tipoDocumento === 'factura') {
                    this.contabilidadIntegracion
                      .integrarFacturaVenta(registro.id)
                      .then(() =>
                        this.auditSifenEvent({
                          empresaId,
                          tipoDocumento,
                          documentoId: registro.id,
                          numero: numeroDoc,
                          action: 'sifen_integracion_contable',
                          resultado: 'OK',
                          detalle: 'Asiento contable generado al aprobar factura',
                        }),
                      )
                      .catch((err) => {
                        this.logger.error(
                          `Error contabilizando factura ${registro.id}: ${err.message}`,
                          'MiddlewareSifenService',
                        );
                        return this.auditSifenEvent({
                          empresaId,
                          tipoDocumento,
                          documentoId: registro.id,
                          numero: numeroDoc,
                          action: 'sifen_integracion_contable',
                          resultado: 'ERROR',
                          detalle: `Error contabilizando factura: ${err.message}`,
                        });
                      });
                  }
                }
                // Reversión de saldos si la NC fue rechazada por SIFEN
                if (estadoSifen === 'Rechazado' && tipoDocumento === 'nota_credito') {
                  this.revertirAplicacionNC(registro.id)
                    .then(() =>
                      this.auditSifenEvent({
                        empresaId,
                        tipoDocumento,
                        documentoId: registro.id,
                        numero: numeroDoc,
                        action: 'sifen_reversion_nc',
                        resultado: 'OK',
                        detalle: 'Saldos de factura restaurados tras rechazo de NC',
                      }),
                    )
                    .catch((err) => {
                      this.logger.error(
                        `Error revirtiendo NC rechazada ${registro.id}: ${err.message}`,
                        'MiddlewareSifenService',
                      );
                      return this.auditSifenEvent({
                        empresaId,
                        tipoDocumento,
                        documentoId: registro.id,
                        numero: numeroDoc,
                        action: 'sifen_reversion_nc',
                        resultado: 'ERROR',
                        detalle: `Error revirtiendo NC rechazada: ${err.message}`,
                      });
                    });
                }
              } catch (updateError) {
                const updateMsg = updateError instanceof Error ? updateError.message : 'Error desconocido';
                console.error(`Error actualizando ${labelDoc} ${registro.id}:`, updateMsg);
                detalles.push(`Error actualizando ${labelDoc} ${dest}-${dpunexp}-${dnumdoc}: ${updateMsg}`);
              }
            }
          }
        } catch (error) {
          const msg = error instanceof Error ? error.message : 'Error desconocido';
          detalles.push(`Error consultando lote ${lote}: ${msg}`);
        }
      }
    }

    if (pendientesEvent.length > 0) {
      const sucursalPorEstablecimiento = await this.sucursalesPorEstablecimiento(empresaId);
      for (const doc of pendientesEvent) {
        try {
          const tipoEvento = doc.evento_aplicado as 'ECAN' | 'EINO' | 'EINU';
          const consulta: RespuestaConsultaEvento = errorLogin
            ? { ok: false, error: `login al middleware: ${errorLogin}` }
            : await this.consultarEventoEnMiddleware(url, tokenMiddleware, doc, DOCUMENTO_CONFIG[tipoDocumento].iTiDE);
          const clasificacion = clasificarEventoPendiente(doc, consulta, new Date());
          casosEvento[doc.id] = clasificacion.caso;

          if (clasificacion.caso !== 'respondido') {
            // Sin respuesta definitiva: si ya es una inconsistencia, se avisa.
            await this.procesarAlertaEvento(empresaId, tipoDocumento, doc, clasificacion, sucursalPorEstablecimiento);
            detalles.push(
              `${labelDoc} ${doc.dest}-${doc.dpunexp}-${doc.dnumdoc}: Evento ${tipoEvento} - ${clasificacion.detalle}`,
            );
          } else {
            const eventoData = (consulta as { ok: true; registro: RegistroEventoMiddleware }).registro;
            if (eventoData?.retorno_sifen?.estado) {
              // Mismo criterio que el camino interactivo: 4003 significa que el
              // evento ya está en SIFEN, así que vale como aprobado.
              const estadoEvento = this.eventoYaRegistrado(
                eventoData.retorno_sifen.codrespuesta,
                eventoData.retorno_sifen.mensaje,
              )
                ? 'Aprobado'
                : eventoData.retorno_sifen.estado;
              const mensajeEvento = eventoData.retorno_sifen.mensaje;
              const fechaEvento = parseFecha(eventoData.retorno_sifen.fecha_retorno);

              let nuevoEstadoSifen: string | undefined;
              if (estadoEvento === 'Aprobado') {
                if (tipoEvento === 'EINU') nuevoEstadoSifen = 'Inutilizado';
                else if (tipoEvento === 'ECAN') nuevoEstadoSifen = 'Cancelado';
                else if (tipoEvento === 'EINO') nuevoEstadoSifen = 'Nominado';
              }

              await prismaModel.update({
                where: { id: doc.id },
                data: {
                  estado_evento: estadoEvento,
                  mensaje_evento: mensajeEvento,
                  fecha_evento: fechaEvento,
                  ...(nuevoEstadoSifen ? { estado_sifen: nuevoEstadoSifen } : {}),
                  // Solo marcar como Anulado si SIFEN aprobó el evento de cancelación/inutilización.
                  // Si el evento fue Rechazado, el documento sigue vigente. En remisiones lo
                  // marca `revertirRemision`: si ya viniera Anulado no repondría el stock.
                  ...(estadoEvento === 'Aprobado' && tipoDocumento !== 'nota_remision' ? { estado: 'Anulado' } : {}),
                },
              });

              actualizados++;
              const numeroEvDoc = `${doc.dest}-${doc.dpunexp}-${doc.dnumdoc}`;
              detalles.push(`${labelDoc} ${numeroEvDoc}: Evento ${tipoEvento} - ${estadoEvento}`);

              // Audit: resolución de evento
              await this.auditSifenEvent({
                empresaId,
                tipoDocumento,
                documentoId: doc.id,
                numero: numeroEvDoc,
                action: 'sifen_sync_evento',
                resultado: estadoEvento === 'Aprobado' ? 'OK' : 'ERROR',
                detalle: `Evento ${tipoEvento} ${estadoEvento}${mensajeEvento ? `: ${mensajeEvento}` : ''}`,
                extra: { tipoEvento, estadoEvento, nuevoEstadoSifen },
              });

              // Reversión contable cuando ECAN aprobado por SIFEN. La remisión no genera
              // asiento: sin el filtro se revertía `factura_cab` con el id de la remisión.
              if (tipoEvento === 'ECAN' && estadoEvento === 'Aprobado' && tipoDocumento !== 'nota_remision') {
                const origenTipo = tipoDocumento === 'nota_credito' ? 'nota_credito_cab' : 'factura_cab';
                // usuarioId debe ser un UUID o null: es una reversión automática por SIFEN
                // (sin usuario humano). Pasar un literal 'SIFEN' rompía cont_asientos.usuario_id (UUID).
                this.contabilidadIntegracion
                  .revertirDocumento(origenTipo, doc.id, null)
                  .then(() =>
                    this.auditSifenEvent({
                      empresaId,
                      tipoDocumento,
                      documentoId: doc.id,
                      numero: numeroEvDoc,
                      action: 'sifen_reversion_contable',
                      resultado: 'OK',
                      detalle: `Asiento revertido por ECAN aprobado`,
                    }),
                  )
                  .catch((err) => {
                    this.logger.error(
                      `Error reversión contable SIFEN ${doc.id}: ${err.message}`,
                      'MiddlewareSifenService',
                    );
                    return this.auditSifenEvent({
                      empresaId,
                      tipoDocumento,
                      documentoId: doc.id,
                      numero: numeroEvDoc,
                      action: 'sifen_reversion_contable',
                      resultado: 'ERROR',
                      detalle: `Error revirtiendo asiento por ECAN: ${err.message}`,
                    });
                  });
              }
              // Reversión de saldos de NC anulada/inutilizada por evento aprobado
              if (
                (tipoEvento === 'ECAN' || tipoEvento === 'EINU') &&
                estadoEvento === 'Aprobado' &&
                tipoDocumento === 'nota_credito'
              ) {
                this.revertirAplicacionNC(doc.id)
                  .then(() =>
                    this.auditSifenEvent({
                      empresaId,
                      tipoDocumento,
                      documentoId: doc.id,
                      numero: numeroEvDoc,
                      action: 'sifen_reversion_nc',
                      resultado: 'OK',
                      detalle: `Saldos de factura restaurados tras ${tipoEvento} aprobado`,
                    }),
                  )
                  .catch((err) => {
                    this.logger.error(
                      `Error revirtiendo NC ${tipoEvento} ${doc.id}: ${err.message}`,
                      'MiddlewareSifenService',
                    );
                    return this.auditSifenEvent({
                      empresaId,
                      tipoDocumento,
                      documentoId: doc.id,
                      numero: numeroEvDoc,
                      action: 'sifen_reversion_nc',
                      resultado: 'ERROR',
                      detalle: `Error revirtiendo NC tras ${tipoEvento}: ${err.message}`,
                    });
                  });
              }
              // Reversión orden de venta cuando ECAN o EINU aprobado en factura
              if (
                (tipoEvento === 'ECAN' || tipoEvento === 'EINU') &&
                estadoEvento === 'Aprobado' &&
                tipoDocumento === 'factura'
              ) {
                this.revertirOrdenVentaDesdeFactura(doc.id, empresaId).catch((err) =>
                  this.logger.error(
                    `Error revirtiendo orden de venta SIFEN ${doc.id}: ${err.message}`,
                    'MiddlewareSifenService',
                  ),
                );
              }
              // Reversión de stock cuando ECAN o EINU aprobado
              if ((tipoEvento === 'ECAN' || tipoEvento === 'EINU') && estadoEvento === 'Aprobado') {
                if (tipoDocumento === 'factura') {
                  // Reversión completa por el punto único. Antes acá sólo se
                  // revertía caja y stock: las cuotas, la cuenta corriente y las
                  // comisiones quedaban vivas y la factura figuraba anulada
                  // debiendo plata.
                  await this.revertirFacturaAnulada(doc.id, empresaId, {
                    motivo: `Evento ${tipoEvento} aprobado por SIFEN`,
                    usuarioId: null,
                    // El asiento ya lo revierte el bloque de arriba de este mismo
                    // flujo (vale también para nota_credito), así que no se repite.
                    revertirContabilidad: false,
                  });
                } else if (tipoDocumento === 'nota_credito') {
                  this.revertirStockNC(doc.id, empresaId).catch((err) =>
                    this.logger.error(
                      `Error reversión stock NC SIFEN ${doc.id}: ${err.message}`,
                      'MiddlewareSifenService',
                    ),
                  );
                }
              }
              // Reversión de la remisión (stock + cantidades aplicadas a facturas) por ECAN/EINU aprobado
              if (
                (tipoEvento === 'ECAN' || tipoEvento === 'EINU') &&
                estadoEvento === 'Aprobado' &&
                tipoDocumento === 'nota_remision'
              ) {
                try {
                  const { revertida, stockRepuesto } = await this.reversionRemision.revertirRemision(doc.id, empresaId);
                  await this.auditSifenEvent({
                    empresaId,
                    tipoDocumento,
                    documentoId: doc.id,
                    numero: numeroEvDoc,
                    action: 'sifen_reversion_remision',
                    resultado: 'OK',
                    detalle: revertida
                      ? `Remisión anulada por ${tipoEvento} aprobado | Stock repuesto en ${stockRepuesto} ítem(s)`
                      : `Remisión ya estaba anulada: sin reversión por ${tipoEvento}`,
                  });
                } catch (err) {
                  const msg = err instanceof Error ? err.message : 'Error desconocido';
                  this.logger.error(`Error revirtiendo remisión SIFEN ${doc.id}: ${msg}`, 'MiddlewareSifenService');
                  await this.auditSifenEvent({
                    empresaId,
                    tipoDocumento,
                    documentoId: doc.id,
                    numero: numeroEvDoc,
                    action: 'sifen_reversion_remision',
                    resultado: 'ERROR',
                    detalle: `Error revirtiendo remisión tras ${tipoEvento}: ${msg}`,
                  });
                }
              }
              // SIFEN respondió: si había una alerta por este evento, ya no aplica.
              await this.alertasEmpresa
                .resolver(empresaId, claveAlertaEvento(tipoDocumento, doc.id), 'auto')
                .catch((err: unknown) =>
                  this.logger.error(
                    `No se pudo resolver la alerta del evento ${tipoDocumento}:${doc.id}: ${err instanceof Error ? err.message : err}`,
                    'MiddlewareSifenService',
                  ),
                );
            }
          }
        } catch (error) {
          const msg = error instanceof Error ? error.message : 'Error desconocido';
          detalles.push(`Error consultando evento ${labelDoc} ${doc.id}: ${msg}`);
        }
      }
    }

    if (actualizados > 0) {
      this.logger.log(
        `Sincronización SIFEN: ${actualizados} ${labelDoc}(s) actualizados | Empresa: ${empresaId}`,
        'MiddlewareSifenService',
      );
      await this.auditService.log({
        empresa_id: empresaId,
        action: 'UPDATE',
        entity_type: tipoDocumento,
        entity_id: empresaId,
        descripcion: `Sincronización SIFEN (${tipoConsulta}): ${actualizados} ${labelDoc}(s) actualizados`,
        new_value: { actualizados, detalles },
      });
    }

    return { actualizados, detalles, casosEvento };
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Acciones sobre eventos trabados (alertas de empresa).
  // Ver docs/plan-alertas-empresa.md.
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * Vuelve a consultar en el middleware el evento pendiente de un documento.
   * Reutiliza el sync (limitado a ese documento) para que una respuesta de
   * SIFEN se aplique igual que en el job: estado, reversiones y alertas.
   */
  async reconsultarEvento(
    empresaId: string,
    tipoDocumento: TipoDocumento,
    documentoId: string,
  ): Promise<{ success: boolean; caso: CasoEventoPendiente | 'sin_evento_pendiente'; message: string }> {
    if (tipoDocumento === 'nota_debito') {
      return { success: false, caso: 'sin_evento_pendiente', message: 'Las notas de débito no tienen eventos SIFEN' };
    }
    const documento = await this.getPrismaModel(tipoDocumento).findFirst({
      where: { id: documentoId, empresa_id: empresaId },
      select: { id: true, estado_evento: true },
    });
    if (!documento || documento.estado_evento !== 'Pendiente') {
      return { success: false, caso: 'sin_evento_pendiente', message: 'El documento no tiene un evento pendiente' };
    }

    const { casosEvento } = await this.sincronizarSifen(empresaId, tipoDocumento, 'evento', undefined, documentoId);
    const caso = casosEvento[documentoId] ?? 'middleware_falla';
    return { success: true, caso, message: MENSAJE_CASO_EVENTO[caso] };
  }

  /**
   * Timbrado para un EINU: el que se usó al emitir el documento
   * (`documentos_electronicos`, por CDC) y, si no quedó registro, el timbrado
   * vigente de la empresa (mismo criterio que el Libro IVA y Marangatu).
   */
  private async timbradoParaEvento(empresaId: string, cdc: string | null): Promise<number | null> {
    const deTimbrado = cdc
      ? await this.prisma.documentos_electronicos.findFirst({
          where: { cdc, empresa_id: empresaId },
          select: { num_timbrado: true },
        })
      : null;
    const num =
      deTimbrado?.num_timbrado ||
      (
        await this.prisma.empresas_timbrado.findFirst({
          where: { empresa_id: empresaId },
          orderBy: { fecha_inicio: 'desc' },
          select: { num_timbrado: true },
        })
      )?.num_timbrado;
    const n = Number(num);
    return num && Number.isFinite(n) && n > 0 ? n : null;
  }

  /**
   * Reenvía el evento trabado de un documento en un solo paso: si está
   * Pendiente lo libera (con la misma guarda que "Liberar": si SIFEN ya lo
   * tiene, no se toca) y vuelve a mandar el mismo evento con el motivo
   * original. Es la acción "Reenviar evento" de la alerta. Sólo ECAN y EINU:
   * la nominación (EINO) necesita los datos del cliente y va desde la factura.
   */
  async reenviarEvento(
    empresaId: string,
    tipoDocumento: TipoDocumento,
    documentoId: string,
    usuarioId: string,
    opciones: { casosLiberables?: CasoEventoPendiente[]; motivoLiberacion?: string } = {},
  ): Promise<{ success: boolean; message: string; numero?: string }> {
    const doc = (await this.getPrismaModel(tipoDocumento).findFirst({
      where: { id: documentoId, empresa_id: empresaId },
      select: { id: true, dest: true, dpunexp: true, dnumdoc: true, estado_sifen: true, evento_aplicado: true, estado_evento: true, mensaje_evento: true },
    })) as {
      id: string;
      dest: string;
      dpunexp: string;
      dnumdoc: string;
      estado_sifen: string | null;
      evento_aplicado: string | null;
      estado_evento: string | null;
      mensaje_evento: string | null;
    } | null;
    const label = DOCUMENTO_CONFIG[tipoDocumento].label;
    if (!doc) return { success: false, message: `${label} no encontrado` };
    const numero = `${doc.dest}-${doc.dpunexp}-${doc.dnumdoc}`;

    const tipoEvento: TipoEvento | null =
      doc.evento_aplicado === 'EINU' ? 'inutilizacion' : doc.evento_aplicado === 'ECAN' ? 'cancelacion' : null;
    if (!tipoEvento) {
      return { success: false, numero, message: 'Sólo se reenvían cancelaciones e inutilizaciones; la nominación se vuelve a hacer desde la factura' };
    }
    if (doc.estado_evento === 'Aprobado') {
      return { success: false, numero, message: 'El evento ya fue aprobado por SIFEN: no hay nada que reenviar' };
    }

    // El motivo original del evento; si el documento ya fue liberado antes, se
    // le saca el prefijo de la liberación.
    const motivo = doc.mensaje_evento?.replace(/^Liberado por .*?: /, '') || 'Documento rechazado por SIFEN';

    // Un evento que no corresponde al estado del documento (una EINU sobre un
    // documento aprobado, un ECAN sobre uno rechazado) no se reenvía: se
    // libera lo trabado y se explica qué hacer, en vez del "Solo se pueden
    // inutilizar documentos rechazados" pelado del envío.
    const estadoRequerido = tipoEvento === 'inutilizacion' ? 'Rechazado' : 'Aprobado';
    const incoherente = doc.estado_sifen !== estadoRequerido;

    if (doc.estado_evento === 'Pendiente') {
      const lib = await this.liberarEvento(
        empresaId,
        tipoDocumento,
        documentoId,
        opciones.motivoLiberacion ?? 'Reenvío del evento desde la alerta',
        usuarioId,
        opciones.casosLiberables,
      );
      if (!lib.success) return { success: false, numero, message: lib.message };
    }

    if (incoherente) {
      const que =
        tipoEvento === 'inutilizacion'
          ? `está ${doc.estado_sifen ?? 'sin estado'} en SIFEN y sólo se inutilizan documentos rechazados. Se liberó el evento trabado; si querés dejar el documento sin efecto, cancelalo (ECAN) desde el documento.`
          : `está ${doc.estado_sifen ?? 'sin estado'} en SIFEN y sólo se cancelan documentos aprobados. Se liberó el evento trabado; si el documento fue rechazado, corresponde inutilizarlo (EINU) desde el documento.`;
      return { success: false, numero, message: `${label} ${numero} ${que}` };
    }

    const envio = await this.enviarEvento(empresaId, tipoDocumento, tipoEvento, documentoId, motivo);
    return { success: envio.success, numero, message: envio.message };
  }

  /**
   * Reenvía las inutilizaciones que quedaron trabadas por haberse mandado sin
   * timbrado. Sólo toma eventos con más de una hora (o sin fecha, como
   * quedaron los viejos): una EINU legítima en vuelo tarda minutos y no se
   * toca. Las Pendientes se liberan salvo que SIFEN ya haya respondido o el
   * middleware no conteste; las Rechazadas se reenvían directo.
   */
  async reenviarInutilizacionesPendientes(
    empresaId: string,
    usuarioId: string,
  ): Promise<{ reenviados: number; omitidos: number; detalles: Array<{ tipo: TipoDocumento; id: string; numero: string; resultado: string }> }> {
    const tipos: TipoDocumento[] = ['factura', 'nota_credito', 'nota_remision'];
    const detalles: Array<{ tipo: TipoDocumento; id: string; numero: string; resultado: string }> = [];
    let reenviados = 0;

    for (const tipo of tipos) {
      const docs = (await this.getPrismaModel(tipo).findMany({
        where: {
          empresa_id: empresaId,
          evento_aplicado: 'EINU',
          estado_sifen: 'Rechazado',
          estado_evento: { in: ['Pendiente', 'Rechazado'] },
          OR: [{ fecha_evento: null }, { fecha_evento: { lt: new Date(Date.now() - UMBRAL_EVENTO_MS) } }],
        },
        select: { id: true },
      })) as Array<{ id: string }>;

      for (const doc of docs) {
        const r = await this.reenviarEvento(empresaId, tipo, doc.id, usuarioId, {
          motivoLiberacion: 'Reenvío de inutilización con timbrado (EINU sin dNumTim)',
          // Los eventos viejos no tienen fecha_evento y el clasificador los ve
          // como recién enviados; el filtro de arriba ya garantiza que no lo son.
          casosLiberables: ['no_registrado', 'incoherente', 'sin_respuesta', 'en_espera'],
        });
        if (r.success) reenviados += 1;
        detalles.push({ tipo, id: doc.id, numero: r.numero ?? '', resultado: r.message });
      }
    }

    this.logger.log(`Reenvío EINU empresa ${empresaId}: ${reenviados}/${detalles.length} reenviadas`, 'MiddlewareSifenService');
    return { reenviados, omitidos: detalles.length - reenviados, detalles };
  }

  /**
   * Libera un evento trabado para que el documento pueda recibir otro. Sólo
   * cuando la reconsulta confirma que SIFEN no lo tiene (no registrado) o que
   * el evento no es coherente con el documento: si SIFEN lo tiene, liberarlo
   * permitiría duplicarlo.
   */
  async liberarEvento(
    empresaId: string,
    tipoDocumento: TipoDocumento,
    documentoId: string,
    motivo: string,
    usuarioId: string,
    // El reenvío de EINU sin timbrado agrega 'sin_respuesta': esa inutilización
    // no puede prosperar aunque el middleware la tenga registrada.
    casosLiberables: CasoEventoPendiente[] = ['no_registrado', 'incoherente'],
  ): Promise<{ success: boolean; caso: CasoEventoPendiente | 'sin_evento_pendiente'; message: string }> {
    const motivoLimpio = String(motivo ?? '').trim();
    if (motivoLimpio.length < 5) {
      throw new BadRequestException('Indicá el motivo por el que se libera el evento');
    }

    const reconsulta = await this.reconsultarEvento(empresaId, tipoDocumento, documentoId);
    if (reconsulta.caso === 'sin_evento_pendiente' || !casosLiberables.includes(reconsulta.caso)) {
      return {
        success: false,
        caso: reconsulta.caso,
        message: `No se liberó: ${reconsulta.message}`,
      };
    }

    const usuario = await this.prisma.usuario.findUnique({ where: { id: usuarioId }, select: { username: true } });
    const ahora = new Date();
    const cuando = ahora.toLocaleString('es-PY', { timeZone: 'America/Asuncion' });
    const mensaje = `Liberado por ${usuario?.username ?? usuarioId} el ${cuando}: ${motivoLimpio}`.slice(0, 500);

    // Condicionado a que siga Pendiente: dos usuarios liberando a la vez no
    // generan dos auditorías ni pisan una respuesta que llegó en el medio.
    const { count } = await this.getPrismaModel(tipoDocumento).updateMany({
      where: { id: documentoId, empresa_id: empresaId, estado_evento: 'Pendiente' },
      data: { estado_evento: 'Liberado', mensaje_evento: mensaje, fecha_evento: ahora },
    });
    if (count === 0) {
      return { success: false, caso: reconsulta.caso, message: 'El evento ya no estaba pendiente' };
    }

    await this.alertasEmpresa.resolver(empresaId, claveAlertaEvento(tipoDocumento, documentoId), 'liberada', usuarioId);
    await this.auditService.log({
      empresa_id: empresaId,
      user_id: usuarioId,
      action: 'UPDATE',
      entity_type: tipoDocumento,
      entity_id: documentoId,
      descripcion: `Evento SIFEN liberado (${reconsulta.caso}) | Motivo: ${motivoLimpio}`.slice(0, 255),
      old_value: { estado_evento: 'Pendiente', caso: reconsulta.caso },
      new_value: { estado_evento: 'Liberado', mensaje_evento: mensaje },
    });
    this.logger.log(
      `Evento SIFEN liberado: ${tipoDocumento}:${documentoId} (${reconsulta.caso}) | Empresa: ${empresaId}`,
      'MiddlewareSifenService',
    );
    return { success: true, caso: reconsulta.caso, message: 'Evento liberado. Ya se puede enviar otro evento al documento.' };
  }

  /** Mapa punto de establecimiento → sucursal, para atar una alerta a la sucursal del documento. */
  private async sucursalesPorEstablecimiento(empresaId: string): Promise<Map<string, string>> {
    const sucursales = await this.prisma.empresas_sucursales.findMany({
      where: { empresa_id: empresaId },
      select: { id: true, punto_establecimiento: true },
    });
    return new Map(
      sucursales.filter((suc) => suc.punto_establecimiento).map((suc) => [suc.punto_establecimiento as string, suc.id]),
    );
  }

  /**
   * Consulta un evento en el middleware. Nunca tira: una falla vuelve como
   * `{ ok: false }` para que el clasificador decida si ya es una inconsistencia.
   *
   * `registro: null` = el middleware respondió bien pero no tiene el evento. Si la
   * respuesta trae un único registro con otra clave se usa igual: un formato de
   * clave distinto no puede leerse como "no registrado", que habilita liberar.
   */
  private async consultarEventoEnMiddleware(
    url: string,
    tokenMiddleware: string | null,
    doc: { cdc?: string | null; dest?: string | null; dpunexp?: string | null; dnumdoc?: string | null; evento_aplicado?: string | null },
    iTiDE: number,
  ): Promise<RespuestaConsultaEvento> {
    const payload = payloadConsultaEvento(doc, iTiDE);
    try {
      const response = await fetch(`${url}/api/maintenance`, {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          ...(tokenMiddleware ? { authorization: `Bearer ${tokenMiddleware}` } : {}),
        },
        body: JSON.stringify(payload),
      });
      const data = (await response.json()) as {
        status?: string;
        message?: string;
        response?: Record<string, RegistroEventoMiddleware> | null;
      };
      if (data?.status !== 'success') {
        return { ok: false, error: data?.message || `respuesta ${data?.status ?? response.status}` };
      }
      const registros = data.response ?? {};
      const valores = Object.values(registros);
      const registro = clavesRegistroEvento(doc).map((k) => registros[k]).find(Boolean) ?? (valores.length === 1 ? valores[0] : null);
      return { ok: true, registro: registro ?? null };
    } catch (error) {
      return { ok: false, error: error instanceof Error ? error.message : 'Error desconocido' };
    }
  }

  /** Registra la alerta de un evento inconsistente. Un error acá no corta el sync. */
  private async procesarAlertaEvento(
    empresaId: string,
    tipoDocumento: TipoDocumento,
    doc: { id: string; dest: string | null; dpunexp: string | null; dnumdoc: string | null; evento_aplicado: string | null },
    clasificacion: { caso: CasoEventoPendiente; detalle: string },
    sucursalPorEstablecimiento: Map<string, string>,
  ): Promise<void> {
    const { caso, detalle } = clasificacion;
    if (caso === 'respondido' || caso === 'en_espera') return;
    try {
      await this.alertasEmpresa.registrar(
        alertaDeEvento({
          empresaId,
          tipoDocumento,
          doc,
          caso,
          detalle,
          sucursalId: (doc.dest && sucursalPorEstablecimiento.get(doc.dest)) || null,
        }),
      );
    } catch (err) {
      this.logger.error(
        `No se pudo registrar la alerta del evento ${tipoDocumento}:${doc.id}: ${err instanceof Error ? err.message : err}`,
        'MiddlewareSifenService',
      );
    }
  }

  /**
   * Revierte el estado de la orden de venta vinculada a una factura que fue
   * cancelada (ECAN) o inutilizada (EINU) por SIFEN.
   * - Restaura cant_pendiente en pedido_detalle (+ cantidad facturada)
   * - Recalcula estado_orden: 'aprobada' si todo fue revertido, 'facturada_parcial' si quedan otras facturas
   */
  private async revertirOrdenVentaDesdeFactura(facturaId: string, empresaId: string): Promise<void> {
    const factura = await this.prisma.factura_cab.findFirst({
      where: { id: facturaId, empresa_id: empresaId },
      select: { pedido_id: true, dest: true, dpunexp: true, dnumdoc: true },
    });
    if (!factura?.pedido_id) return;

    const orden = await this.prisma.pedidos.findFirst({
      where: { id: factura.pedido_id, empresa_id: empresaId, tipo_pedido: 'orden_venta' },
      select: { id: true, estado_orden: true },
    });
    if (!orden) return;

    // Items de la factura cancelada
    const facturaDets = await this.prisma.factura_det.findMany({
      where: { factura_cab_id: facturaId },
      select: { producto_id: true, dcantproser: true },
    });

    // Todos los ítems de la orden para poder restaurar cant_pendiente
    const pedidoDets = await this.prisma.pedido_detalle.findMany({
      where: { pedido_id: orden.id },
      select: { id: true, producto_id: true, cantidad: true, cant_pendiente: true },
    });

    for (const fd of facturaDets) {
      const det = pedidoDets.find((d) => d.producto_id === fd.producto_id);
      if (!det) continue;
      const cantidadFacturada = Number(fd.dcantproser ?? 0);
      const cantidadOriginal = Number(det.cantidad);
      const cantPendienteActual = det.cant_pendiente != null ? Number(det.cant_pendiente) : 0;
      // Restaurar lo que se descontó, sin superar la cantidad original de la orden
      const nuevaCantPendiente = Math.min(cantPendienteActual + cantidadFacturada, cantidadOriginal);
      await this.prisma.pedido_detalle.update({
        where: { id: det.id },
        data: { cant_pendiente: nuevaCantPendiente },
      });
    }

    // Releer para calcular el nuevo estado de la orden
    const detallesActualizados = await this.prisma.pedido_detalle.findMany({
      where: { pedido_id: orden.id },
      select: { cantidad: true, cant_pendiente: true },
    });
    const todoPendiente = detallesActualizados.every(
      (d) => (d.cant_pendiente != null ? Number(d.cant_pendiente) : Number(d.cantidad)) >= Number(d.cantidad),
    );
    // Si algún ítem aún tiene cant_pendiente < cantidad → otra factura cubre esa diferencia
    const nuevoEstado = todoPendiente ? 'aprobada' : 'facturada_parcial';

    const facturaNumero = `${factura.dest}-${factura.dpunexp}-${factura.dnumdoc}`;
    await this.prisma.pedidos.update({
      where: { id: orden.id },
      data: { estado_orden: nuevoEstado, updated_at: new Date() },
    });
    await this.prisma.pedido_historial.create({
      data: {
        pedido_id: orden.id,
        accion: nuevoEstado,
        descripcion: `Revertido a "${nuevoEstado}" por cancelación/inutilización SIFEN de factura ${facturaNumero}`,
        datos: { factura_id: facturaId, factura_numero: facturaNumero },
      },
    });
    this.logger.log(
      `Orden de venta ${orden.id} revertida a "${nuevoEstado}" por SIFEN factura ${facturaNumero}`,
      'MiddlewareSifenService',
    );
  }

  // Métodos de conveniencia
  async sincronizarEventosSifen(empresaId: string) {
    return this.sincronizarSifen(empresaId, 'factura', 'evento');
  }

  async sincronizarEventosSifenNC(empresaId: string) {
    return this.sincronizarSifen(empresaId, 'nota_credito', 'evento');
  }

  async sincronizarEstadoSifenNC(empresaId: string, nroLote?: string) {
    return this.sincronizarSifen(empresaId, 'nota_credito', 'documento', nroLote);
  }

  /**
   * Revierte stock de una factura cancelada/inutilizada (ECAN/EINU aprobado).
   * Incrementa stock_deposito, devuelve los lotes consumidos por FIFO y registra
   * movimiento tipo devolución.
   *
   * Ojo: este es un camino de anulación DISTINTO al de `FacturasService.anular`.
   * Ambos deben revertir lo mismo — antes acá se reintegraba `stock_deposito` pero
   * NO los lotes, dejando el stock del depósito y el de los lotes desincronizados.
   */
  /**
   * Reversión completa de una factura anulada/cancelada. ÚNICO punto de verdad.
   *
   * Antes esto estaba implementado tres veces y ninguna cubría todo:
   *
   *   | reversión          | sincronizarSifen | esperarYConsultarEstado | FacturasService.anular |
   *   | orden de venta     |        si        |           si            |          si            |
   *   | caja               |        si        |           si            |          si            |
   *   | stock + lotes      |        si        |           si            |          si            |
   *   | contabilidad       |        si        |           si            |          si            |
   *   | cuotas             |        NO        |           si            |          si            |
   *   | cuentas a cobrar   |        NO        |           si            |          si            |
   *   | saldo del cliente  |        NO        |           si            |          si            |
   *   | comisiones         |        NO        |           NO            |          si            |
   *
   * Consecuencia real (DOBA, factura 001-001-0002374): cancelada en SIFEN y
   * viva en el ERP con Gs. 396.000 de saldo. Y cualquier factura cancelada
   * desde la UI quedaba con las comisiones del vendedor sin anular, porque la
   * pantalla usa el camino de eventos SIFEN y nunca pasa por FacturasService.
   *
   * Todo va con captura de error propia: que falle una reversión no puede
   * impedir las demás ni dejar la factura sin anular. Todas son idempotentes,
   * así que llamar de más no duplica.
   */
  async revertirFacturaAnulada(
    facturaId: string,
    empresaId: string,
    opciones: { motivo: string; usuarioId?: string | null; revertirContabilidad?: boolean } = {
      motivo: 'Anulación',
    },
  ): Promise<void> {
    const { motivo, usuarioId = null, revertirContabilidad = true } = opciones;
    const fallo = (que: string) => (err: unknown) =>
      this.logger.error(
        `Reversión de factura ${facturaId} — ${que} falló: ${(err as Error)?.message}`,
        'MiddlewareSifenService',
      );

    await this.revertirOrdenVentaDesdeFactura(facturaId, empresaId).catch(fallo('orden de venta'));
    await this.reversionCaja.revertirCajaFactura(facturaId, empresaId, motivo).catch(fallo('caja'));
    await this.revertirStockFactura(facturaId, empresaId).catch(fallo('stock'));

    // Cuotas y cuenta corriente: sin esto la cartera sigue mostrando deuda de
    // una venta que ya no existe. En la base conviven 'Pendiente' y 'pendiente'
    // (y hay 'parcial'): se anula todo lo que no esté pagado.
    try {
      await this.prisma.factura_cuotas.updateMany({
        where: {
          factura_cab_id: facturaId,
          estado: { notIn: ['pagado', 'Pagado', 'pagada', 'Pagada', EstadoFactura.ANULADA] },
        },
        data: { estado: EstadoFactura.ANULADA },
      });
      await this.prisma.cuentas_cobrar.updateMany({
        where: { factura_venta_id: facturaId, empresa_id: empresaId },
        data: { estado: 'anulada', updated_at: new Date() },
      });
    } catch (err) {
      fallo('cuotas / cuentas a cobrar')(err);
    }

    // Baja el saldo de la cuenta y del cliente. El sincronizador respeta el
    // estado 'anulada' que se acaba de escribir.
    try {
      await sincronizarCuentaCobrar(this.prisma as unknown as TxSincronizacion, facturaId);
    } catch (err) {
      fallo('saldo de cuenta corriente')(err);
    }

    // `revertirContabilidad: false` lo usa quien ya revirtió el asiento por su
    // cuenta (p. ej. una factura que nunca fue a SIFEN y lo hace con el usuario
    // humano, no con null).
    if (revertirContabilidad) {
      await this.contabilidadIntegracion
        .revertirDocumento('factura_cab', facturaId, usuarioId)
        .catch(fallo('contabilidad'));
    }

    await this.asignacionFacturas
      .cancelarComisionesPorFactura(facturaId, empresaId)
      .catch(fallo('comisiones'));
  }

  private async revertirStockFactura(facturaId: string, empresaId: string): Promise<void> {
    const detalles = await this.prisma.factura_det.findMany({
      where: { factura_cab_id: facturaId },
      include: { productos: { select: { maneja_inventario: true } } },
    });
    const factura = await this.prisma.factura_cab.findUnique({
      where: { id: facturaId },
      select: { dest: true, dpunexp: true, dnumdoc: true },
    });
    const tipoMovDev = await this.prisma.tipo_movimiento_inventario.findFirst({
      where: {
        OR: [
          { codigo: 'DEVOLUCION' },
          { descripcion: { contains: 'Devolución', mode: 'insensitive' } },
          { descripcion: { contains: 'Entrada', mode: 'insensitive' } },
        ],
      },
    });
    const depositoPrincipalFact = await this.prisma.depositos.findFirst({
      where: { empresa_id: empresaId, es_principal: true, active: true },
    });
    for (const det of detalles) {
      if (!det.productos?.maneja_inventario) continue;
      const depositoId = det.deposito_id ?? depositoPrincipalFact?.id;
      if (!depositoId) continue;
      const cantidad = Number(det.dcantproser);
      await this.prisma.stock_deposito.upsert({
        where: { deposito_id_producto_id: { deposito_id: depositoId, producto_id: det.producto_id } },
        create: { deposito_id: depositoId, producto_id: det.producto_id, cantidad_disponible: cantidad },
        update: { cantidad_disponible: { increment: cantidad }, updated_at: new Date() },
      });

      // Devolver los lotes que la venta consumió por FIFO. Si la factura no
      // consumió lotes, `restoreFacturaDetLotes` no hace nada. Se aísla en
      // try/catch para que un problema de lotes no deje la cancelación a medias:
      // el stock ya se reintegró y el evento SIFEN ya está aprobado.
      try {
        const sucursalDep = await this.prisma.depositos.findFirst({
          where: { id: depositoId, empresa_id: empresaId, active: true },
          select: { sucursal_id: true },
        });
        const lotesEnabled = sucursalDep?.sucursal_id
          ? await this.lotesService.isEnabledForEmpresa(empresaId, sucursalDep.sucursal_id)
          : false;
        if (lotesEnabled) {
          await this.lotesService.restoreFacturaDetLotes(this.prisma, {
            facturaDetId: det.id,
            depositoId,
            cantidad,
          });
        }
      } catch (err) {
        this.logger.error(
          `No se pudieron restaurar lotes al cancelar factura ${facturaId} (detalle ${det.id}): ${(err as Error).message}`,
          'MiddlewareSifenService',
        );
      }

      if (tipoMovDev) {
        const nroDoc = factura ? `${factura.dest}-${factura.dpunexp}-${factura.dnumdoc}` : facturaId;
        await this.prisma.movimientos_inventario.create({
          data: {
            deposito_origen_id: depositoId,
            producto_id: det.producto_id,
            tipo_movimiento_id: tipoMovDev.id,
            cantidad,
            documento_origen: 'factura_venta',
            documento_id: facturaId,
            observaciones: `Reversión stock por cancelación Fact. ${nroDoc}`,
          },
        });
      }
    }
    this.logger.log(`Stock revertido por ECAN/EINU factura ${facturaId}`, 'MiddlewareSifenService');
  }

  /**
   * Revierte stock de una NC cancelada (ECAN aprobado).
   * Decrementa stock_deposito (deshace el retorno que hizo la NC) y registra movimiento.
   */
  private async revertirStockNC(ncId: string, empresaId: string): Promise<void> {
    const ncCab = await this.prisma.nota_credito_cab.findUnique({
      where: { id: ncId },
      select: { dest: true, dpunexp: true, dnumdoc: true, factura_cab_id: true },
    });
    const detallesNC = await this.prisma.nota_credito_det.findMany({
      where: { nota_credito_cab_id: ncId },
      include: { productos: { select: { maneja_inventario: true } } },
    });
    const depositosPorItem = new Map<string, string>();
    if (ncCab?.factura_cab_id) {
      const detallesFactura = await this.prisma.factura_det.findMany({
        where: { factura_cab_id: ncCab.factura_cab_id },
        select: { producto_id: true, deposito_id: true },
      });
      for (const det of detallesFactura) {
        if (det.deposito_id) depositosPorItem.set(det.producto_id, det.deposito_id);
      }
    }
    const depositoPrincipal = await this.prisma.depositos.findFirst({
      where: { empresa_id: empresaId, es_principal: true, active: true },
    });
    const tipoMovVenta = await this.prisma.tipo_movimiento_inventario.findFirst({
      where: {
        OR: [
          { codigo: 'VENTA' },
          { descripcion: { contains: 'Venta', mode: 'insensitive' } },
          { descripcion: { contains: 'Salida', mode: 'insensitive' } },
        ],
      },
    });
    for (const det of detallesNC) {
      if (!det.productos?.maneja_inventario) continue;
      const depositoId = det.deposito_id ?? depositosPorItem.get(det.producto_id) ?? depositoPrincipal?.id;
      if (!depositoId) continue;
      const cantidad = Number(det.dcantproser);
      await this.prisma.stock_deposito.upsert({
        where: { deposito_id_producto_id: { deposito_id: depositoId, producto_id: det.producto_id } },
        create: { deposito_id: depositoId, producto_id: det.producto_id, cantidad_disponible: -cantidad },
        update: { cantidad_disponible: { decrement: cantidad }, updated_at: new Date() },
      });
      if (tipoMovVenta) {
        const nroNC = ncCab ? `${ncCab.dest}-${ncCab.dpunexp}-${ncCab.dnumdoc}` : ncId;
        await this.prisma.movimientos_inventario.create({
          data: {
            deposito_origen_id: depositoId,
            producto_id: det.producto_id,
            tipo_movimiento_id: tipoMovVenta.id,
            cantidad,
            documento_origen: 'nota_credito',
            documento_id: ncId,
            observaciones: `Reversión stock por cancelación NC ${nroNC}`,
          },
        });
      }
    }
    this.logger.log(`Stock revertido por ECAN NC ${ncId}`, 'MiddlewareSifenService');
  }

  /**
   * Enviar evento de inutilización (EINU) - Para documentos rechazados
   */
  async enviarEventoInutilizacion(
    empresaId: string,
    facturaId: string,
    motivo: string,
  ): Promise<{ success: boolean; message: string; data?: unknown }> {
    const factura = (await this.prisma.factura_cab.findFirst({
      where: { id: facturaId, empresa_id: empresaId },
    })) as Record<string, unknown> | null;

    if (!factura) {
      return { success: false, message: 'Factura no encontrada' };
    }

    if (factura.estado_sifen !== 'Rechazado') {
      return {
        success: false,
        message: 'Solo se pueden inutilizar documentos rechazados',
      };
    }

    const nroLote = `einu-${Date.now()}`;
    const payload = {
      nro_lote: nroLote,
      tipOpe: '3',
      iTiDE: '1',
      tipoEve: 'EINU',
      comprobantes: [
        {
          dNumTim: await this.timbradoParaEvento(empresaId, factura.cdc as string | null),
          dEst: factura.dest,
          dPunExp: factura.dpunexp,
          dNumIn: factura.dnumdoc,
          dNumFin: factura.dnumdoc,
          dSerieNum: null,
          mOtEve: motivo,
        },
      ],
    };

    try {
      // Guardar estado inicial como Pendiente
      await this.prisma.factura_cab.update({
        where: { id: facturaId },
        data: {
          evento_aplicado: 'EINU',
          estado_evento: 'Pendiente',
          mensaje_evento: 'Enviando evento de inutilización...',
          fecha_evento: new Date(),
        },
      });

      const response = await this.sendEventoToMiddleware(empresaId, payload);

      // Esperar 3 segundos y consultar estado
      const resultado = await this.esperarYConsultarEstado(
        empresaId,
        facturaId,
        {
          dest: factura.dest as string,
          dpunexp: factura.dpunexp as string,
          dnumdoc: factura.dnumdoc as string,
        },
        'EINU',
      );

      this.logger.log(
        `Evento EINU enviado: factura ${facturaId} | Estado: ${resultado.estado} | Empresa: ${empresaId}`,
        'MiddlewareSifenService',
      );
      await this.auditService.log({
        empresa_id: empresaId,
        action: 'UPDATE',
        entity_type: 'factura',
        entity_id: facturaId,
        descripcion: `Evento inutilización (EINU) enviado | Motivo: ${motivo} | Estado: ${resultado.estado}`,
        new_value: { evento: 'EINU', estado: resultado.estado, motivo },
      });

      return {
        success: resultado.estado === 'Aprobado',
        message: resultado.mensaje,
        data: { response, estadoEvento: resultado },
      };
    } catch (error) {
      const msg = error instanceof Error ? error.message : 'Error desconocido';
      // Actualizar estado como error
      await this.prisma.factura_cab.update({
        where: { id: facturaId },
        data: {
          estado_evento: 'Rechazado',
          mensaje_evento: `Error: ${msg}`,
        },
      });
      return { success: false, message: `Error al enviar evento: ${msg}` };
    }
  }

  /**
   * Enviar evento de cancelación (ECAN) - Para documentos aprobados (< 48h)
   */
  async enviarEventoCancelacion(
    empresaId: string,
    facturaId: string,
    motivo: string,
  ): Promise<{ success: boolean; message: string; data?: unknown }> {
    const factura = (await this.prisma.factura_cab.findFirst({
      where: { id: facturaId, empresa_id: empresaId },
    })) as Record<string, unknown> | null;

    if (!factura) {
      return { success: false, message: 'Factura no encontrada' };
    }

    if (factura.estado_sifen !== 'Aprobado') {
      return {
        success: false,
        message: 'Solo se pueden cancelar documentos aprobados',
      };
    }

    if (!factura.cdc) {
      return { success: false, message: 'El documento no tiene CDC asignado' };
    }

    // Verificar que no hayan pasado más de 48 horas desde el envío
    if (factura.fecha_envio_sifen) {
      const horasTranscurridas =
        (Date.now() - new Date(factura.fecha_envio_sifen as string | Date).getTime()) / (1000 * 60 * 60);
      if (horasTranscurridas > 48) {
        return {
          success: false,
          message: 'No se puede cancelar: han pasado más de 48 horas desde el envío',
        };
      }
    }

    const nroLote = `ecan-${Date.now()}`;
    const payload = {
      nro_lote: nroLote,
      tipOpe: '3',
      iTiDE: '1',
      tipoEve: 'ECAN',
      comprobantes: [
        {
          cdc: factura.cdc,
          mOtEve: motivo,
        },
      ],
    };

    try {
      // Guardar estado inicial como Pendiente
      await this.prisma.factura_cab.update({
        where: { id: facturaId },
        data: {
          evento_aplicado: 'ECAN',
          estado_evento: 'Pendiente',
          mensaje_evento: 'Enviando evento de cancelación...',
          fecha_evento: new Date(),
        },
      });

      const response = await this.sendEventoToMiddleware(empresaId, payload);

      // Esperar 3 segundos y consultar estado
      const resultado = await this.esperarYConsultarEstado(
        empresaId,
        facturaId,
        { cdc: factura.cdc as string },
        'ECAN',
      );

      this.logger.log(
        `Evento ECAN enviado: factura ${facturaId} | Estado: ${resultado.estado} | Empresa: ${empresaId}`,
        'MiddlewareSifenService',
      );
      await this.auditService.log({
        empresa_id: empresaId,
        action: 'UPDATE',
        entity_type: 'factura',
        entity_id: facturaId,
        descripcion: `Evento cancelación (ECAN) enviado | Motivo: ${motivo} | Estado: ${resultado.estado}`,
        new_value: { evento: 'ECAN', estado: resultado.estado, motivo },
      });

      return {
        success: resultado.estado === 'Aprobado',
        message: resultado.mensaje,
        data: { response, estadoEvento: resultado },
      };
    } catch (error) {
      const msg = error instanceof Error ? error.message : 'Error desconocido';
      await this.prisma.factura_cab.update({
        where: { id: facturaId },
        data: {
          estado_evento: 'Rechazado',
          mensaje_evento: `Error: ${msg}`,
        },
      });
      return { success: false, message: `Error al enviar evento: ${msg}` };
    }
  }

  /**
   * Enviar evento de nominación (EINO) - Para documentos aprobados innominados
   */
  async enviarEventoNominacion(
    empresaId: string,
    facturaId: string,
    datosCliente: {
      motivo: string;
      clienteId?: string; // ID del cliente en nuestra BD para actualizar la factura
      iNatRec: number;
      iTiOpe: number;
      cPaisRec: string;
      iTiContRec?: number | null;
      dRucRec?: string | null;
      dDVRec?: string | null;
      iTipIDRec: number;
      dNumIDRec: string;
      dNomRec: string;
      dNomFanRec?: string | null;
      dDirRec: string;
      dNumCasRec?: string | null;
      cDepRec?: string | null;
      cDisRec?: string | null;
      cCiuRec?: string | null;
      dTelRec?: string | null;
      dCelRec?: string | null;
      dEmailRec?: string | null;
      dCodCliente?: string | null;
    },
  ): Promise<{ success: boolean; message: string; data?: unknown }> {
    const factura = await this.prisma.factura_cab.findFirst({
      where: { id: facturaId, empresa_id: empresaId },
    });

    if (!factura) {
      return { success: false, message: 'Factura no encontrada' };
    }

    if (factura.estado_sifen !== 'Aprobado') {
      return {
        success: false,
        message: 'Solo se pueden nominar documentos aprobados',
      };
    }

    if (!factura.cdc) {
      return { success: false, message: 'El documento no tiene CDC asignado' };
    }

    const nroLote = `eino-${Date.now()}`;
    const payload = {
      nro_lote: nroLote,
      tipOpe: '3',
      iTiDE: '1',
      tipoEve: 'EINO',
      comprobantes: [
        {
          cdc: factura.cdc,
          mOtEve: datosCliente.motivo,
          iNatRec: datosCliente.iNatRec,
          iTiOpe: datosCliente.iTiOpe,
          cPaisRec: datosCliente.cPaisRec,
          iTiContRec: datosCliente.iTiContRec,
          dRucRec: datosCliente.dRucRec,
          dDVRec: datosCliente.dDVRec,
          iTipIDRec: datosCliente.iTipIDRec,
          dNumIDRec: datosCliente.dNumIDRec,
          dNomRec: datosCliente.dNomRec,
          dNomFanRec: datosCliente.dNomFanRec,
          dDirRec: datosCliente.dDirRec,
          dNumCasRec: datosCliente.dNumCasRec || '0',
          cDepRec: datosCliente.cDepRec,
          cDisRec: datosCliente.cDisRec,
          cCiuRec: datosCliente.cCiuRec,
          dTelRec: datosCliente.dTelRec,
          dCelRec: datosCliente.dCelRec,
          dEmailRec: datosCliente.dEmailRec,
          dCodCliente: datosCliente.dCodCliente,
        },
      ],
    };

    try {
      // Guardar estado inicial como Pendiente
      await this.prisma.factura_cab.update({
        where: { id: facturaId },
        data: {
          evento_aplicado: 'EINO',
          estado_evento: 'Pendiente',
          mensaje_evento: `Enviando evento de nominación a: ${datosCliente.dNomRec}...`,
          fecha_evento: new Date(),
          cliente_nominado_id: datosCliente.clienteId || null,
        },
      });

      const response = await this.sendEventoToMiddleware(empresaId, payload);

      // Esperar 3 segundos y consultar estado (pasando el clienteId para actualizar si se aprueba)
      const resultado = await this.esperarYConsultarEstado(
        empresaId,
        facturaId,
        { cdc: factura.cdc },
        'EINO',
        datosCliente.clienteId,
      );

      this.logger.log(
        `Evento EINO enviado: factura ${facturaId} | Estado: ${resultado.estado} | Empresa: ${empresaId}`,
        'MiddlewareSifenService',
      );
      await this.auditService.log({
        empresa_id: empresaId,
        action: 'UPDATE',
        entity_type: 'factura',
        entity_id: facturaId,
        descripcion: `Evento nominación (EINO) enviado | Receptor: ${datosCliente.dNomRec} | Estado: ${resultado.estado}`,
        new_value: { evento: 'EINO', estado: resultado.estado, receptor: datosCliente.dNomRec },
      });

      return {
        success: resultado.estado === 'Aprobado',
        message: resultado.mensaje,
        data: { response, estadoEvento: resultado },
      };
    } catch (error) {
      const msg = error instanceof Error ? error.message : 'Error desconocido';
      await this.prisma.factura_cab.update({
        where: { id: facturaId },
        data: {
          estado_evento: 'Rechazado',
          mensaje_evento: `Error: ${msg}`,
        },
      });
      return { success: false, message: `Error al enviar evento: ${msg}` };
    }
  }

  /**
   * Método auxiliar para enviar eventos al middleware
   */
  private async sendEventoToMiddleware(empresaId: string, payload: Record<string, unknown>): Promise<unknown> {
    const url = envs.middlewareSifenUrl;
    if (!url) throw new Error('MIDDLEWARE_SIFEN_URL no está configurado');

    const responseLogin = await this.login(empresaId);
    const tokenMiddleware = (responseLogin as { token?: string })?.token || null;

    const response = await fetch(`${url}/api/maintenance`, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        ...(tokenMiddleware ? { authorization: `Bearer ${tokenMiddleware}` } : {}),
      },
      body: JSON.stringify(payload),
    });

    const text = await response.text();
    if (!response.ok) {
      throw new Error(`Error del middleware: ${text}`);
    }

    return text ? JSON.parse(text) : null;
  }

  /**
   * Consultar estado de un evento SIFEN
   */
  /**
   * SIFEN responde 4003 ("CDC ya se encuentra con el mismo evento solicitado")
   * cuando el evento YA está registrado. Es la respuesta correcta ante un
   * reenvío idempotente, no un fallo: el documento está cancelado.
   *
   * Tratarlo como rechazo dejaba el documento vigente en el ERP. Caso real: la
   * factura 001-001-0002374 de DOBA se canceló en e-kuatia a las 17:20 y el ERP
   * reintentó el ECAN a las 17:40; SIFEN contestó 4003, el ERP lo leyó como
   * rechazo y la factura siguió viva con Gs. 396.000 de saldo. Peor: el
   * `estado_evento: 'Rechazado'` resultante la dejó fuera del set de reintentos
   * del job, que sólo mira los 'Pendiente'.
   */
  private eventoYaRegistrado(codRespuesta?: string | null, mensaje?: string | null): boolean {
    if (String(codRespuesta ?? '').trim() === '4003') return true;
    return /ya se encuentra con el mismo evento/i.test(mensaje ?? '');
  }

  private async consultarEstadoEvento(
    empresaId: string,
    factura: {
      cdc?: string | null;
      dest?: string;
      dpunexp?: string;
      dnumdoc?: string;
    },
    tipoEvento: 'EINU' | 'ECAN' | 'EINO',
    iTiDE: number,
  ): Promise<{ estado: string; mensaje: string }> {
    const url = envs.middlewareSifenUrl;
    if (!url) throw new Error('MIDDLEWARE_SIFEN_URL no está configurado');

    const responseLogin = await this.login(empresaId);
    const tokenMiddleware = (responseLogin as { token?: string })?.token || null;

    // Misma consulta y misma lectura que el sync de pendientes: antes acá se
    // leía `estado` en la raíz de la respuesta, que en el formato real no
    // existe, y el chequeo devolvía siempre "Pendiente".
    const consulta = await this.consultarEventoEnMiddleware(url, tokenMiddleware, { ...factura, evento_aplicado: tipoEvento }, iTiDE);
    if (!consulta.ok) return { estado: 'Pendiente', mensaje: 'Error al consultar estado' };

    const retorno = consulta.registro?.retorno_sifen;
    // 4003 = el evento ya estaba aplicado: se normaliza a Aprobado para que el
    // documento se cancele localmente en vez de quedar vigente.
    if (this.eventoYaRegistrado(retorno?.codrespuesta, retorno?.mensaje)) {
      return { estado: 'Aprobado', mensaje: retorno?.mensaje || 'El evento ya estaba registrado en SIFEN' };
    }
    if (retorno?.estado === 'Aprobado' || retorno?.estado === 'Rechazado') {
      return { estado: retorno.estado, mensaje: retorno.mensaje || `Evento ${retorno.estado.toLowerCase()}` };
    }
    return { estado: 'Pendiente', mensaje: 'Esperando respuesta de SIFEN' };
  }

  /**
   * Esperar y consultar estado del evento (con timeout de 3 segundos)
   */
  private async esperarYConsultarEstado(
    empresaId: string,
    facturaId: string,
    factura: {
      cdc?: string | null;
      dest?: string;
      dpunexp?: string;
      dnumdoc?: string;
    },
    tipoEvento: 'EINU' | 'ECAN' | 'EINO',
    clienteNominadoId?: string,
  ): Promise<{ estado: string; mensaje: string }> {
    // Esperar 3 segundos
    await new Promise((resolve) => setTimeout(resolve, 3000));

    // Consultar estado
    const resultado = await this.consultarEstadoEvento(empresaId, factura, tipoEvento, DOCUMENTO_CONFIG.factura.iTiDE);

    // Actualizar factura con el resultado
    const updateData: Record<string, unknown> = {
      estado_evento: resultado.estado,
      mensaje_evento: resultado.mensaje,
    };

    // Si el evento fue aprobado, actualizar estado_sifen según el tipo
    if (resultado.estado === 'Aprobado') {
      if (tipoEvento === 'EINU') {
        updateData.estado_sifen = 'Inutilizado';
        updateData.estado = 'Inutilizado';
      } else if (tipoEvento === 'ECAN') {
        updateData.estado_sifen = 'Cancelado';
        updateData.estado = 'Cancelado';
      } else if (tipoEvento === 'EINO') {
        updateData.estado_sifen = 'Nominado';
        if (clienteNominadoId) {
          updateData.cliente_id = clienteNominadoId;
          updateData.cliente_nominado_id = clienteNominadoId;
        }
      }
    }

    await this.prisma.factura_cab.update({
      where: { id: facturaId },
      data: updateData,
    });

    // Reversión completa por el punto único: orden de venta, caja, stock, cuotas,
    // cuenta corriente, contabilidad y comisiones. Antes este camino hacía todo
    // menos las comisiones, y es el que usa la pantalla ("Eventos SIFEN"), así
    // que toda factura cancelada desde la UI dejaba viva la comisión del vendedor.
    if (resultado.estado === 'Aprobado' && (tipoEvento === 'ECAN' || tipoEvento === 'EINU')) {
      await this.revertirFacturaAnulada(facturaId, empresaId, {
        motivo: `Evento ${tipoEvento} aprobado por SIFEN`,
        usuarioId: null,
        // El asiento sólo se revierte en cancelación: una inutilización no tenía
        // asiento que revertir.
        revertirContabilidad: tipoEvento === 'ECAN',
      });
    }

    return resultado;
  }

  /**
   * Método auxiliar para esperar y consultar estado del evento para notas de crédito
   */
  private async esperarYConsultarEstadoNC(
    empresaId: string,
    notaCreditoId: string,
    notaCredito: {
      cdc?: string | null;
      dest?: string;
      dpunexp?: string;
      dnumdoc?: string;
    },
    tipoEvento: 'EINU' | 'ECAN',
  ): Promise<{ estado: string; mensaje: string }> {
    await new Promise((resolve) => setTimeout(resolve, 3000));

    const resultado = await this.consultarEstadoEvento(empresaId, notaCredito, tipoEvento, DOCUMENTO_CONFIG.nota_credito.iTiDE);

    const updateData: Record<string, unknown> = {
      estado_evento: resultado.estado,
      mensaje_evento: resultado.mensaje,
    };

    if (resultado.estado === 'Aprobado') {
      if (tipoEvento === 'EINU') {
        updateData.estado_sifen = 'Inutilizado';
        updateData.estado = 'Inutilizado';
      } else if (tipoEvento === 'ECAN') {
        updateData.estado_sifen = 'Cancelado';
        updateData.estado = 'Cancelado';
      }
    }

    await this.prisma.nota_credito_cab.update({
      where: { id: notaCreditoId },
      data: updateData,
    });

    // === REVERSIÓN DE STOCK para ECAN/EINU aprobados en NC ===
    // Cuando una NC se cancela/inutiliza, el stock que ella devolvió debe descontarse de nuevo
    if (resultado.estado === 'Aprobado' && (tipoEvento === 'ECAN' || tipoEvento === 'EINU')) {
      try {
        const ncCab = await this.prisma.nota_credito_cab.findUnique({
          where: { id: notaCreditoId },
          select: { factura_cab_id: true },
        });

        const detallesNC = await this.prisma.nota_credito_det.findMany({
          where: { nota_credito_cab_id: notaCreditoId },
          include: { productos: { select: { maneja_inventario: true } } },
        });

        // Fallback: mapear deposito_id desde la factura original (para NCs sin deposito_id)
        const depositosPorItem: Map<string, string> = new Map();
        if (ncCab?.factura_cab_id) {
          const detallesFactura = await this.prisma.factura_det.findMany({
            where: { factura_cab_id: ncCab.factura_cab_id },
            select: { producto_id: true, deposito_id: true },
          });
          for (const det of detallesFactura) {
            if (det.deposito_id) depositosPorItem.set(det.producto_id, det.deposito_id);
          }
        }

        const depositoPrincipal = await this.prisma.depositos.findFirst({
          where: { empresa_id: empresaId, es_principal: true, active: true },
        });

        const tipoMovVenta = await this.prisma.tipo_movimiento_inventario.findFirst({
          where: {
            OR: [
              { codigo: 'VENTA' },
              { descripcion: { contains: 'Venta', mode: 'insensitive' } },
              { descripcion: { contains: 'Salida', mode: 'insensitive' } },
            ],
          },
        });

        for (const det of detallesNC) {
          if (!det.productos?.maneja_inventario) continue;
          // Prioridad: nota_credito_det.deposito_id > factura_det.deposito_id > deposito principal
          const depositoId = det.deposito_id ?? depositosPorItem.get(det.producto_id) ?? depositoPrincipal?.id;
          if (!depositoId) continue;
          const cantidad = Number(det.dcantproser);
          await this.prisma.stock_deposito.upsert({
            where: { deposito_id_producto_id: { deposito_id: depositoId, producto_id: det.producto_id } },
            create: { deposito_id: depositoId, producto_id: det.producto_id, cantidad_disponible: -cantidad },
            update: { cantidad_disponible: { decrement: cantidad }, updated_at: new Date() },
          });
          if (tipoMovVenta) {
            await this.prisma.movimientos_inventario.create({
              data: {
                deposito_origen_id: depositoId,
                producto_id: det.producto_id,
                tipo_movimiento_id: tipoMovVenta.id,
                cantidad: cantidad,
                documento_origen: 'nota_credito',
                documento_id: notaCreditoId,
                observaciones: `Reversión stock NC por ${tipoEvento} aprobado`,
              },
            });
          }
        }
        this.logger.log(`Stock NC revertido por ${tipoEvento}: NC ${notaCreditoId}`, 'MiddlewareSifenService');
      } catch (stockError) {
        const msg = stockError instanceof Error ? stockError.message : 'Error';
        this.logger.error(`Error reversión stock NC ${tipoEvento} ${notaCreditoId}: ${msg}`, 'MiddlewareSifenService');
      }
    }

    // Reversión contable cuando ECAN aprobado en NC
    if (resultado.estado === 'Aprobado' && tipoEvento === 'ECAN') {
      this.contabilidadIntegracion
        .revertirDocumento('nota_credito_cab', notaCreditoId, 'SIFEN')
        .catch((err) =>
          this.logger.error(
            `Error reversión contable ECAN NC ${notaCreditoId}: ${err.message}`,
            'MiddlewareSifenService',
          ),
        );
    }

    return resultado;
  }

  /**
   * Enviar evento de cancelación para nota de crédito (ECAN)
   */
  async enviarEventoCancelacionNC(
    empresaId: string,
    notaCreditoId: string,
    motivo: string,
  ): Promise<{ success: boolean; message: string; data?: unknown }> {
    const notaCredito = await this.prisma.nota_credito_cab.findFirst({
      where: { id: notaCreditoId, empresa_id: empresaId },
    });

    if (!notaCredito) {
      return { success: false, message: 'Nota de crédito no encontrada' };
    }
    console.log('notaCredito', notaCredito);

    if (notaCredito.estado_sifen !== 'Aprobado') {
      return {
        success: false,
        message: 'Solo se pueden cancelar documentos aprobados',
      };
    }

    if (!notaCredito.cdc) {
      return { success: false, message: 'El documento no tiene CDC asignado' };
    }

    if (notaCredito.fecha_envio_sifen) {
      const horasTranscurridas = (Date.now() - new Date(notaCredito.fecha_envio_sifen).getTime()) / (1000 * 60 * 60);
      if (horasTranscurridas > 48) {
        return {
          success: false,
          message: 'No se puede cancelar: han pasado más de 48 horas desde el envío',
        };
      }
    }

    const nroLote = `ecan-nc-${Date.now()}`;
    const payload = {
      nro_lote: nroLote,
      tipOpe: '3',
      iTiDE: '5',
      tipoEve: 'ECAN',
      comprobantes: [{ cdc: notaCredito.cdc, mOtEve: motivo }],
    };

    try {
      await this.prisma.nota_credito_cab.update({
        where: { id: notaCreditoId },
        data: {
          evento_aplicado: 'ECAN',
          estado_evento: 'Pendiente',
          mensaje_evento: 'Enviando evento de cancelación...',
          fecha_evento: new Date(),
        },
      });

      const response = await this.sendEventoToMiddleware(empresaId, payload);

      const resultado = await this.esperarYConsultarEstadoNC(
        empresaId,
        notaCreditoId,
        { cdc: notaCredito.cdc },
        'ECAN',
      );

      this.logger.log(
        `Evento ECAN NC enviado: nota_credito ${notaCreditoId} | Estado: ${resultado.estado} | Empresa: ${empresaId}`,
        'MiddlewareSifenService',
      );
      await this.auditService.log({
        empresa_id: empresaId,
        action: 'UPDATE',
        entity_type: 'nota_credito',
        entity_id: notaCreditoId,
        descripcion: `Evento cancelación (ECAN) NC enviado | Motivo: ${motivo} | Estado: ${resultado.estado}`,
        new_value: { evento: 'ECAN', estado: resultado.estado, motivo },
      });

      return {
        success: resultado.estado === 'Aprobado',
        message: resultado.mensaje,
        data: { response, estadoEvento: resultado },
      };
    } catch (error) {
      const msg = error instanceof Error ? error.message : 'Error desconocido';
      await this.prisma.nota_credito_cab.update({
        where: { id: notaCreditoId },
        data: { estado_evento: 'Rechazado', mensaje_evento: `Error: ${msg}` },
      });
      return { success: false, message: `Error al enviar evento: ${msg}` };
    }
  }

  /**
   * Enviar evento de inutilización para nota de crédito (EINU)
   */
  async enviarEventoInutilizacionNC(
    empresaId: string,
    notaCreditoId: string,
    motivo: string,
  ): Promise<{ success: boolean; message: string; data?: unknown }> {
    const notaCredito = await this.prisma.nota_credito_cab.findFirst({
      where: { id: notaCreditoId, empresa_id: empresaId },
    });

    if (!notaCredito) {
      return { success: false, message: 'Nota de crédito no encontrada' };
    }

    if (notaCredito.estado_sifen !== 'Rechazado') {
      return {
        success: false,
        message: 'Solo se pueden inutilizar documentos rechazados',
      };
    }

    const nroLote = `einu-nc-${Date.now()}`;
    const payload = {
      nro_lote: nroLote,
      tipOpe: '3',
      iTiDE: '5',
      tipoEve: 'EINU',
      comprobantes: [
        {
          dNumTim: await this.timbradoParaEvento(empresaId, notaCredito.cdc),
          dEst: notaCredito.dest,
          dPunExp: notaCredito.dpunexp,
          dNumIn: notaCredito.dnumdoc,
          dNumFin: notaCredito.dnumdoc,
          dSerieNum: null,
          mOtEve: motivo,
        },
      ],
    };

    try {
      await this.prisma.nota_credito_cab.update({
        where: { id: notaCreditoId },
        data: {
          evento_aplicado: 'EINU',
          estado_evento: 'Pendiente',
          mensaje_evento: 'Enviando evento de inutilización...',
          fecha_evento: new Date(),
        },
      });

      const response = await this.sendEventoToMiddleware(empresaId, payload);

      const resultado = await this.esperarYConsultarEstadoNC(
        empresaId,
        notaCreditoId,
        {
          dest: notaCredito.dest ?? '',
          dpunexp: notaCredito.dpunexp ?? '',
          dnumdoc: notaCredito.dnumdoc ?? '',
        },
        'EINU',
      );

      this.logger.log(
        `Evento EINU NC enviado: nota_credito ${notaCreditoId} | Estado: ${resultado.estado} | Empresa: ${empresaId}`,
        'MiddlewareSifenService',
      );
      await this.auditService.log({
        empresa_id: empresaId,
        action: 'UPDATE',
        entity_type: 'nota_credito',
        entity_id: notaCreditoId,
        descripcion: `Evento inutilización (EINU) NC enviado | Motivo: ${motivo} | Estado: ${resultado.estado}`,
        new_value: { evento: 'EINU', estado: resultado.estado, motivo },
      });

      return {
        success: resultado.estado === 'Aprobado',
        message: resultado.mensaje,
        data: { response, estadoEvento: resultado },
      };
    } catch (error) {
      const msg = error instanceof Error ? error.message : 'Error desconocido';
      await this.prisma.nota_credito_cab.update({
        where: { id: notaCreditoId },
        data: { estado_evento: 'Rechazado', mensaje_evento: `Error: ${msg}` },
      });
      return { success: false, message: `Error al enviar evento: ${msg}` };
    }
  }
}
