import {
  BadRequestException,
  ConflictException,
  HttpException,
  HttpStatus,
  Injectable,
  NotFoundException,
  Optional,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import {
  AccionDesvinculacionDto,
  CalcularDesvinculacionDto,
  CrearDesvinculacionDto,
  QueryDesvinculacionesDto,
  UpdateDesvinculacionDto,
} from '../dto/desvinculaciones.dto';
import { LegajosCompletitudService } from './legajos-completitud.service';
import { ParametrosService } from './parametros.service';
import { ContabilidadIntegracionService } from '../../contabilidad/services/integracion.service';
import { MapeoCuentasService } from '../../contabilidad/services/mapeo-cuentas.service';

const ORIGEN_MOVIMIENTO_TESORERIA = 'rrhh_desvinculacion';

const r2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
const num = (v: unknown, fb = 0) => {
  const n = Number(v);
  return Number.isFinite(n) ? n : fb;
};
const DIA_MS = 24 * 60 * 60 * 1000;

const diferenciaAniosMeses = (desde: Date, hasta: Date) => {
  let anios = hasta.getFullYear() - desde.getFullYear();
  let meses = hasta.getMonth() - desde.getMonth();
  let dias = hasta.getDate() - desde.getDate();
  if (dias < 0) meses -= 1;
  if (meses < 0) { anios -= 1; meses += 12; }
  return { anios: Math.max(0, anios), meses: Math.max(0, meses) };
};

const truthy = (v: unknown) => v === true || v === 'true' || v === 1 || v === '1';

/**
 * Días de preaviso según antigüedad (Art. 87 CT Paraguay).
 * Se paga si la empresa NO cumplió el preaviso al despedir.
 */
const diasPreavisoPorAntiguedad = (aniosAnt: number): number => {
  if (aniosAnt < 1) return 30;
  if (aniosAnt <= 5) return 45;
  if (aniosAnt <= 10) return 60;
  return 90;
};

/**
 * Indemnización por antigüedad: 15 días por año cumplido.
 * Fracción >= 6 meses cuenta como año completo (criterio práctico).
 */
const aniosIndemnizacion = (anios: number, meses: number): number => {
  return anios + (meses >= 6 ? 1 : 0);
};

@Injectable()
export class DesvinculacionesService {
  constructor(
    private readonly prisma: PrismaService,
    @Optional() private readonly legajosCompletitud?: LegajosCompletitudService,
    @Optional() private readonly parametros?: ParametrosService,
    @Optional() private readonly contabilidad?: ContabilidadIntegracionService,
    @Optional() private readonly mapeo?: MapeoCuentasService,
  ) {}

  private async tieneModuloTesoreria(empresaId: string): Promise<boolean> {
    const registro = await this.prisma.suscripcion_modulos.findFirst({
      where: {
        activo: true,
        modulos: { codigo: 'TESORERIA' },
        suscripciones: { empresa_id: empresaId },
      },
    });
    return !!registro;
  }

  /**
   * Registra el egreso de tesorería del finiquito (idempotente por origen) con un detalle por
   * componente (indemnización, preaviso, vacaciones, aguinaldo, salario). La integración de
   * tesorería → contabilidad genera el asiento desglosado (DEBE cada gasto / HABER banco).
   * Los `otros_descuentos` se netean contra los componentes para que el total coincida con lo
   * efectivamente pagado (total_liquidacion_final).
   */
  private async registrarMovimientoTesoreria(
    desv: {
      id: string;
      empresa_id: string;
      cuenta_tesoreria_id: string | null;
      salario_pendiente: Prisma.Decimal;
      otros_haberes: Prisma.Decimal;
      otros_descuentos: Prisma.Decimal;
      monto_vacaciones_proporcional: Prisma.Decimal;
      monto_vacaciones_no_gozadas: Prisma.Decimal;
      aguinaldo_proporcional: Prisma.Decimal;
      monto_preaviso: Prisma.Decimal;
      monto_indemnizacion: Prisma.Decimal;
      total_liquidacion_final: Prisma.Decimal;
    },
    userId?: string,
  ): Promise<void> {
    const empresaId = desv.empresa_id;
    const cuentaId = desv.cuenta_tesoreria_id;
    if (!cuentaId) {
      throw new BadRequestException(
        'Seleccioná la cuenta de tesorería desde la que se paga para confirmar el finiquito.',
      );
    }
    const cuenta = await this.prisma.tes_cuentas.findFirst({
      where: { id: cuentaId, empresa_id: empresaId, activo: true },
      select: { id: true, moneda: true },
    });
    if (!cuenta) throw new BadRequestException('Cuenta de tesorería inválida o inactiva.');
    if (cuenta.moneda !== 'PYG') {
      throw new BadRequestException(
        `El finiquito se paga en PYG. Seleccioná una cuenta de tesorería en PYG (la elegida está en ${cuenta.moneda}).`,
      );
    }

    // Idempotencia: no duplicar el movimiento si ya existe para este finiquito.
    const existente = await this.prisma.tes_movimientos.findFirst({
      where: { empresa_id: empresaId, origen_tipo: ORIGEN_MOVIMIENTO_TESORERIA, origen_id: desv.id },
    });
    if (existente) return;

    const categoria = await this.prisma.tes_categorias.findFirst({
      where: { codigo_interno: 'PAGO_FINIQUITO', is_sistema: true },
    });
    if (!categoria) {
      throw new BadRequestException(
        'Falta la categoría de tesorería "Pago de Finiquito". Ejecutá las migraciones de tesorería.',
      );
    }

    const tryMapeo = async (concepto: string): Promise<string | null> => {
      try {
        return (await this.mapeo?.getCuentaPorConcepto(empresaId, concepto)) ?? null;
      } catch {
        return null;
      }
    };

    // Componentes (bruto), en orden de prioridad para netear descuentos al final.
    const componentes = [
      { concepto: 'INDEMNIZACION', label: 'Indemnización por antigüedad', monto: num(desv.monto_indemnizacion) },
      { concepto: 'PREAVISO', label: 'Preaviso', monto: num(desv.monto_preaviso) },
      {
        concepto: 'VACACIONES_FINIQUITO',
        label: 'Vacaciones (proporcional + no gozadas)',
        monto: r2(num(desv.monto_vacaciones_proporcional) + num(desv.monto_vacaciones_no_gozadas)),
      },
      { concepto: 'AGUINALDO_FINIQUITO', label: 'Aguinaldo proporcional', monto: num(desv.aguinaldo_proporcional) },
      {
        concepto: 'SUELDOS_JORNALES',
        label: 'Salario pendiente y otros haberes',
        monto: r2(num(desv.salario_pendiente) + num(desv.otros_haberes)),
      },
    ];

    // Netear otros_descuentos contra los componentes (del último al primero) para que la suma
    // de detalles iguale lo efectivamente pagado.
    let descuentos = num(desv.otros_descuentos);
    for (let i = componentes.length - 1; i >= 0 && descuentos > 0; i--) {
      const aplicar = Math.min(componentes[i].monto, descuentos);
      componentes[i].monto = r2(componentes[i].monto - aplicar);
      descuentos = r2(descuentos - aplicar);
    }

    const detallesData: Array<{ categoria_id: string; monto: number; descripcion: string; cuenta_contable_id: string | null }> = [];
    for (const c of componentes.filter((x) => x.monto > 0)) {
      detallesData.push({
        categoria_id: categoria.id,
        monto: c.monto,
        descripcion: c.label,
        cuenta_contable_id: await tryMapeo(c.concepto),
      });
    }
    if (detallesData.length === 0) return; // nada que pagar

    const montoTotal = r2(num(desv.total_liquidacion_final));
    const movimiento = await this.prisma.$transaction(async (tx) => {
      const mov = await tx.tes_movimientos.create({
        data: {
          empresa_id: empresaId,
          cuenta_id: cuentaId,
          tipo: 'EGRESO',
          estado: 'CONFIRMADO',
          fecha: new Date(),
          monto: montoTotal,
          descripcion: 'Pago de finiquito (liquidación final)',
          origen_tipo: ORIGEN_MOVIMIENTO_TESORERIA,
          origen_id: desv.id,
          confirmado_por: userId ?? null,
          confirmado_en: new Date(),
          created_by: userId ?? null,
          detalles: { create: detallesData },
        },
      });
      await tx.tes_cuentas.update({
        where: { id: cuentaId },
        data: { saldo_actual: { increment: -montoTotal } },
      });
      return mov;
    });

    // Integración contable (self-gated por módulo CONTABILIDAD; fire-and-forget).
    this.contabilidad?.integrarMovimientoTesoreria(movimiento.id).catch(() => undefined);
  }

  // ── Listado / detalle ──────────────────────────────────
  async listar(empresaId: string, query: QueryDesvinculacionesDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const where: Prisma.rrhh_desvinculacionesWhereInput = { empresa_id: empresaId };
    if (query.estado) where.estado = query.estado;
    if (query.tipo_desvinculacion) where.tipo_desvinculacion = query.tipo_desvinculacion;
    if (query.empleado_id) where.empleado_id = query.empleado_id;

    const [data, total] = await this.prisma.$transaction([
      this.prisma.rrhh_desvinculaciones.findMany({
        where,
        skip,
        take: limit,
        orderBy: [{ fecha_ultimo_dia: 'desc' }, { created_at: 'desc' }],
        include: {
          empleado: {
            select: { id: true, numero_empleado: true, nombres: true, apellidos: true, cedula_identidad: true },
          },
        },
      }),
      this.prisma.rrhh_desvinculaciones.count({ where }),
    ]);
    return { data, total, page, limit };
  }

  async obtener(id: string, empresaId: string) {
    const desv = await this.prisma.rrhh_desvinculaciones.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        empleado: {
          select: {
            id: true, numero_empleado: true, nombres: true, apellidos: true,
            cedula_identidad: true, salario_base: true, fecha_ingreso: true, estado: true,
            cargo: { select: { id: true, nombre: true } },
            departamento: { select: { id: true, nombre: true } },
          },
        },
      },
    });
    if (!desv) throw new NotFoundException('Desvinculación no encontrada');
    return desv;
  }

  // ── Motor de cálculo (preview, sin persistir) ──────────
  async calcularPreview(empresaId: string, dto: CalcularDesvinculacionDto) {
    const emp = await this.prisma.rrhh_empleados.findFirst({
      where: { id: dto.empleado_id, empresa_id: empresaId },
    });
    if (!emp) throw new NotFoundException('Empleado no encontrado');

    const fechaUlt = new Date(dto.fecha_ultimo_dia);
    if (Number.isNaN(fechaUlt.getTime())) throw new BadRequestException('fecha_ultimo_dia inválida');
    const fechaIng = emp.fecha_ingreso ? new Date(emp.fecha_ingreso) : null;
    if (!fechaIng) throw new BadRequestException('El empleado no tiene fecha de ingreso registrada');
    if (fechaUlt < fechaIng) {
      throw new BadRequestException('fecha_ultimo_dia no puede ser anterior a la fecha de ingreso');
    }

    const fechaCom = dto.fecha_comunicacion ? new Date(dto.fecha_comunicacion) : new Date();

    const { anios, meses } = diferenciaAniosMeses(fechaIng, fechaUlt);
    const anios_decimal = r2(anios + meses / 12);
    const salario = num(emp.salario_base);
    const jornalDiario = r2(salario / 30);

    // ── Reglas según tipo ──
    // Preaviso: se paga cuando la empresa rompe la relación sin preaviso (no en renuncia ni causal).
    const tipo = dto.tipo_desvinculacion;
    const generaPreavisoPorDefecto =
      tipo === 'DESPIDO_INJUSTIFICADO' || tipo === 'FALLECIMIENTO' || tipo === 'JUBILACION';
    const preaviso_omitido =
      dto.preaviso_omitido === undefined ? generaPreavisoPorDefecto : truthy(dto.preaviso_omitido);
    const dias_preaviso = preaviso_omitido ? diasPreavisoPorAntiguedad(anios) : 0;
    const monto_preaviso = r2(jornalDiario * dias_preaviso);

    // Indemnización por antigüedad (15 días/año) — sólo si despido injustificado, fallecimiento o mutuo acuerdo si así se pacta.
    const aplicaIndemnizacion =
      tipo === 'DESPIDO_INJUSTIFICADO' ||
      tipo === 'FALLECIMIENTO' ||
      tipo === 'MUTUO_ACUERDO';
    const anios_indemnizacion = aplicaIndemnizacion ? aniosIndemnizacion(anios, meses) : 0;
    const monto_indemnizacion = r2(jornalDiario * 15 * anios_indemnizacion);

    // Vacaciones proporcionales del año en curso: días/365 * (corresp_año / 12) — simplificado.
    // Tomamos los días anuales según escala vigente (Art. 219 CT):
    //   <5 años: 12 ; 5-10: 18 ; >10: 30
    const diasAnuales = anios < 5 ? 12 : anios <= 10 ? 18 : 30;
    const inicioAnio = new Date(fechaUlt.getFullYear(), 0, 1);
    const diasEnAnioTrabajados = Math.max(
      0,
      Math.round((fechaUlt.getTime() - Math.max(inicioAnio.getTime(), fechaIng.getTime())) / DIA_MS) + 1,
    );
    const diasTotalAnio = 365;
    const dias_vacaciones_proporcional = r2((diasAnuales * diasEnAnioTrabajados) / diasTotalAnio);
    const monto_vacaciones_proporcional = r2(jornalDiario * dias_vacaciones_proporcional);

    // Vacaciones no gozadas: viene de input manual (o 0). Si no se especifica, intentamos leer saldo del año previo.
    let diasNoGozados = num(dto.dias_vacaciones_no_gozadas);
    if (!dto.dias_vacaciones_no_gozadas) {
      const saldoPrev = await this.prisma.rrhh_vacaciones_saldo.findFirst({
        where: { empleado_id: emp.id, anio: fechaUlt.getFullYear() - 1, cerrado: false },
      });
      if (saldoPrev) diasNoGozados = num(saldoPrev.dias_pendientes);
    }
    const monto_vacaciones_no_gozadas = r2(jornalDiario * diasNoGozados);

    // Aguinaldo proporcional: 1/12 del total ganado en el año (Art. 244 CT) — aproximamos como salario*meses_trabajados_anio / 12.
    let aguinaldo_proporcional = 0;
    if (dto.aguinaldo_proporcional_override !== undefined && dto.aguinaldo_proporcional_override !== null) {
      aguinaldo_proporcional = r2(num(dto.aguinaldo_proporcional_override));
    } else {
      const inicioReferencia = fechaIng > inicioAnio ? fechaIng : inicioAnio;
      const mesesEnAnio =
        (fechaUlt.getFullYear() - inicioReferencia.getFullYear()) * 12 +
        (fechaUlt.getMonth() - inicioReferencia.getMonth()) +
        (fechaUlt.getDate() >= inicioReferencia.getDate() ? 1 : 0);
      const mesesEfectivos = Math.max(0, Math.min(12, mesesEnAnio));
      aguinaldo_proporcional = r2((salario * mesesEfectivos) / 12);
    }

    // Salario pendiente: input manual (días del último período no liquidados aún).
    const salario_pendiente = r2(num(dto.salario_pendiente_override));

    const total_liquidacion_final = r2(
      salario_pendiente +
        monto_vacaciones_proporcional +
        monto_vacaciones_no_gozadas +
        aguinaldo_proporcional +
        monto_preaviso +
        monto_indemnizacion,
    );

    return {
      empleado: {
        id: emp.id,
        numero_empleado: emp.numero_empleado,
        nombres: emp.nombres,
        apellidos: emp.apellidos,
        cedula: emp.cedula_identidad,
        fecha_ingreso: emp.fecha_ingreso,
        salario_base: salario,
      },
      tipo_desvinculacion: tipo,
      fecha_comunicacion: fechaCom,
      fecha_ultimo_dia: fechaUlt,
      anios_antiguedad: anios_decimal,
      meses_fraccion: meses,
      salario_base_calculo: salario,
      jornal_diario_calculo: jornalDiario,
      preaviso_omitido,
      salario_pendiente,
      dias_vacaciones_proporcional,
      monto_vacaciones_proporcional,
      dias_vacaciones_no_gozadas: r2(diasNoGozados),
      monto_vacaciones_no_gozadas,
      aguinaldo_proporcional,
      dias_preaviso,
      monto_preaviso,
      anios_indemnizacion,
      monto_indemnizacion,
      otros_haberes: 0,
      otros_descuentos: 0,
      total_liquidacion_final,
    };
  }

  // ── Crear (persiste el cálculo en estado CALCULADO) ────
  async crear(empresaId: string, dto: CrearDesvinculacionDto, userId?: string) {
    // Bloqueo si ya existe finiquito no anulado
    const previo = await this.prisma.rrhh_desvinculaciones.findFirst({
      where: { empleado_id: dto.empleado_id, estado: { not: 'ANULADO' } },
    });
    if (previo) {
      throw new ConflictException(
        `El empleado ya tiene una desvinculación en estado ${previo.estado}. Anulala antes de crear otra.`,
      );
    }

    const calc = await this.calcularPreview(empresaId, dto);

    return this.prisma.rrhh_desvinculaciones.create({
      data: {
        empresa_id: empresaId,
        empleado_id: dto.empleado_id,
        tipo_desvinculacion: dto.tipo_desvinculacion,
        fecha_comunicacion: calc.fecha_comunicacion,
        fecha_ultimo_dia: calc.fecha_ultimo_dia,
        fecha_calculo_antiguedad: calc.fecha_ultimo_dia,
        anios_antiguedad: calc.anios_antiguedad,
        meses_fraccion: calc.meses_fraccion,
        salario_base_calculo: calc.salario_base_calculo,
        jornal_diario_calculo: calc.jornal_diario_calculo,
        salario_pendiente: calc.salario_pendiente,
        dias_vacaciones_proporcional: calc.dias_vacaciones_proporcional,
        monto_vacaciones_proporcional: calc.monto_vacaciones_proporcional,
        dias_vacaciones_no_gozadas: calc.dias_vacaciones_no_gozadas,
        monto_vacaciones_no_gozadas: calc.monto_vacaciones_no_gozadas,
        aguinaldo_proporcional: calc.aguinaldo_proporcional,
        dias_preaviso: calc.dias_preaviso,
        monto_preaviso: calc.monto_preaviso,
        anios_indemnizacion: calc.anios_indemnizacion,
        monto_indemnizacion: calc.monto_indemnizacion,
        otros_haberes: 0,
        otros_descuentos: 0,
        total_liquidacion_final: calc.total_liquidacion_final,
        estado: 'CALCULADO',
        observaciones: dto.observaciones,
        creado_por: userId,
      },
    });
  }

  // ── Editar (ajustes manuales en CALCULADO) ─────────────
  async actualizar(id: string, empresaId: string, dto: UpdateDesvinculacionDto) {
    const desv = await this.prisma.rrhh_desvinculaciones.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!desv) throw new NotFoundException('Desvinculación no encontrada');
    if (desv.estado !== 'CALCULADO') {
      throw new ConflictException(
        `Solo se puede editar una desvinculación CALCULADA (actual: ${desv.estado}).`,
      );
    }

    const data: Prisma.rrhh_desvinculacionesUpdateInput = { updated_at: new Date() };
    const setIfPresent = <K extends keyof UpdateDesvinculacionDto>(key: K) => {
      if (dto[key] !== undefined) (data as any)[key] = dto[key];
    };
    setIfPresent('salario_pendiente');
    setIfPresent('dias_vacaciones_proporcional');
    setIfPresent('monto_vacaciones_proporcional');
    setIfPresent('dias_vacaciones_no_gozadas');
    setIfPresent('monto_vacaciones_no_gozadas');
    setIfPresent('aguinaldo_proporcional');
    setIfPresent('dias_preaviso');
    setIfPresent('monto_preaviso');
    setIfPresent('anios_indemnizacion');
    setIfPresent('monto_indemnizacion');
    setIfPresent('otros_haberes');
    setIfPresent('otros_descuentos');
    setIfPresent('observaciones');

    // Recompute total con valores efectivos
    const eff = (k: keyof UpdateDesvinculacionDto) =>
      num((dto as any)[k] !== undefined ? (dto as any)[k] : (desv as any)[k]);
    const total = r2(
      eff('salario_pendiente') +
        eff('monto_vacaciones_proporcional') +
        eff('monto_vacaciones_no_gozadas') +
        eff('aguinaldo_proporcional') +
        eff('monto_preaviso') +
        eff('monto_indemnizacion') +
        eff('otros_haberes') -
        eff('otros_descuentos'),
    );
    data.total_liquidacion_final = total;

    return this.prisma.rrhh_desvinculaciones.update({ where: { id }, data });
  }

  // ── Workflow ───────────────────────────────────────────
  async aprobar(id: string, empresaId: string, dto: AccionDesvinculacionDto, userId?: string) {
    const desv = await this.prisma.rrhh_desvinculaciones.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!desv) throw new NotFoundException('Desvinculación no encontrada');
    if (desv.estado !== 'CALCULADO') {
      throw new ConflictException(`Solo se puede aprobar una desvinculación CALCULADA (actual: ${desv.estado}).`);
    }
    const advertencias = await this.verificarLegajoAntesDeCierre(empresaId, desv.empleado_id);

    const aprobado = await this.prisma.rrhh_desvinculaciones.update({
      where: { id },
      data: {
        estado: 'APROBADO',
        aprobado_por: userId,
        fecha_aprobacion: new Date(),
        observaciones: dto.comentario
          ? desv.observaciones
            ? `${desv.observaciones}\n[APROBADO] ${dto.comentario}`
            : `[APROBADO] ${dto.comentario}`
          : desv.observaciones,
        updated_at: new Date(),
      },
    });

    return advertencias.length > 0
      ? {
          ...aprobado,
          advertencias,
        }
      : aprobado;
  }

  async pagar(id: string, empresaId: string, dto: AccionDesvinculacionDto, userId?: string) {
    const desv = await this.prisma.rrhh_desvinculaciones.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!desv) throw new NotFoundException('Desvinculación no encontrada');
    if (desv.estado !== 'APROBADO' && desv.estado !== 'CALCULADO') {
      throw new ConflictException(
        `Solo se puede pagar una desvinculación CALCULADA o APROBADA (actual: ${desv.estado}).`,
      );
    }
    const advertencias = await this.verificarLegajoAntesDeCierre(empresaId, desv.empleado_id);

    const cuentaTesoreriaId = dto.cuenta_tesoreria_id ?? desv.cuenta_tesoreria_id;

    // Si la empresa usa Tesorería, registrar el egreso ANTES de marcar PAGADO, para que un fallo
    // (cuenta faltante, etc.) no deje el finiquito pagado sin movimiento. Es idempotente.
    if (await this.tieneModuloTesoreria(empresaId)) {
      await this.registrarMovimientoTesoreria({ ...desv, cuenta_tesoreria_id: cuentaTesoreriaId }, userId);
    }

    const pagada = await this.prisma.$transaction(async (tx) => {
      const pagada = await tx.rrhh_desvinculaciones.update({
        where: { id },
        data: {
          estado: 'PAGADO',
          pagado_por: userId,
          fecha_pago: new Date(),
          cuenta_tesoreria_id: cuentaTesoreriaId,
          observaciones: dto.comentario
            ? desv.observaciones
              ? `${desv.observaciones}\n[PAGADO] ${dto.comentario}`
              : `[PAGADO] ${dto.comentario}`
            : desv.observaciones,
          updated_at: new Date(),
        },
      });

      // Marcar empleado como EGRESADO con fecha_egreso = fecha_ultimo_dia.
      // Esto evita que aparezca en futuras liquidaciones y bloquea la creación de nuevos anticipos/préstamos.
      await tx.rrhh_empleados.update({
        where: { id: desv.empleado_id },
        data: {
          estado: 'EGRESADO',
          fecha_egreso: desv.fecha_ultimo_dia,
          updated_at: new Date(),
        },
      });

      return pagada;
    });

    return advertencias.length > 0
      ? {
          ...pagada,
          advertencias,
        }
      : pagada;
  }

  async anular(id: string, empresaId: string, dto: AccionDesvinculacionDto, userId?: string) {
    const desv = await this.prisma.rrhh_desvinculaciones.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!desv) throw new NotFoundException('Desvinculación no encontrada');
    if (desv.estado === 'ANULADO') throw new ConflictException('Ya está anulada');
    if (desv.estado === 'PAGADO') {
      throw new ConflictException(
        'Una desvinculación PAGADA no se puede anular automáticamente porque el empleado ya quedó EGRESADO. Hacelo manualmente con observación.',
      );
    }
    return this.prisma.rrhh_desvinculaciones.update({
      where: { id },
      data: {
        estado: 'ANULADO',
        anulado_por: userId,
        fecha_anulacion: new Date(),
        observaciones: dto.comentario
          ? desv.observaciones
            ? `${desv.observaciones}\n[ANULADO] ${dto.comentario}`
            : `[ANULADO] ${dto.comentario}`
          : desv.observaciones,
        updated_at: new Date(),
      },
    });
  }

  private async verificarLegajoAntesDeCierre(
    empresaId: string,
    empleadoId: string,
  ): Promise<string[]> {
    if (!this.legajosCompletitud || !this.parametros) return [];

    const detalle = await this.legajosCompletitud.verificarBloquea(empresaId, empleadoId);
    if (!detalle) return [];

    const param = await this.parametros.obtenerVigente(
      empresaId,
      'LEGAJO_COMPLETITUD_OBLIGATORIO_BLOQUEA',
    );
    const bloquea = String(param?.valor ?? 'false').toLowerCase() === 'true';

    const faltantesTxt = (detalle.faltantes || [])
      .map((f) => `${f.codigo}${f.nombre ? ` (${f.nombre})` : ''}`)
      .join(', ');
    const vencidosTxt = (detalle.vencidos || [])
      .map((v) => `${v.codigo}${v.nombre ? ` (${v.nombre})` : ''}`)
      .join(', ');

    const resumen = [
      faltantesTxt ? `Faltantes: ${faltantesTxt}` : null,
      vencidosTxt ? `Vencidos: ${vencidosTxt}` : null,
    ]
      .filter(Boolean)
      .join(' · ');

    if (bloquea) {
      throw new HttpException(
        {
          success: false,
          message:
            'No se puede cerrar la desvinculación: el legajo obligatorio del empleado está incompleto.',
          detalle_legajo: detalle,
        },
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }

    return [
      `Legajo incompleto detectado. ${resumen || 'Hay documentos obligatorios faltantes/vencidos.'}`,
    ];
  }
}
