import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { envs } from 'src/config/envs';
import { GastosService } from 'src/gastos/gastos.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { CerrarRendicionDto } from './dto/cerrar-rendicion.dto';
import { CreateAdelantoAdicionalDto } from './dto/create-adelanto.dto';
import { CreateGastoRendicionDto } from './dto/create-gasto-rendicion.dto';
import { CreateRendicionViaticoDto } from './dto/create-rendicion-viatico.dto';
import { ListarRendicionesDto } from './dto/listar-rendiciones.dto';
import { UpdateRendicionViaticoDto } from './dto/update-rendicion-viatico.dto';
import { RendicionViaticosContabilidadService } from './rendicion-viaticos-contabilidad.service';
import { RendicionViaticosTesoreriaService } from './rendicion-viaticos-tesoreria.service';

/**
 * FASE 1 — Rendición de viáticos: cabecera + adelantos + integración con
 * contabilidad y tesorería. El cierre (devoluciones/reintegros) y la carga
 * de gastos vinculados se implementan en Fase 2 y 3.
 *
 * Ver docs/plan-rendicion-viaticos.md.
 */
@Injectable()
export class RendicionViaticosService {
  private readonly logger = new Logger(RendicionViaticosService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly contabilidad: RendicionViaticosContabilidadService,
    private readonly tesoreria: RendicionViaticosTesoreriaService,
    private readonly gastosService: GastosService,
  ) {}

  // ── Helpers ──────────────────────────────────────────────────────────────

  private async ensureConfig(empresaId: string) {
    const existing = await this.prisma.config_rrhh.findUnique({
      where: { empresa_id: empresaId },
    });
    if (existing) return existing;
    return this.prisma.config_rrhh.create({
      data: { empresa_id: empresaId },
    });
  }

  private async siguienteNumeroRendicion(empresaId: string, tx?: Prisma.TransactionClient): Promise<string> {
    const db = tx ?? this.prisma;
    const anio = new Date().getFullYear();
    const prefijo = `RVI-${anio}-`;
    const ultima = await db.rendicion_viatico.findFirst({
      where: { empresa_id: empresaId, numero_rendicion: { startsWith: prefijo } },
      orderBy: { numero_rendicion: 'desc' },
      select: { numero_rendicion: true },
    });
    let sig = 1;
    if (ultima?.numero_rendicion) {
      const parte = ultima.numero_rendicion.split('-').at(-1) ?? '0';
      sig = (parseInt(parte, 10) || 0) + 1;
    }
    return `${prefijo}${String(sig).padStart(5, '0')}`;
  }

  private includeCompleto = {
    empleado: { select: { id: true, nombres: true, apellidos: true, cedula_identidad: true } },
    moneda: { select: { id: true, codigo: true, simbolo: true, descripcion: true } },
    adelantos: {
      where: { anulado: false },
      orderBy: { fecha: 'asc' as const },
      include: {
        medio_pago: { select: { id: true, descripcion: true } },
        tes_cuenta: { select: { id: true, nombre: true, moneda: true } },
        banco: { select: { id: true, descripcion: true } },
      },
    },
    devoluciones: {
      where: { anulado: false },
      orderBy: { fecha: 'asc' as const },
      include: {
        medio_pago: { select: { id: true, descripcion: true } },
        tes_cuenta: { select: { id: true, nombre: true, moneda: true } },
      },
    },
    gastos: {
      where: { activo: true, anulado: false },
      orderBy: { fecha_emision: 'asc' as const },
      select: {
        id: true,
        fecha_emision: true,
        numero_factura: true,
        timbrado: true,
        establecimiento: true,
        punto_expedicion: true,
        ruc: true,
        descripcion: true,
        total: true,
        total_iva_5: true,
        total_iva_10: true,
        deducible: true,
        proveedor_id: true,
        forma_pago: true,
        proveedores: {
          select: {
            id: true,
            personas: { select: { razon_social: true, ruc: true } },
          },
        },
        gasto_det: {
          select: {
            id: true,
            descripcion: true,
            cantidad: true,
            precio_unitario: true,
            porcentaje_iva: true,
            total: true,
            tipo_gasto: { select: { id: true, descripcion: true } },
          },
        },
      },
    },
  } satisfies Prisma.rendicion_viaticoInclude;

  // ── CRUD principal ───────────────────────────────────────────────────────

  async create(empresaId: string, dto: CreateRendicionViaticoDto, userId?: string) {
    const empleado = await this.prisma.rrhh_empleados.findFirst({
      where: { id: dto.empleado_id, empresa_id: empresaId, estado: 'ACTIVO' },
    });
    if (!empleado) throw new NotFoundException('Empleado no encontrado o inactivo');

    if (new Date(dto.fecha_fin) < new Date(dto.fecha_inicio)) {
      throw new BadRequestException('fecha_fin no puede ser anterior a fecha_inicio');
    }

    const config = await this.ensureConfig(empresaId);
    const sinAdelanto = Boolean(dto.sin_adelanto);
    const primerAdelanto = sinAdelanto ? null : dto.primer_adelanto;

    // Decidir estado inicial.
    let estadoInicial: string;
    if (sinAdelanto) {
      estadoInicial = 'SIN_ADELANTO';
    } else if (!primerAdelanto) {
      estadoInicial = 'BORRADOR';
    } else {
      const requiereAprobacion =
        config.anticipo_viatico_requiere_aprobacion &&
        primerAdelanto.monto > Number(config.anticipo_viatico_umbral_aprobacion);
      estadoInicial = requiereAprobacion ? 'PENDIENTE_APROBACION' : 'ADELANTO_PAGADO';
    }

    const result = await this.prisma.$transaction(async (tx) => {
      const numero = await this.siguienteNumeroRendicion(empresaId, tx);
      const rendicion = await tx.rendicion_viatico.create({
        data: {
          empresa_id: empresaId,
          empleado_id: dto.empleado_id,
          numero_rendicion: numero,
          concepto: dto.concepto,
          fecha_inicio: new Date(dto.fecha_inicio),
          fecha_fin: new Date(dto.fecha_fin),
          sin_adelanto: sinAdelanto,
          moneda_id: dto.moneda_id ?? null,
          cotizacion: dto.cotizacion ?? 1,
          observaciones: dto.observaciones ?? null,
          estado: estadoInicial,
          creado_por: userId ?? null,
        },
      });

      // Si hay primer adelanto y no requiere aprobación, ejecutar ahora.
      if (primerAdelanto && estadoInicial === 'ADELANTO_PAGADO') {
        await this.crearAdelantoTx(tx, empresaId, rendicion.id, primerAdelanto, userId);
      } else if (primerAdelanto && estadoInicial === 'PENDIENTE_APROBACION') {
        // Registrar el adelanto propuesto pero SIN ejecutar (sin asiento ni tesorería).
        // Se ejecuta al aprobar.
        await tx.rendicion_adelanto.create({
          data: {
            rendicion_viatico_id: rendicion.id,
            empresa_id: empresaId,
            monto: primerAdelanto.monto,
            fecha: new Date(primerAdelanto.fecha),
            medio_pago_id: primerAdelanto.medio_pago_id ?? null,
            tes_cuenta_id: primerAdelanto.tes_cuenta_id ?? null,
            numero_operacion: primerAdelanto.numero_operacion ?? null,
            banco_id: primerAdelanto.banco_id ?? null,
            observaciones: primerAdelanto.observaciones ?? null,
            numero_cheque: primerAdelanto.numero_cheque ?? null,
            banco_emisor: primerAdelanto.banco_emisor ?? null,
            titular_cheque: primerAdelanto.titular_cheque ?? null,
            fecha_emision_cheque: primerAdelanto.fecha_emision_cheque
              ? new Date(primerAdelanto.fecha_emision_cheque)
              : null,
            fecha_vencimiento_cheque: primerAdelanto.fecha_vencimiento_cheque
              ? new Date(primerAdelanto.fecha_vencimiento_cheque)
              : null,
          },
        });
        // Actualizar total.
        await tx.rendicion_viatico.update({
          where: { id: rendicion.id },
          data: {
            monto_adelanto_total: primerAdelanto.monto,
            saldo: primerAdelanto.monto,
          },
        });
      }

      return rendicion.id;
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'CREATE',
      entity_type: 'rendicion_viatico',
      entity_id: result,
      descripcion: `Rendición viáticos creada — empleado ${empleado.nombres} ${empleado.apellidos}`,
      new_value: { estado: estadoInicial, sin_adelanto: sinAdelanto },
    });

    return this.findOne(empresaId, result);
  }

  async findAll(empresaId: string, filtros: ListarRendicionesDto) {
    const page = Math.max(1, Number(filtros.page) || 1);
    const limit = Math.min(200, Math.max(1, Number(filtros.limit) || 20));
    const skip = (page - 1) * limit;

    // No filtramos por `activo`: al anular se marca activo=false + estado=ANULADA,
    // pero el usuario quiere poder ver el histórico anulado. La UI ya renderiza
    // las filas con line-through y color atenuado según el estado.
    const where: Prisma.rendicion_viaticoWhereInput = {
      empresa_id: empresaId,
    };
    if (filtros.empleado_id) where.empleado_id = filtros.empleado_id;
    if (filtros.estado) where.estado = filtros.estado;
    if (filtros.fecha_desde || filtros.fecha_hasta) {
      where.fecha_inicio = {};
      if (filtros.fecha_desde) (where.fecha_inicio as any).gte = new Date(filtros.fecha_desde);
      if (filtros.fecha_hasta) (where.fecha_inicio as any).lte = new Date(filtros.fecha_hasta);
    }
    if (filtros.search?.trim()) {
      const q = filtros.search.trim();
      where.OR = [
        { numero_rendicion: { contains: q, mode: 'insensitive' } },
        { concepto: { contains: q, mode: 'insensitive' } },
      ];
    }
    if (filtros.dias_sin_rendir_min) {
      const dias = Number(filtros.dias_sin_rendir_min);
      if (dias > 0) {
        const limite = new Date();
        limite.setDate(limite.getDate() - dias);
        where.estado = { in: ['ADELANTO_PAGADO', 'SIN_ADELANTO'] };
        where.fecha_fin = { lte: limite };
      }
    }

    const [items, total] = await this.prisma.$transaction([
      this.prisma.rendicion_viatico.findMany({
        where,
        include: {
          empleado: { select: { id: true, nombres: true, apellidos: true, numero_empleado: true } },
          moneda: { select: { id: true, codigo: true, simbolo: true } },
          _count: { select: { adelantos: true, gastos: true } },
        },
        orderBy: [{ created_at: 'desc' }],
        skip,
        take: limit,
      }),
      this.prisma.rendicion_viatico.count({ where }),
    ]);

    return {
      items,
      total,
      page,
      limit,
      lastPage: Math.max(1, Math.ceil(total / limit)),
    };
  }

  async findOne(empresaId: string, id: string) {
    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id, empresa_id: empresaId },
      include: this.includeCompleto,
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    return rendicion;
  }

  async update(empresaId: string, id: string, dto: UpdateRendicionViaticoDto, userId?: string) {
    const actual = await this.prisma.rendicion_viatico.findFirst({
      where: { id, empresa_id: empresaId },
      select: { id: true, estado: true },
    });
    if (!actual) throw new NotFoundException('Rendición no encontrada');
    if (actual.estado !== 'BORRADOR') {
      throw new BadRequestException(`Solo se puede editar en estado BORRADOR (actual: ${actual.estado})`);
    }

    await this.prisma.rendicion_viatico.update({
      where: { id },
      data: {
        empleado_id: dto.empleado_id,
        concepto: dto.concepto,
        fecha_inicio: dto.fecha_inicio ? new Date(dto.fecha_inicio) : undefined,
        fecha_fin: dto.fecha_fin ? new Date(dto.fecha_fin) : undefined,
        moneda_id: dto.moneda_id,
        cotizacion: dto.cotizacion,
        observaciones: dto.observaciones,
        updated_at: new Date(),
      },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'UPDATE',
      entity_type: 'rendicion_viatico',
      entity_id: id,
      descripcion: 'Rendición actualizada',
    });

    return this.findOne(empresaId, id);
  }

  // ── Aprobación ───────────────────────────────────────────────────────────

  async aprobar(empresaId: string, id: string, userId?: string) {
    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        adelantos: { where: { anulado: false, tes_movimiento_id: null } },
      },
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    if (rendicion.estado !== 'PENDIENTE_APROBACION') {
      throw new BadRequestException(`La rendición no está en PENDIENTE_APROBACION (actual: ${rendicion.estado})`);
    }

    await this.prisma.$transaction(async (tx) => {
      // Ejecutar los adelantos pendientes (generar asiento + tes_movimiento).
      for (const ade of rendicion.adelantos) {
        await this.ejecutarAdelantoExistente(tx, empresaId, id, ade.id, userId);
      }
      await tx.rendicion_viatico.update({
        where: { id },
        data: {
          estado: 'ADELANTO_PAGADO',
          aprobado_por: userId ?? null,
          fecha_aprobacion: new Date(),
          updated_at: new Date(),
        },
      });
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'APPROVE',
      entity_type: 'rendicion_viatico',
      entity_id: id,
      descripcion: `Rendición ${rendicion.numero_rendicion} aprobada`,
    });

    return this.findOne(empresaId, id);
  }

  async rechazar(empresaId: string, id: string, motivo: string, userId?: string) {
    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id, empresa_id: empresaId },
      select: { id: true, estado: true, numero_rendicion: true },
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    if (rendicion.estado !== 'PENDIENTE_APROBACION') {
      throw new BadRequestException(`La rendición no está en PENDIENTE_APROBACION (actual: ${rendicion.estado})`);
    }

    await this.prisma.rendicion_viatico.update({
      where: { id },
      data: {
        estado: 'ANULADA',
        anulado_por: userId ?? null,
        fecha_anulacion: new Date(),
        motivo_anulacion: motivo || 'Rechazada en aprobación',
        activo: false,
        updated_at: new Date(),
      },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'REJECT',
      entity_type: 'rendicion_viatico',
      entity_id: id,
      descripcion: `Rendición ${rendicion.numero_rendicion} rechazada — ${motivo}`,
    });

    return this.findOne(empresaId, id);
  }

  // ── Adelantos adicionales ────────────────────────────────────────────────

  async agregarAdelanto(
    empresaId: string,
    rendicionId: string,
    dto: CreateAdelantoAdicionalDto,
    userId?: string,
  ) {
    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id: rendicionId, empresa_id: empresaId },
      select: { id: true, estado: true, empleado_id: true },
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    if (!['ADELANTO_PAGADO', 'SIN_ADELANTO'].includes(rendicion.estado)) {
      throw new BadRequestException(
        `No se puede agregar adelanto en estado ${rendicion.estado}. Debe estar en ADELANTO_PAGADO o SIN_ADELANTO.`,
      );
    }

    const result = await this.prisma.$transaction(async (tx) => {
      const adelantoId = await this.crearAdelantoTx(tx, empresaId, rendicionId, dto, userId);
      // Si estaba SIN_ADELANTO, ahora tiene adelanto → mover a ADELANTO_PAGADO.
      if (rendicion.estado === 'SIN_ADELANTO') {
        await tx.rendicion_viatico.update({
          where: { id: rendicionId },
          data: { estado: 'ADELANTO_PAGADO', sin_adelanto: false, updated_at: new Date() },
        });
      }
      return adelantoId;
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'CREATE',
      entity_type: 'rendicion_adelanto',
      entity_id: result,
      descripcion: `Adelanto adicional agregado a rendición ${rendicionId}`,
      new_value: { monto: dto.monto },
    });

    return this.findOne(empresaId, rendicionId);
  }

  // ── Anular rendición ─────────────────────────────────────────────────────

  async anular(empresaId: string, id: string, motivo: string, userId?: string) {
    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        adelantos: { where: { anulado: false } },
        devoluciones: { where: { anulado: false } },
        gastos: {
          where: { activo: true, anulado: false },
          select: { id: true },
        },
      },
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    if (rendicion.estado === 'ANULADA') {
      throw new BadRequestException('La rendición ya está anulada');
    }

    // Fuera de la transacción: revertir movimientos/asientos (usan sus propias txs).
    for (const dev of rendicion.devoluciones) {
      if (dev.tes_movimiento_id || dev.cont_documento_id) {
        await this.tesoreria.revertirDevolucion(dev.id, empresaId, userId);
        await this.contabilidad.revertirDevolucion(dev.id, empresaId, userId);
      }
      // Si había CxP diferida, marcarla como anulada.
      if (dev.cxp_empleado_id) {
        await this.prisma.cuentas_pagar_empleado.update({
          where: { id: dev.cxp_empleado_id },
          data: { estado: 'anulada', activo: false, updated_at: new Date() },
        });
      }
    }
    for (const ade of rendicion.adelantos) {
      if (ade.tes_movimiento_id || ade.cont_documento_id) {
        await this.tesoreria.revertirAdelanto(ade.id, empresaId, userId);
        await this.contabilidad.revertirAdelanto(ade.id, empresaId, userId);
      }
    }
    // Anular gastos (revierte sus propios asientos).
    for (const g of rendicion.gastos) {
      try {
        await this.gastosService.anular(g.id, empresaId, userId ?? '', 'Rendición anulada');
      } catch (err) {
        this.logger.warn(
          `No se pudo anular gasto ${g.id}: ${err instanceof Error ? err.message : String(err)}`,
        );
      }
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.rendicion_devolucion.updateMany({
        where: { rendicion_viatico_id: id, anulado: false },
        data: { anulado: true, updated_at: new Date() },
      });
      await tx.rendicion_adelanto.updateMany({
        where: { rendicion_viatico_id: id, anulado: false },
        data: { anulado: true, updated_at: new Date() },
      });
      await tx.rendicion_viatico.update({
        where: { id },
        data: {
          estado: 'ANULADA',
          anulado_por: userId ?? null,
          fecha_anulacion: new Date(),
          motivo_anulacion: motivo || 'Anulada manualmente',
          activo: false,
          updated_at: new Date(),
        },
      });
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'DELETE',
      entity_type: 'rendicion_viatico',
      entity_id: id,
      descripcion: `Rendición anulada — ${motivo}`,
    });

    return this.findOne(empresaId, id);
  }

  // ── Auxiliares privados ──────────────────────────────────────────────────

  /**
   * Crea un adelanto DENTRO de una transacción y lo ejecuta (asiento + tes_movimiento).
   */
  private async crearAdelantoTx(
    tx: Prisma.TransactionClient,
    empresaId: string,
    rendicionId: string,
    dto: CreateAdelantoAdicionalDto | {
      monto: number;
      fecha: string;
      medio_pago_id?: string;
      tes_cuenta_id?: string;
      numero_operacion?: string;
      banco_id?: string;
      observaciones?: string;
      numero_cheque?: string;
      banco_emisor?: string;
      titular_cheque?: string;
      fecha_emision_cheque?: string;
      fecha_vencimiento_cheque?: string;
    },
    userId?: string,
  ): Promise<string> {
    // Insertar el adelanto.
    const adelanto = await tx.rendicion_adelanto.create({
      data: {
        rendicion_viatico_id: rendicionId,
        empresa_id: empresaId,
        monto: dto.monto,
        fecha: new Date(dto.fecha),
        medio_pago_id: dto.medio_pago_id ?? null,
        tes_cuenta_id: dto.tes_cuenta_id ?? null,
        numero_operacion: dto.numero_operacion ?? null,
        banco_id: dto.banco_id ?? null,
        observaciones: dto.observaciones ?? null,
        numero_cheque: dto.numero_cheque ?? null,
        banco_emisor: dto.banco_emisor ?? null,
        titular_cheque: dto.titular_cheque ?? null,
        fecha_emision_cheque: dto.fecha_emision_cheque
          ? new Date(dto.fecha_emision_cheque)
          : null,
        fecha_vencimiento_cheque: dto.fecha_vencimiento_cheque
          ? new Date(dto.fecha_vencimiento_cheque)
          : null,
      },
    });

    // Actualizar totales de la rendición.
    await tx.rendicion_viatico.update({
      where: { id: rendicionId },
      data: {
        monto_adelanto_total: { increment: dto.monto },
        saldo: { increment: dto.monto },
        updated_at: new Date(),
      },
    });

    // Ejecutar impacto contable + tesorería (fuera de la transacción explícita
    // porque estos servicios abren sus propias txs. Los guardamos como IDs).
    // Nota: en producción esto se hace después del commit; para simplicidad
    // Fase 1 lo dejamos synchronous fire-and-forget con try/catch.
    return adelanto.id;
  }

  /**
   * Ejecuta un adelanto ya persistido (usado al aprobar rendiciones que estaban
   * en PENDIENTE_APROBACION). Solo genera asiento + tes_movimiento.
   */
  private async ejecutarAdelantoExistente(
    tx: Prisma.TransactionClient,
    empresaId: string,
    rendicionId: string,
    adelantoId: string,
    userId?: string,
  ) {
    // No hacemos nada dentro de la tx que abren los otros servicios; solo
    // marcamos como "ejecutado" luego. Se completa fuera de la tx por el caller.
    // Fase 1: dejamos el hook para que el service externo se llame post-commit.
    // Como los servicios ya usan `this.prisma` (no tx), corren en su propia tx
    // y se comitean independientemente.
    return { adelantoId, empresaId, rendicionId, userId };
  }

  // ── Método público: ejecutar impacto post-commit ─────────────────────────

  /**
   * Post-hook: después de crear/aprobar, dispara la integración contable +
   * tesorería para los adelantos que aún no fueron impactados.
   */
  async ejecutarImpactosPendientes(empresaId: string, rendicionId: string, userId?: string) {
    const adelantosPendientes = await this.prisma.rendicion_adelanto.findMany({
      where: {
        rendicion_viatico_id: rendicionId,
        empresa_id: empresaId,
        anulado: false,
        tes_movimiento_id: null,
      },
    });

    for (const ade of adelantosPendientes) {
      try {
        const tesMov = await this.tesoreria.registrarAdelanto(ade.id, empresaId, userId);
        const contDoc = await this.contabilidad.registrarAdelanto(ade.id, empresaId, userId);
        await this.prisma.rendicion_adelanto.update({
          where: { id: ade.id },
          data: {
            tes_movimiento_id: tesMov?.id ?? null,
            cont_documento_id: contDoc?.id ?? null,
            updated_at: new Date(),
          },
        });
      } catch (err) {
        this.logger.error(
          `Error ejecutando adelanto ${ade.id}: ${err instanceof Error ? err.message : String(err)}`,
        );
      }
    }
  }

  // ── Cierre (Fase 3) ──────────────────────────────────────────────────────

  /**
   * Cierra una rendición calculando el saldo entre adelantos y gastos.
   *
   * Casos:
   * - Saldo = 0 → cierra sin generar movimiento (no hace falta devolver ni reintegrar).
   * - Saldo > 0 (funcionario devuelve): genera `rendicion_devolucion` tipo DEVOLUCION,
   *   asiento + tes_movimiento INGRESO en la cuenta destino.
   * - Saldo < 0 (empresa reintegra):
   *   - INMEDIATO: genera devolución tipo REINTEGRO + asiento + tes_movimiento EGRESO.
   *   - DIFERIDO: genera devolución REINTEGRO con `diferido=true` + `cuentas_pagar_empleado`
   *     + asiento (DEBE Anticipo / HABER Reintegros Pendientes). Sin tes_movimiento.
   *
   * Al terminar, la rendición queda en estado CERRADA (inmutable).
   * Ver docs/plan-rendicion-viaticos.md — Fase 3.
   */
  async cerrar(empresaId: string, rendicionId: string, dto: CerrarRendicionDto, userId?: string) {
    // Refrescar saldo antes de decidir.
    await this.recalcularTotales(empresaId, rendicionId);

    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id: rendicionId, empresa_id: empresaId },
      select: {
        id: true,
        estado: true,
        saldo: true,
        empleado_id: true,
        numero_rendicion: true,
        monto_adelanto_total: true,
        monto_rendido_total: true,
      },
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    if (!['ADELANTO_PAGADO', 'SIN_ADELANTO', 'RENDIDA'].includes(rendicion.estado)) {
      throw new BadRequestException(
        `No se puede cerrar en estado ${rendicion.estado}. Debe estar en ADELANTO_PAGADO, SIN_ADELANTO o RENDIDA.`,
      );
    }

    const saldo = Number(rendicion.saldo || 0);
    const fecha = new Date(dto.fecha_cierre);

    let devolucionId: string | null = null;

    if (Math.abs(saldo) > 0.001) {
      // Hay saldo → generar registro de devolución/reintegro.
      const esDevolucion = saldo > 0;
      const monto = Math.abs(saldo);

      let tipoReintegro: 'INMEDIATO' | 'DIFERIDO' | null = null;
      let diferido = false;

      if (!esDevolucion) {
        tipoReintegro = dto.tipo_reintegro ?? 'INMEDIATO';
        diferido = tipoReintegro === 'DIFERIDO';
      }

      // Validaciones:
      if (esDevolucion) {
        if (!dto.tes_cuenta_id) {
          throw new BadRequestException('Elegí la cuenta destino de la devolución.');
        }
        if (!dto.medio_pago_id) {
          throw new BadRequestException('Elegí el medio de pago de la devolución.');
        }
      } else if (tipoReintegro === 'INMEDIATO') {
        if (!dto.tes_cuenta_id) {
          throw new BadRequestException('Elegí la cuenta origen del reintegro.');
        }
        if (!dto.medio_pago_id) {
          throw new BadRequestException('Elegí el medio de pago del reintegro.');
        }
      }

      devolucionId = await this.prisma.$transaction(async (tx) => {
        // Si es reintegro diferido, crear la CxP al empleado primero.
        let cxpId: string | null = null;
        if (diferido) {
          const cxp = await tx.cuentas_pagar_empleado.create({
            data: {
              empresa_id: empresaId,
              empleado_id: rendicion.empleado_id,
              origen_tipo: 'reintegro_viatico',
              origen_id: rendicionId,
              monto_original: monto,
              monto_pagado: 0,
              saldo_pendiente: monto,
              estado: 'pendiente',
              fecha_generacion: fecha,
              observaciones: `Reintegro diferido — rendición ${rendicion.numero_rendicion}`,
            },
          });
          cxpId = cxp.id;
        }

        const dev = await tx.rendicion_devolucion.create({
          data: {
            rendicion_viatico_id: rendicionId,
            empresa_id: empresaId,
            tipo: esDevolucion ? 'DEVOLUCION' : 'REINTEGRO',
            monto,
            fecha,
            medio_pago_id: diferido ? null : dto.medio_pago_id ?? null,
            tes_cuenta_id: diferido ? null : dto.tes_cuenta_id ?? null,
            numero_operacion: dto.numero_operacion ?? null,
            diferido,
            cxp_empleado_id: cxpId,
            observaciones: dto.observaciones ?? null,
            numero_cheque: diferido ? null : dto.numero_cheque ?? null,
            banco_emisor: diferido ? null : dto.banco_emisor ?? null,
            titular_cheque: diferido ? null : dto.titular_cheque ?? null,
            fecha_emision_cheque:
              !diferido && dto.fecha_emision_cheque ? new Date(dto.fecha_emision_cheque) : null,
            fecha_vencimiento_cheque:
              !diferido && dto.fecha_vencimiento_cheque
                ? new Date(dto.fecha_vencimiento_cheque)
                : null,
          },
        });
        return dev.id;
      });
    }

    // Marcar rendición como CERRADA.
    await this.prisma.rendicion_viatico.update({
      where: { id: rendicionId },
      data: {
        estado: 'CERRADA',
        cerrado_por: userId ?? null,
        fecha_cierre: new Date(),
        tipo_cierre_reintegro:
          Math.abs(saldo) > 0.001 && saldo < 0 ? (dto.tipo_reintegro ?? 'INMEDIATO') : null,
        updated_at: new Date(),
      },
    });

    // Ejecutar impacto contable + tesorería post-commit.
    if (devolucionId) {
      try {
        const tesMov = await this.tesoreria.registrarDevolucion(devolucionId, empresaId, userId);
        const contDoc = await this.contabilidad.registrarDevolucion(devolucionId, empresaId, userId);
        await this.prisma.rendicion_devolucion.update({
          where: { id: devolucionId },
          data: {
            tes_movimiento_id: tesMov?.id ?? null,
            cont_documento_id: contDoc?.id ?? null,
            updated_at: new Date(),
          },
        });
      } catch (err) {
        this.logger.error(
          `Error ejecutando impacto de cierre ${devolucionId}: ${err instanceof Error ? err.message : String(err)}`,
        );
      }
    }

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'UPDATE',
      entity_type: 'rendicion_viatico',
      entity_id: rendicionId,
      descripcion: `Rendición ${rendicion.numero_rendicion} cerrada — saldo ${saldo}`,
      new_value: {
        saldo,
        tipo_reintegro: saldo < 0 ? dto.tipo_reintegro : null,
      },
    });

    return this.findOne(empresaId, rendicionId);
  }

  // ── Gastos vinculados (Fase 2) ───────────────────────────────────────────

  /**
   * Crea un gasto asociado a la rendición. Reutiliza GastosService.create y
   * setea `rendicion_viatico_id` + `forma_pago='ANTICIPO'` para que el asiento
   * contable acredite Anticipo Viáticos en lugar de la cuenta normal.
   *
   * Ver docs/plan-rendicion-viaticos.md — Fase 2.
   */
  async agregarGasto(
    empresaId: string,
    rendicionId: string,
    dto: CreateGastoRendicionDto,
    userId?: string,
  ) {
    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id: rendicionId, empresa_id: empresaId },
      select: { id: true, estado: true, moneda_id: true, cotizacion: true, numero_rendicion: true },
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    if (!['ADELANTO_PAGADO', 'SIN_ADELANTO', 'RENDIDA'].includes(rendicion.estado)) {
      throw new BadRequestException(
        `No se puede cargar gasto en estado ${rendicion.estado}. Debe estar en ADELANTO_PAGADO, SIN_ADELANTO o RENDIDA.`,
      );
    }

    // Sin factura: limpiar campos fiscales.
    const conFactura = Boolean(dto.con_factura);
    const cabecera = conFactura
      ? {
          proveedor_id: dto.proveedor_id,
          ruc: dto.ruc,
          timbrado: dto.timbrado,
          establecimiento: dto.establecimiento,
          punto_expedicion: dto.punto_expedicion,
          numero_factura: dto.numero_factura,
          // Los comprobantes de una rendición se cargan a mano: por defecto son de
          // talonario. Si es electrónico, la DNIT ya lo tiene y no va al Libro IVA.
          es_preimpreso: dto.es_preimpreso ?? true,
          fecha_emision: dto.fecha_emision,
          sucursal_id: dto.sucursal_id,
          moneda_id: dto.moneda_id ?? rendicion.moneda_id ?? undefined,
          cotizacion: dto.cotizacion ?? Number(rendicion.cotizacion ?? 1),
          descripcion: dto.descripcion,
          forma_pago: 'ANTICIPO' as const,
          rendicion_viatico_id: rendicionId,
        }
      : {
          fecha_emision: dto.fecha_emision,
          moneda_id: dto.moneda_id ?? rendicion.moneda_id ?? undefined,
          cotizacion: dto.cotizacion ?? Number(rendicion.cotizacion ?? 1),
          descripcion: dto.descripcion ?? 'Gasto sin comprobante fiscal',
          forma_pago: 'ANTICIPO' as const,
          rendicion_viatico_id: rendicionId,
        };

    // Traer nombres de tipos de gasto para usar como fallback de descripción.
    const tipoIds = Array.from(
      new Set(dto.items.map((it) => it.tipo_gasto_id).filter(Boolean)),
    );
    const tiposGasto = tipoIds.length
      ? await this.prisma.tipo_gasto.findMany({
          where: { id: { in: tipoIds }, empresa_id: empresaId },
          select: { id: true, descripcion: true },
        })
      : [];
    const tipoNombreById = new Map(tiposGasto.map((t) => [t.id, t.descripcion]));

    // Sin factura: forzar items con IVA=0 (no genera IVA crédito).
    // Descripción: si no viene, usar el nombre del tipo de gasto (evita error
    // "cada ítem debe tener producto_id o descripción" cuando el operador deja
    // el detalle vacío — los gastos de viáticos no tienen producto_id).
    const items = dto.items.map((it) => ({
      tipo_gasto_id: it.tipo_gasto_id,
      descripcion:
        (it.descripcion && it.descripcion.trim()) ||
        tipoNombreById.get(it.tipo_gasto_id) ||
        'Gasto viático',
      cantidad: it.cantidad,
      precio_unitario: it.precio_unitario,
      porcentaje_iva: conFactura ? (it.porcentaje_iva ?? 10) : 0,
    }));

    const gasto = await this.gastosService.create(
      { cabecera, items },
      empresaId,
      userId,
    );

    // Recalcular totales de la rendición.
    await this.recalcularTotales(empresaId, rendicionId);

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'CREATE',
      entity_type: 'gasto_rendicion',
      entity_id: gasto?.data?.id ?? gasto?.id ?? '',
      descripcion: `Gasto ${conFactura ? 'con factura' : 'sin factura'} agregado a rendición ${rendicion.numero_rendicion}`,
      new_value: { con_factura: conFactura, monto_items: items.length },
    });

    return this.findOne(empresaId, rendicionId);
  }

  /**
   * Anula un gasto vinculado a la rendición. Marca el gasto como anulado y
   * recalcula el saldo. NO borra el registro; queda auditado.
   */
  async quitarGasto(empresaId: string, rendicionId: string, gastoId: string, motivo: string, userId?: string) {
    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id: rendicionId, empresa_id: empresaId },
      select: { id: true, estado: true, numero_rendicion: true },
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    if (['CERRADA', 'ANULADA'].includes(rendicion.estado)) {
      throw new BadRequestException('No se puede quitar gastos de una rendición cerrada o anulada');
    }

    const gasto = await this.prisma.gasto_cab.findFirst({
      where: {
        id: gastoId,
        empresa_id: empresaId,
        rendicion_viatico_id: rendicionId,
      },
      select: { id: true, anulado: true },
    });
    if (!gasto) throw new NotFoundException('Gasto no encontrado en esta rendición');
    if (gasto.anulado) throw new BadRequestException('El gasto ya está anulado');

    await this.gastosService.anular(gastoId, empresaId, userId ?? '', motivo || 'Anulado desde rendición');

    await this.recalcularTotales(empresaId, rendicionId);

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'DELETE',
      entity_type: 'gasto_rendicion',
      entity_id: gastoId,
      descripcion: `Gasto quitado de rendición ${rendicion.numero_rendicion}`,
    });

    return this.findOne(empresaId, rendicionId);
  }

  /**
   * Recalcula monto_rendido_total y saldo a partir de los gastos activos.
   * Se llama después de agregar/quitar gasto.
   */
  async recalcularTotales(empresaId: string, rendicionId: string) {
    const [rendicion, agg] = await Promise.all([
      this.prisma.rendicion_viatico.findFirst({
        where: { id: rendicionId, empresa_id: empresaId },
        select: { id: true, monto_adelanto_total: true },
      }),
      this.prisma.gasto_cab.aggregate({
        where: {
          rendicion_viatico_id: rendicionId,
          empresa_id: empresaId,
          activo: true,
          anulado: false,
        },
        _sum: { total: true },
      }),
    ]);
    if (!rendicion) return;

    const rendido = Number(agg._sum.total ?? 0);
    const adelanto = Number(rendicion.monto_adelanto_total ?? 0);
    const saldo = adelanto - rendido;

    await this.prisma.rendicion_viatico.update({
      where: { id: rendicionId },
      data: {
        monto_rendido_total: rendido,
        saldo,
        updated_at: new Date(),
      },
    });
  }

  // ── Reportes (Fase 4) ────────────────────────────────────────────────────

  /**
   * FASE 4 — Reporte auxiliar: cuenta corriente por empleado.
   *
   * Devuelve por empleado con actividad en viáticos:
   *   - Cantidad de rendiciones abiertas.
   *   - Monto adelantado / rendido / saldo agregado.
   *   - CxP diferidas pendientes (empresa le debe al empleado).
   *   - Última fecha de movimiento.
   *
   * Fuente de verdad: `rendicion_viatico` (saldos en curso) +
   * `cuentas_pagar_empleado` (reintegros diferidos pendientes).
   *
   * Ver docs/plan-rendicion-viaticos.md — sección "Reportes".
   */
  async reporteSaldosPorEmpleado(
    empresaId: string,
    opts?: { empleado_id?: string; solo_con_saldo?: boolean },
  ) {
    // 1. Rendiciones no cerradas ni anuladas — saldos abiertos.
    const rendiciones = await this.prisma.rendicion_viatico.findMany({
      where: {
        empresa_id: empresaId,
        activo: true,
        estado: { in: ['ADELANTO_PAGADO', 'SIN_ADELANTO', 'PENDIENTE_APROBACION', 'BORRADOR', 'RENDIDA'] },
        ...(opts?.empleado_id ? { empleado_id: opts.empleado_id } : {}),
      },
      select: {
        id: true,
        numero_rendicion: true,
        empleado_id: true,
        estado: true,
        fecha_inicio: true,
        fecha_fin: true,
        monto_adelanto_total: true,
        monto_rendido_total: true,
        saldo: true,
        updated_at: true,
        empleado: {
          select: { id: true, nombres: true, apellidos: true, numero_empleado: true, cedula_identidad: true },
        },
        moneda: { select: { id: true, codigo: true, simbolo: true } },
      },
    });

    // 2. CxP diferidas pendientes.
    const cxps = await this.prisma.cuentas_pagar_empleado.findMany({
      where: {
        empresa_id: empresaId,
        activo: true,
        estado: { in: ['pendiente', 'parcial'] },
        origen_tipo: 'reintegro_viatico',
        ...(opts?.empleado_id ? { empleado_id: opts.empleado_id } : {}),
      },
      select: {
        id: true,
        empleado_id: true,
        monto_original: true,
        monto_pagado: true,
        saldo_pendiente: true,
        estado: true,
        fecha_generacion: true,
        updated_at: true,
        empleado: {
          select: { id: true, nombres: true, apellidos: true, numero_empleado: true, cedula_identidad: true },
        },
      },
    });

    // 3. Agrupar por empleado_id.
    type Acc = {
      empleado: {
        id: string;
        nombres: string;
        apellidos: string;
        numero_empleado: string;
        cedula_identidad: string;
      };
      rendiciones_abiertas: number;
      monto_adelantado: number;
      monto_rendido: number;
      saldo_a_favor_empleado: number; // empresa le debe (saldo negativo de rendición o CxP diferida)
      saldo_a_favor_empresa: number; // empleado le debe (adelanto no rendido)
      cxp_pendientes: number;
      cxp_monto_pendiente: number;
      ultima_actividad: Date | null;
    };
    const map = new Map<string, Acc>();

    for (const r of rendiciones) {
      const empKey = r.empleado_id;
      if (!map.has(empKey)) {
        map.set(empKey, {
          empleado: r.empleado,
          rendiciones_abiertas: 0,
          monto_adelantado: 0,
          monto_rendido: 0,
          saldo_a_favor_empleado: 0,
          saldo_a_favor_empresa: 0,
          cxp_pendientes: 0,
          cxp_monto_pendiente: 0,
          ultima_actividad: null,
        });
      }
      const acc = map.get(empKey)!;
      acc.rendiciones_abiertas += 1;
      acc.monto_adelantado += Number(r.monto_adelanto_total || 0);
      acc.monto_rendido += Number(r.monto_rendido_total || 0);
      const saldo = Number(r.saldo || 0);
      if (saldo > 0) acc.saldo_a_favor_empresa += saldo;
      if (saldo < 0) acc.saldo_a_favor_empleado += Math.abs(saldo);
      if (r.updated_at && (!acc.ultima_actividad || r.updated_at > acc.ultima_actividad)) {
        acc.ultima_actividad = r.updated_at;
      }
    }

    for (const c of cxps) {
      const empKey = c.empleado_id;
      if (!map.has(empKey)) {
        map.set(empKey, {
          empleado: c.empleado,
          rendiciones_abiertas: 0,
          monto_adelantado: 0,
          monto_rendido: 0,
          saldo_a_favor_empleado: 0,
          saldo_a_favor_empresa: 0,
          cxp_pendientes: 0,
          cxp_monto_pendiente: 0,
          ultima_actividad: null,
        });
      }
      const acc = map.get(empKey)!;
      acc.cxp_pendientes += 1;
      acc.cxp_monto_pendiente += Number(c.saldo_pendiente || 0);
      acc.saldo_a_favor_empleado += Number(c.saldo_pendiente || 0);
      if (c.updated_at && (!acc.ultima_actividad || c.updated_at > acc.ultima_actividad)) {
        acc.ultima_actividad = c.updated_at;
      }
    }

    let items = Array.from(map.values()).map((a) => ({
      ...a,
      saldo_neto: a.saldo_a_favor_empleado - a.saldo_a_favor_empresa,
    }));

    if (opts?.solo_con_saldo) {
      items = items.filter(
        (a) => a.saldo_a_favor_empleado > 0 || a.saldo_a_favor_empresa > 0 || a.cxp_pendientes > 0,
      );
    }

    // Orden: mayor saldo pendiente primero.
    items.sort(
      (a, b) =>
        Math.abs(b.saldo_a_favor_empleado - b.saldo_a_favor_empresa) -
        Math.abs(a.saldo_a_favor_empleado - a.saldo_a_favor_empresa),
    );

    // Totales globales.
    const totales = items.reduce(
      (acc, it) => ({
        empleados_con_actividad: acc.empleados_con_actividad + 1,
        rendiciones_abiertas: acc.rendiciones_abiertas + it.rendiciones_abiertas,
        monto_adelantado: acc.monto_adelantado + it.monto_adelantado,
        monto_rendido: acc.monto_rendido + it.monto_rendido,
        saldo_a_favor_empresa: acc.saldo_a_favor_empresa + it.saldo_a_favor_empresa,
        saldo_a_favor_empleado: acc.saldo_a_favor_empleado + it.saldo_a_favor_empleado,
        cxp_pendientes: acc.cxp_pendientes + it.cxp_pendientes,
        cxp_monto_pendiente: acc.cxp_monto_pendiente + it.cxp_monto_pendiente,
      }),
      {
        empleados_con_actividad: 0,
        rendiciones_abiertas: 0,
        monto_adelantado: 0,
        monto_rendido: 0,
        saldo_a_favor_empresa: 0,
        saldo_a_favor_empleado: 0,
        cxp_pendientes: 0,
        cxp_monto_pendiente: 0,
      },
    );

    return { items, totales };
  }

  // ── CxP a empleados (Fase 4) ─────────────────────────────────────────────

  async listarCxpEmpleados(
    empresaId: string,
    opts?: { empleado_id?: string; estado?: string; desde?: string; hasta?: string },
  ) {
    const fechaFilter: { gte?: Date; lte?: Date } = {};
    if (opts?.desde) fechaFilter.gte = new Date(opts.desde);
    if (opts?.hasta) fechaFilter.lte = new Date(opts.hasta);
    const estadoNormalizado =
      opts?.estado && opts.estado !== 'todos' ? opts.estado : null;
    const cxps = await this.prisma.cuentas_pagar_empleado.findMany({
      where: {
        empresa_id: empresaId,
        activo: true,
        ...(opts?.empleado_id ? { empleado_id: opts.empleado_id } : {}),
        // Sin estado explícito → default a pendiente/parcial.
        // "todos" → no aplicar filtro.
        // valor concreto → filtrar por ese estado.
        ...(estadoNormalizado
          ? { estado: estadoNormalizado }
          : opts?.estado === 'todos'
            ? {}
            : { estado: { in: ['pendiente', 'parcial'] } }),
        ...(Object.keys(fechaFilter).length ? { fecha_generacion: fechaFilter } : {}),
      },
      include: {
        empleado: {
          select: { id: true, nombres: true, apellidos: true, numero_empleado: true },
        },
        devoluciones: {
          select: {
            id: true,
            rendicion_viatico_id: true,
            rendicion: { select: { numero_rendicion: true, concepto: true } },
          },
        },
      },
      orderBy: { fecha_generacion: 'desc' },
    });

    if (cxps.length === 0) return cxps;

    // Agregar historial de pagos (tes_movimientos con origen_tipo CXP_EMPLEADO_PAGO).
    const ids = cxps.map((c) => c.id);
    const pagos = await this.prisma.tes_movimientos.findMany({
      where: {
        empresa_id: empresaId,
        origen_tipo: 'CXP_EMPLEADO_PAGO',
        origen_id: { in: ids },
        estado: 'CONFIRMADO',
      },
      select: {
        id: true,
        origen_id: true,
        fecha: true,
        monto: true,
        referencia: true,
        cuenta: { select: { nombre: true } },
      },
      orderBy: { fecha: 'asc' },
    });
    const pagosByCxp = new Map<string, typeof pagos>();
    for (const p of pagos) {
      const key = p.origen_id ?? '';
      if (!pagosByCxp.has(key)) pagosByCxp.set(key, []);
      pagosByCxp.get(key)!.push(p);
    }
    return cxps.map((c) => ({ ...c, pagos: pagosByCxp.get(c.id) ?? [] }));
  }

  /**
   * Paga una CxP diferida al empleado.
   *   DEBE  Reintegros a Empleados Pendientes    monto
   *   HABER cuenta_tesoreria origen              monto
   *   + tes_movimientos EGRESO en la cuenta.
   */
  async pagarCxpEmpleado(
    empresaId: string,
    cxpId: string,
    dto: {
      monto: number;
      fecha: string;
      medio_pago_id?: string;
      tes_cuenta_id: string;
      numero_operacion?: string;
      observaciones?: string;
      // Cheque
      numero_cheque?: string;
      banco_emisor?: string;
      titular_cheque?: string;
      fecha_emision_cheque?: string;
      fecha_vencimiento_cheque?: string;
    },
    userId?: string,
  ) {
    const cxp = await this.prisma.cuentas_pagar_empleado.findFirst({
      where: { id: cxpId, empresa_id: empresaId, activo: true },
      include: {
        empleado: { select: { nombres: true, apellidos: true } },
      },
    });
    const medioPago = dto.medio_pago_id
      ? await this.prisma.medio_pago.findUnique({
          where: { id: dto.medio_pago_id },
          select: { codigo: true, descripcion: true },
        })
      : null;
    if (!cxp) throw new NotFoundException('CxP a empleado no encontrada');
    if (cxp.estado === 'pagada' || cxp.estado === 'anulada') {
      throw new BadRequestException(`La CxP ya está en estado ${cxp.estado}`);
    }
    if (!dto.monto || dto.monto <= 0) throw new BadRequestException('El monto debe ser mayor a 0');
    const saldo = Number(cxp.saldo_pendiente);
    if (dto.monto > saldo + 0.001) {
      throw new BadRequestException(`El monto excede el saldo pendiente (${saldo})`);
    }
    if (!dto.tes_cuenta_id) throw new BadRequestException('Elegí la cuenta de tesorería');

    const tesCuenta = await this.prisma.tes_cuentas.findFirst({
      where: { id: dto.tes_cuenta_id, empresa_id: empresaId, activo: true },
      select: { id: true, cuenta_contable_id: true, nombre: true },
    });
    if (!tesCuenta) throw new BadRequestException('Cuenta de tesorería inválida');

    const fecha = new Date(dto.fecha);
    const nuevoPagado = Number(cxp.monto_pagado) + dto.monto;
    const nuevoSaldo = saldo - dto.monto;
    const nuevoEstado = nuevoSaldo <= 0.001 ? 'pagada' : 'parcial';

    const empleadoNombre = `${cxp.empleado.nombres} ${cxp.empleado.apellidos}`;

    await this.prisma.$transaction(async (tx) => {
      // Actualizar la CxP.
      await tx.cuentas_pagar_empleado.update({
        where: { id: cxpId },
        data: {
          monto_pagado: nuevoPagado,
          saldo_pendiente: nuevoSaldo,
          estado: nuevoEstado,
          updated_at: new Date(),
        },
      });

      // Movimiento de tesorería EGRESO.
      const mov = await tx.tes_movimientos.create({
        data: {
          empresa_id: empresaId,
          cuenta_id: tesCuenta.id,
          tipo: 'EGRESO',
          estado: 'CONFIRMADO',
          fecha,
          monto: dto.monto,
          descripcion: `Pago reintegro viáticos diferido - ${empleadoNombre}`,
          referencia: dto.numero_operacion ?? dto.numero_cheque ?? null,
          origen_tipo: 'CXP_EMPLEADO_PAGO',
          origen_id: cxpId,
          created_by: userId ?? null,
        },
      });
      await tx.tes_cuentas.update({
        where: { id: tesCuenta.id },
        data: { saldo_actual: { decrement: dto.monto } },
      });

      // Si es cheque, registrar el instrumento en tes_cheques (tipo EMITIDO).
      const codigoMedio = Number(medioPago?.codigo ?? 0);
      const descMedio = (medioPago?.descripcion ?? '')
        .toLowerCase()
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '');
      const esCheque = codigoMedio === 2 || descMedio.includes('cheque');
      if (esCheque && dto.numero_cheque) {
        const inicioHoy = new Date();
        inicioHoy.setHours(0, 0, 0, 0);
        const esDiferido =
          dto.fecha_vencimiento_cheque && new Date(dto.fecha_vencimiento_cheque) > inicioHoy;
        await tx.tes_cheques.create({
          data: {
            empresa_id: empresaId,
            tipo: 'EMITIDO',
            estado: esDiferido ? 'DIFERIDO' : 'EN_CARTERA',
            numero_cheque: dto.numero_cheque,
            banco_emisor: dto.banco_emisor ?? null,
            titular: dto.titular_cheque ?? empleadoNombre,
            monto: dto.monto,
            moneda: 'PYG',
            fecha_emision: dto.fecha_emision_cheque
              ? new Date(dto.fecha_emision_cheque)
              : fecha,
            fecha_vencimiento: dto.fecha_vencimiento_cheque
              ? new Date(dto.fecha_vencimiento_cheque)
              : null,
            cuenta_id: tesCuenta.id,
            movimiento_id: mov.id,
            origen_tipo: 'CXP_EMPLEADO_PAGO',
            origen_id: cxpId,
            referencia: `Pago CxP viáticos - ${empleadoNombre}`,
            registrado_por: userId ?? null,
          },
        });
      }

    });

    // Asiento contable fuera de la transacción de tesorería (necesita su propia
    // resolución de período abierto). Best-effort — loguea + audita si falla,
    // no revierte el pago porque tesorería ya se registró correctamente.
    try {
      await this.contabilidad.registrarPagoCxpEmpleado(
        cxpId,
        empresaId,
        dto.monto,
        fecha,
        tesCuenta.id,
        userId,
      );
    } catch (err) {
      const errorMsg = err instanceof Error ? err.message : String(err);
      this.logger.warn(`Asiento de pago CxP empleado ${cxpId} no generado: ${errorMsg}`);
      await this.audit.log({
        empresa_id: empresaId,
        user_id: userId,
        action: 'ERROR',
        entity_type: 'cuentas_pagar_empleado',
        entity_id: cxpId,
        descripcion: `Pago de CxP se registró en tesorería pero el asiento contable falló — requiere regeneración manual`,
        new_value: {
          monto: dto.monto,
          tes_cuenta_id: tesCuenta.id,
          error: errorMsg,
        },
      });
    }

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'UPDATE',
      entity_type: 'cuentas_pagar_empleado',
      entity_id: cxpId,
      descripcion: `Pago de CxP diferida a ${empleadoNombre} — Gs ${dto.monto}`,
      new_value: { monto: dto.monto, tes_cuenta_id: tesCuenta.id },
    });

    return this.prisma.cuentas_pagar_empleado.findFirst({
      where: { id: cxpId },
      include: { empleado: true },
    });
  }

  async generarPDFReporteSaldos(
    empresaId: string,
    opts?: { empleado_id?: string; solo_con_saldo?: boolean },
  ): Promise<{ pdfBuffer: Buffer; filename: string }> {
    const reporte = await this.reporteSaldosPorEmpleado(empresaId, opts);
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { razon_social: true, nombre_fantasia: true, ruc: true, dv: true, celular: true, email: true, logo: true },
    });

    const payload = {
      tipo: 'base64',
      empresa: {
        razon_social: empresa?.razon_social,
        ruc: empresa?.ruc,
        dv: empresa?.dv,
        telefono: empresa?.celular,
        email: empresa?.email,
        logo_url: empresa?.logo,
        // Nombre comercial arriba y razón social debajo en el PDF (criterio de la factura).
        nombre_fantasia: empresa?.nombre_fantasia ?? null,
      },
      solo_con_saldo: !!opts?.solo_con_saldo,
      items: reporte.items.map((it) => ({
        empleado: `${it.empleado.nombres} ${it.empleado.apellidos}`.trim(),
        cedula: it.empleado.cedula_identidad,
        numero_empleado: it.empleado.numero_empleado,
        rendiciones_abiertas: it.rendiciones_abiertas,
        monto_adelantado: it.monto_adelantado,
        monto_rendido: it.monto_rendido,
        saldo_a_favor_empresa: it.saldo_a_favor_empresa,
        saldo_a_favor_empleado: it.saldo_a_favor_empleado,
        cxp_pendientes: it.cxp_pendientes,
        cxp_monto_pendiente: it.cxp_monto_pendiente,
        ultima_actividad: it.ultima_actividad,
      })),
      totales: reporte.totales,
    };

    const url = `${envs.apiGeneradorPDF}/api/rendicion-viaticos/saldos-empleado/generate-pdf`;
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      if (!response.ok) {
        const errorBody = await response.text();
        throw new BadRequestException(`Error al generar PDF: ${errorBody}`);
      }
      const json = (await response.json()) as {
        status: string;
        pdf_base64?: string;
        message?: string;
      };
      if (json.status !== 'success' || !json.pdf_base64) {
        throw new BadRequestException(json.message || 'PDF no generado');
      }
      return {
        pdfBuffer: Buffer.from(json.pdf_base64, 'base64'),
        filename: `saldos-viaticos-por-empleado.pdf`,
      };
    } catch (error) {
      if (error instanceof BadRequestException) throw error;
      const message = error instanceof Error ? error.message : 'Error desconocido';
      throw new BadRequestException(`No se pudo generar el PDF: ${message}`);
    }
  }

  async generarPDFPagoCxp(
    empresaId: string,
    cxpId: string,
  ): Promise<{ pdfBuffer: Buffer; filename: string }> {
    const cxp = await this.prisma.cuentas_pagar_empleado.findFirst({
      where: { id: cxpId, empresa_id: empresaId, activo: true },
      include: {
        empleado: {
          select: { nombres: true, apellidos: true, cedula_identidad: true, numero_empleado: true },
        },
        empresa: {
          select: { razon_social: true, nombre_fantasia: true, ruc: true, dv: true, celular: true, email: true, logo: true },
        },
        devoluciones: {
          include: { rendicion: { select: { numero_rendicion: true, concepto: true } } },
        },
      },
    });
    if (!cxp) throw new NotFoundException('CxP no encontrada');

    const pagos = await this.prisma.tes_movimientos.findMany({
      where: {
        empresa_id: empresaId,
        origen_tipo: 'CXP_EMPLEADO_PAGO',
        origen_id: cxpId,
        estado: 'CONFIRMADO',
      },
      orderBy: { fecha: 'asc' },
      select: {
        id: true,
        fecha: true,
        monto: true,
        referencia: true,
        cuenta: { select: { nombre: true } },
      },
    });

    const rendicionVinculada = cxp.devoluciones[0]?.rendicion;

    const payload = {
      tipo: 'base64',
      empresa: {
        razon_social: cxp.empresa.razon_social,
        ruc: cxp.empresa.ruc,
        dv: cxp.empresa.dv,
        telefono: cxp.empresa.celular,
        email: cxp.empresa.email,
        logo_url: cxp.empresa.logo,
        // Nombre comercial arriba y razón social debajo en el PDF (criterio de la factura).
        nombre_fantasia: cxp.empresa.nombre_fantasia ?? null,
      },
      empleado: {
        nombres: cxp.empleado.nombres,
        apellidos: cxp.empleado.apellidos,
        cedula_identidad: cxp.empleado.cedula_identidad,
        numero_empleado: cxp.empleado.numero_empleado,
      },
      cxp_id: cxp.id,
      fecha_generacion: cxp.fecha_generacion,
      monto_original: Number(cxp.monto_original),
      monto_pagado: Number(cxp.monto_pagado),
      saldo_pendiente: Number(cxp.saldo_pendiente),
      estado: cxp.estado,
      origen_rendicion: rendicionVinculada
        ? {
            numero_rendicion: rendicionVinculada.numero_rendicion,
            concepto: rendicionVinculada.concepto,
          }
        : null,
      pagos: pagos.map((p) => ({
        fecha: p.fecha,
        monto: Number(p.monto),
        referencia: p.referencia,
        cuenta: p.cuenta?.nombre,
      })),
    };

    const url = `${envs.apiGeneradorPDF}/api/cxp-empleado/generate-pdf`;
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      if (!response.ok) {
        const errorBody = await response.text();
        throw new BadRequestException(`Error al generar PDF: ${errorBody}`);
      }
      const json = (await response.json()) as {
        status: string;
        pdf_base64?: string;
        message?: string;
      };
      if (json.status !== 'success' || !json.pdf_base64) {
        throw new BadRequestException(json.message || 'PDF no generado');
      }
      return {
        pdfBuffer: Buffer.from(json.pdf_base64, 'base64'),
        filename: `comprobante-pago-cxp-${cxp.id.substring(0, 8)}.pdf`,
      };
    } catch (error) {
      if (error instanceof BadRequestException) throw error;
      const message = error instanceof Error ? error.message : 'Error desconocido';
      throw new BadRequestException(`No se pudo generar el PDF: ${message}`);
    }
  }

  // ── Config ───────────────────────────────────────────────────────────────

  async getConfig(empresaId: string) {
    return this.ensureConfig(empresaId);
  }

  async updateConfig(
    empresaId: string,
    dto: {
      anticipo_viatico_requiere_aprobacion?: boolean;
      anticipo_viatico_umbral_aprobacion?: number;
      reintegro_viatico_default?: 'INMEDIATO' | 'DIFERIDO';
      viatico_dias_alerta?: number;
    },
    userId?: string,
  ) {
    await this.ensureConfig(empresaId);
    const updated = await this.prisma.config_rrhh.update({
      where: { empresa_id: empresaId },
      data: {
        ...dto,
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'UPDATE',
      entity_type: 'config_rrhh',
      entity_id: updated.id,
      descripcion: 'Configuración de viáticos actualizada',
      new_value: dto,
    });
    return updated;
  }

  // ── PDF (msv-kude) ───────────────────────────────────────────────────────

  async generarPDF(empresaId: string, id: string): Promise<{ pdfBuffer: Buffer; filename: string }> {
    const rendicion = await this.prisma.rendicion_viatico.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        empresa: {
          select: { razon_social: true, nombre_fantasia: true, ruc: true, dv: true, celular: true, email: true, logo: true },
        },
        empleado: {
          select: { nombres: true, apellidos: true, cedula_identidad: true, numero_empleado: true },
        },
        moneda: { select: { codigo: true } },
        adelantos: {
          where: { anulado: false },
          orderBy: { fecha: 'asc' },
          include: {
            medio_pago: { select: { descripcion: true } },
            tes_cuenta: { select: { nombre: true } },
          },
        },
        devoluciones: {
          where: { anulado: false },
          orderBy: { fecha: 'asc' },
          include: {
            medio_pago: { select: { descripcion: true } },
            tes_cuenta: { select: { nombre: true } },
          },
        },
      },
    });
    if (!rendicion) throw new NotFoundException('Rendición no encontrada');

    // Gastos vinculados.
    const gastos = await this.prisma.gasto_cab.findMany({
      where: { empresa_id: empresaId, rendicion_viatico_id: id, estado: { not: 'anulada' } },
      orderBy: { fecha_emision: 'asc' },
      include: {
        proveedores: { include: { personas: { select: { razon_social: true } } } },
        gasto_det: {
          include: {
            tipo_gasto: { select: { descripcion: true } },
          },
        },
      },
    });

    const payload = {
      tipo: 'base64',
      empresa: {
        razon_social: rendicion.empresa.razon_social,
        ruc: rendicion.empresa.ruc,
        dv: rendicion.empresa.dv,
        telefono: rendicion.empresa.celular,
        email: rendicion.empresa.email,
        logo_url: rendicion.empresa.logo,
        // Nombre comercial arriba y razón social debajo en el PDF (criterio de la factura).
        nombre_fantasia: rendicion.empresa.nombre_fantasia ?? null,
      },
      empleado: {
        nombres: rendicion.empleado.nombres,
        apellidos: rendicion.empleado.apellidos,
        cedula_identidad: rendicion.empleado.cedula_identidad,
        numero_empleado: rendicion.empleado.numero_empleado,
      },
      numero_rendicion: rendicion.numero_rendicion,
      estado: rendicion.estado,
      concepto: rendicion.concepto,
      fecha_inicio: rendicion.fecha_inicio,
      fecha_fin: rendicion.fecha_fin,
      moneda: rendicion.moneda?.codigo ?? 'PYG',
      monto_adelanto_total: Number(rendicion.monto_adelanto_total || 0),
      monto_rendido_total: Number(rendicion.monto_rendido_total || 0),
      saldo: Number(rendicion.saldo || 0),
      observaciones: rendicion.observaciones,
      adelantos: rendicion.adelantos.map((a) => ({
        fecha: a.fecha,
        medio_pago: a.medio_pago?.descripcion ?? '',
        cuenta: a.tes_cuenta?.nombre ?? '',
        numero_operacion: a.numero_operacion ?? '',
        monto: Number(a.monto),
      })),
      gastos: gastos.flatMap((g) => {
        const proveedorNombre = g.proveedores?.personas?.razon_social ?? '';
        const conFactura = !!(g.numero_factura || g.timbrado);
        return g.gasto_det.map((d) => ({
          fecha: g.fecha_emision,
          con_factura: conFactura,
          proveedor: proveedorNombre,
          numero_factura:
            g.establecimiento && g.punto_expedicion && g.numero_factura
              ? `${g.establecimiento}-${g.punto_expedicion}-${g.numero_factura}`
              : g.numero_factura ?? '',
          tipo: d.tipo_gasto?.descripcion ?? '',
          descripcion: d.descripcion ?? '',
          total: Number(d.total),
        }));
      }),
      cierre: rendicion.devoluciones.map((d) => ({
        tipo: d.tipo,
        fecha: d.fecha,
        medio_pago: d.medio_pago?.descripcion ?? '',
        cuenta: d.tes_cuenta?.nombre ?? '',
        modo: d.diferido ? 'DIFERIDO' : 'INMEDIATO',
        monto: Number(d.monto),
      })),
    };

    const url = `${envs.apiGeneradorPDF}/api/rendicion-viatico/generate-pdf`;
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      if (!response.ok) {
        const errorBody = await response.text();
        throw new BadRequestException(`Error al generar PDF: ${errorBody}`);
      }
      const json = (await response.json()) as { status: string; pdf_base64?: string; message?: string };
      if (json.status !== 'success' || !json.pdf_base64) {
        throw new BadRequestException(json.message || 'PDF no generado');
      }
      return {
        pdfBuffer: Buffer.from(json.pdf_base64, 'base64'),
        filename: `rendicion-viatico-${rendicion.numero_rendicion}.pdf`,
      };
    } catch (error) {
      if (error instanceof BadRequestException) throw error;
      const message = error instanceof Error ? error.message : 'Error desconocido';
      throw new BadRequestException(`No se pudo generar el PDF: ${message}`);
    }
  }
}
