import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { ContabilidadIntegracionService } from 'src/contabilidad/services/integracion.service';
import { envs } from 'src/config/envs';
import { PrismaService } from 'src/prisma/prisma.service';
import {
  AnularOrdenPagoProveedorDto,
  CreateOrdenPagoProveedorDto,
} from './dto/create-orden-pago-proveedor.dto';
import { UpdateConfigComprasDto } from './dto/update-config-compras.dto';
import { alcanceSucursalUsuario } from 'src/utils/alcance-sucursal';
import { logoEmisor, nombreFantasiaEmisor, sucursalUnicaDeDocumentos } from 'src/utils/emisor-comprobante';

type Tx = Prisma.TransactionClient;

type NumeracionOrdenPago = {
  numeracionId: string;
  numeroOrden: string;
};

type ModoFlujoCompra = 'directo' | 'requisicion_opcional' | 'requisicion_obligatoria';

type ConfigComprasAP = {
  habilitar_requisicion_compra: boolean;
  modo_flujo_compra: ModoFlujoCompra;
  afectar_stock_en_compra_directa: boolean;
  habilitar_orden_compra: boolean;
  habilitar_recepcion_compra: boolean;
  habilitar_matching_3_vias: boolean;
  habilitar_orden_pago: boolean;
  requiere_aprobacion: boolean;
  permite_pago_sin_aprobacion: boolean;
  generar_cxp_automatico: boolean;
  permitir_edicion_cxp: boolean;
  permitir_pago_parcial: boolean;
  usar_workflow: boolean;
  workflow_obligatorio: boolean;
  aplicar_pago_al_guardar: boolean;
  incluir_contado_en_orden_pago: boolean;
  permitir_editar_gasto_pagado: boolean;
};

export type TesoreriaSyncResult = {
  warning?: string;
};

const TES_REGLA_PAGO_PROVEEDOR = 'PAGO_PROVEEDOR';
const TES_ORIGEN_OP_PROVEEDOR = 'ORDEN_PAGO_PROVEEDOR';
const TES_CATEGORIA_CODIGO_PAGO_PROVEEDOR = 'PAGO_PROVEEDOR';
const TES_CATEGORIA_CODIGO_EGRESO_OTRO = 'EGRESO_OTRO';

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly contabilidadIntegracion: ContabilidadIntegracionService,
  ) {}

  async getConfigCompras(empresaId: string): Promise<ConfigComprasAP> {
    return this.prisma.$transaction(async (tx) => this.getOrCreateConfigCompras(tx, empresaId));
  }

  async updateConfigCompras(empresaId: string, dto: UpdateConfigComprasDto): Promise<ConfigComprasAP> {
    return this.prisma.$transaction(async (tx) => {
      const current = await this.getOrCreateConfigCompras(tx, empresaId);
      const payload: Partial<ConfigComprasAP> = {};

      const booleanFields: Array<keyof Omit<ConfigComprasAP, 'modo_flujo_compra'>> = [
        'habilitar_requisicion_compra',
        'afectar_stock_en_compra_directa',
        'habilitar_orden_compra',
        'habilitar_recepcion_compra',
        'habilitar_matching_3_vias',
        'habilitar_orden_pago',
        'requiere_aprobacion',
        'permite_pago_sin_aprobacion',
        'generar_cxp_automatico',
        'permitir_edicion_cxp',
        'permitir_pago_parcial',
        'usar_workflow',
        'workflow_obligatorio',
        'aplicar_pago_al_guardar',
        'incluir_contado_en_orden_pago',
        'permitir_editar_gasto_pagado',
      ];

      for (const field of booleanFields) {
        if (dto[field] !== undefined) {
          payload[field] = Boolean(dto[field]);
        }
      }
      if (dto.modo_flujo_compra !== undefined) {
        payload.modo_flujo_compra = dto.modo_flujo_compra;
      }

      if (!Object.keys(payload).length) {
        return current;
      }

      const merged = this.normalizeConfigCompras({
        ...current,
        ...payload,
      });

      await tx.config_compras.upsert({
        where: { empresa_id: empresaId },
        create: {
          empresa_id: empresaId,
          ...merged,
        },
        update: {
          ...merged,
          updated_at: new Date(),
        },
      });

      return this.getOrCreateConfigCompras(tx, empresaId);
    });
  }

  async create(dto: CreateOrdenPagoProveedorDto, empresaId: string, userId?: string) {
    const result = await this.prisma.$transaction(async (tx) => {
      const configCompras = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!configCompras.habilitar_orden_pago) {
        throw new BadRequestException(
          'La empresa tiene deshabilitada la orden de pago en Configuración de Compras',
        );
      }
      const requiereAprobacion = this.requiereAprobacionOrdenPago(configCompras);
      const aplicarPagoAlGuardar = Boolean(configCompras.aplicar_pago_al_guardar) && !requiereAprobacion;

      const proveedor = await tx.proveedores.findFirst({
        where: { id: dto.proveedor_id, empresa_id: empresaId, activo: true },
        select: { id: true },
      });

      if (!proveedor) {
        throw new NotFoundException('Proveedor no encontrado para la empresa');
      }

      const sucursal = await tx.empresas_sucursales.findFirst({
        where: { id: dto.sucursal_id, empresa_id: empresaId, active: true },
        select: { id: true, config: true },
      });

      if (!sucursal) {
        throw new NotFoundException('Sucursal no encontrada para la empresa');
      }

      await this.validateSesionCaja(tx, {
        empresaId,
        sucursalId: dto.sucursal_id,
        sesionCajaId: dto.sesion_caja_id || null,
        requiereCaja: Boolean(
          ((sucursal.config as Record<string, unknown> | null)?.requiere_caja_pagos_proveedor as boolean) ?? false,
        ),
      });

      const detalleIds = dto.detalles.map((d) => d.cuenta_pagar_id);
      const uniqueDetalleIds = new Set(detalleIds);
      if (detalleIds.length !== uniqueDetalleIds.size) {
        throw new BadRequestException('No se puede repetir la misma cuenta por pagar en la orden');
      }

      const cuentas = await tx.cuentas_pagar.findMany({
        where: {
          id: { in: detalleIds },
          empresa_id: empresaId,
          proveedor_id: dto.proveedor_id,
          activo: true,
          estado: { not: 'anulada' },
        },
        select: {
          id: true,
          compra_id: true,
          origen_tipo: true,
          origen_id: true,
          moneda_id: true,
          saldo_pendiente: true,
          monto_pagado: true,
        },
      });

      if (cuentas.length !== uniqueDetalleIds.size) {
        throw new BadRequestException('Una o más cuentas por pagar no existen o no pertenecen al proveedor seleccionado');
      }

      const cuentasById = new Map(cuentas.map((c) => [c.id, c]));

      // Validar coherencia de moneda: todas las CxP deben estar en la misma moneda
      // que la OP, para no cerrar CxP USD con pagos en Gs. y viceversa.
      // Ver docs/plan-acreedores-proveedores-exterior.md (regla de cuenta_pasivo_id congelada).
      const monedasDistintas = new Set(
        cuentas.map((c) => c.moneda_id).filter(Boolean) as string[],
      );
      if (monedasDistintas.size > 1) {
        throw new BadRequestException(
          'Las cuentas por pagar seleccionadas tienen monedas distintas. Emití una orden de pago por moneda.',
        );
      }
      if (dto.moneda_id && monedasDistintas.size === 1) {
        const monedaCxp = Array.from(monedasDistintas)[0];
        if (monedaCxp && monedaCxp !== dto.moneda_id) {
          throw new BadRequestException(
            'La moneda de la orden de pago debe coincidir con la moneda de las cuentas por pagar seleccionadas.',
          );
        }
      }

      let totalOrden = 0;
      for (const detalle of dto.detalles) {
        const cuenta = cuentasById.get(detalle.cuenta_pagar_id);
        if (!cuenta) {
          throw new BadRequestException(`Cuenta por pagar no encontrada: ${detalle.cuenta_pagar_id}`);
        }

        const saldoPendiente = this.toNumber(cuenta.saldo_pendiente);
        if (saldoPendiente <= 0) {
          throw new BadRequestException(`La cuenta ${detalle.cuenta_pagar_id} ya no tiene saldo pendiente`);
        }

        const montoDetalle = this.toNumber(detalle.monto);
        this.assertMontoPagoValido({
          cuentaId: detalle.cuenta_pagar_id,
          montoDetalle,
          saldoPendiente,
          permitirPagoParcial: Boolean(configCompras.permitir_pago_parcial),
        });

        totalOrden += montoDetalle;
      }

      if (totalOrden <= 0) {
        throw new BadRequestException('El total de la orden de pago debe ser mayor a cero');
      }

      const { numeroOrden, numeracionId } = await this.getNextNumeroOrdenPago(tx, empresaId, dto.sucursal_id);

      const fechaPago = dto.fecha_pago ? new Date(dto.fecha_pago) : new Date();
      const cotizacion = this.toNumber(dto.cotizacion ?? 1) || 1;

      const cabecera = await tx.orden_pago_proveedor_cab.create({
        data: {
          empresa_id: empresaId,
          sucursal_id: dto.sucursal_id,
          proveedor_id: dto.proveedor_id,
          compra_id: null,
          usuario_id: userId || null,
          sesion_caja_id: dto.sesion_caja_id || null,
          moneda_id: dto.moneda_id || null,
          numero_orden_pago: numeroOrden,
          fecha_pago: fechaPago,
          monto_total: totalOrden,
          cotizacion,
          referencia: dto.referencia || null,
          observaciones: dto.observaciones || null,
          origen: 'manual',
          estado: aplicarPagoAlGuardar ? 'PAGADO' : 'BORRADOR',
          anulado: false,
          activo: true,
        },
      });

      const comprasToRecalculate = new Set<string>();
      const gastosToRecalculate = new Set<string>();

      for (const detalle of dto.detalles) {
        const cuenta = cuentasById.get(detalle.cuenta_pagar_id);
        if (!cuenta) {
          throw new BadRequestException(`Cuenta por pagar no encontrada: ${detalle.cuenta_pagar_id}`);
        }
        const montoAplicado = this.toNumber(detalle.monto);
        const montoMoneda = this.toNumber(detalle.monto_moneda ?? montoAplicado);

        await tx.orden_pago_proveedor_det.create({
          data: {
            orden_pago_id: cabecera.id,
            cuenta_pagar_id: detalle.cuenta_pagar_id,
            medio_pago_id: detalle.medio_pago_id,
            monto: montoAplicado,
            monto_moneda: montoMoneda,
            cotizacion: this.toNumber(detalle.cotizacion ?? cotizacion) || 1,
            referencia: detalle.referencia || dto.referencia || null,
            observaciones: detalle.observaciones || null,
            numero_cheque: detalle.numero_cheque || null,
            banco_emisor: detalle.banco_emisor || null,
            fecha_emision_cheque: detalle.fecha_emision_cheque ? new Date(detalle.fecha_emision_cheque) : null,
            fecha_vencimiento_cheque: detalle.fecha_vencimiento_cheque ? new Date(detalle.fecha_vencimiento_cheque) : null,
            titular_cheque: detalle.titular_cheque || null,
            cuenta_bancaria_id: detalle.cuenta_bancaria_id || null,
            numero_operacion: detalle.numero_operacion || null,
          },
        });

        if (aplicarPagoAlGuardar) {
          // Compatibilidad legacy: persistir también en pagos_proveedor existente.
          await tx.pagos_proveedor.create({
            data: {
              empresa_id: empresaId,
              cuenta_pagar_id: detalle.cuenta_pagar_id,
              proveedor_id: dto.proveedor_id,
              medio_pago_id: detalle.medio_pago_id,
              moneda_id: detalle.moneda_id || dto.moneda_id || cuenta.moneda_id || null,
              usuario_id: userId || null,
              sesion_caja_id: dto.sesion_caja_id || null,
              numero_recibo: numeroOrden,
              fecha_pago: fechaPago,
              monto: montoAplicado,
              monto_moneda: montoMoneda,
              cotizacion: this.toNumber(detalle.cotizacion ?? cotizacion) || 1,
              referencia: detalle.referencia || dto.referencia || null,
              observaciones: detalle.observaciones || dto.observaciones || null,
              estado: 'aplicado',
              anulado: false,
              activo: true,
            },
          });

          const saldoPendiente = this.toNumber(cuenta.saldo_pendiente);
          const montoPagadoActual = this.toNumber(cuenta.monto_pagado);
          const nuevoSaldo = this.round(Math.max(saldoPendiente - montoAplicado, 0), 2);
          const nuevoMontoPagado = this.round(montoPagadoActual + montoAplicado, 2);
          const nuevoEstado = nuevoSaldo <= 0 ? 'pagada' : 'parcial';

          await tx.cuentas_pagar.update({
            where: { id: detalle.cuenta_pagar_id },
            data: {
              monto_pagado: nuevoMontoPagado,
              saldo_pendiente: nuevoSaldo,
              estado: nuevoEstado,
              updated_at: new Date(),
            },
          });

          if (cuenta.compra_id) {
            comprasToRecalculate.add(cuenta.compra_id);
          }
          const cuentaOrigen = this.resolveCuentaOrigen(cuenta);
          if (cuentaOrigen.origen_tipo === 'gasto' && cuentaOrigen.origen_id) {
            gastosToRecalculate.add(cuentaOrigen.origen_id);
          }
        }
      }

      if (aplicarPagoAlGuardar) {
        for (const compraId of comprasToRecalculate) {
          await this.recalculateCompraEstado(tx, compraId);
          await this.recalculateAutoOrderEstado(tx, compraId);
        }
        for (const gastoId of gastosToRecalculate) {
          await this.recalculateGastoEstado(tx, gastoId);
        }
      }

      await tx.numeraciones_documento.update({
        where: { id: numeracionId },
        data: { updated_at: new Date() },
      });

      return this.findOne(cabecera.id, empresaId, tx);
    });

    let tesoreriaSync: TesoreriaSyncResult | null = null;
    if (result?.id) {
      tesoreriaSync = await this.syncTesoreriaOrdenPago(result.id, empresaId, userId);
    }

    // Hook contable — fire and forget (solo integra si la orden queda en estado PAGADO)
    if (result?.id) {
      this.contabilidadIntegracion.integrarOrdenPago(result.id).catch((err) => {
        this.logger.error(`Error contabilizando orden pago ${result.id}: ${err.message}`);
      });
    }

    if (tesoreriaSync?.warning) {
      return {
        ...result,
        tesoreria_warning: tesoreriaSync.warning,
      };
    }

    return result;
  }

  async findAll(
    empresaId: string,
    query?: {
      page?: string;
      limit?: string;
      fecha_desde?: string;
      fecha_hasta?: string;
      proveedor_id?: string;
      compra_id?: string;
      origen?: string;
      origen_tipo?: string;
      estado?: string;
      busqueda?: string;
    },
  ) {
    // El tope era 100 y se aplicaba en silencio: quien pedia limit=1000 recibia 100
    // sin ninguna senal, y como la pantalla de Cuentas por Pagar agrupa y filtra del
    // lado del cliente, terminaba mostrando solo un pedazo de la deuda mientras las
    // tarjetas -que suman en el server- mostraban el total. Se sube el tope y la
    // respuesta expone `limit` y `total` para que el consumidor sepa si falta paginar.
    const take = Math.max(1, Math.min(500, Number(query?.limit) || 20));
    const currentPage = Math.max(1, Number(query?.page) || 1);
    const skip = (currentPage - 1) * take;

    const where: Prisma.orden_pago_proveedor_cabWhereInput = {
      empresa_id: empresaId,
      activo: true,
    };

    if (query?.proveedor_id) where.proveedor_id = query.proveedor_id;
    if (query?.compra_id) where.compra_id = query.compra_id;
    if (query?.origen) where.origen = query.origen;
    const origenTipo = this.normalizeOrigenTipo(query?.origen_tipo);
    if (origenTipo) {
      where.orden_pago_proveedor_det = {
        some: {
          cuentas_pagar: this.getCuentaOrigenFilter(origenTipo),
        },
      };
    }

    if (query?.estado) {
      const estado = query.estado.toLowerCase();
      if (estado === 'anulada' || estado === 'anulado') {
        where.anulado = true;
      } else if (estado === 'borrador' || estado === 'pendiente') {
        where.estado = { in: ['BORRADOR', 'pendiente'] };
        where.anulado = false;
      } else if (estado === 'pendiente_aprobacion' || estado === 'en_aprobacion') {
        where.estado = 'PENDIENTE_APROBACION';
        where.anulado = false;
      } else if (estado === 'rechazada' || estado === 'rechazado') {
        where.estado = 'RECHAZADA';
        where.anulado = false;
      } else if (estado === 'pagado' || estado === 'aplicado') {
        where.estado = { in: ['PAGADO', 'aplicado'] };
        where.anulado = false;
      } else {
        where.estado = query.estado;
        where.anulado = false;
      }
    }

    if (query?.fecha_desde || query?.fecha_hasta) {
      where.fecha_pago = {};
      if (query.fecha_desde) where.fecha_pago.gte = new Date(query.fecha_desde);
      if (query.fecha_hasta) where.fecha_pago.lte = new Date(query.fecha_hasta);
    }

    const busqueda = query?.busqueda?.trim();
    if (busqueda) {
      const cuentaBusquedaWhere = await this.buildCuentaPagarBusquedaWhere(empresaId, busqueda);
      where.OR = [
        { numero_orden_pago: { contains: busqueda, mode: 'insensitive' } },
        {
          proveedores: {
            personas: {
              razon_social: { contains: busqueda, mode: 'insensitive' },
            },
          },
        },
        {
          proveedores: {
            personas: {
              ruc: { contains: busqueda, mode: 'insensitive' },
            },
          },
        },
        {
          orden_pago_proveedor_det: {
            some: {
              cuentas_pagar: cuentaBusquedaWhere,
            },
          },
        },
      ];
    }

    const [items, total] = await this.prisma.$transaction([
      this.prisma.orden_pago_proveedor_cab.findMany({
        where,
        include: {
          proveedores: {
            include: {
              personas: {
                select: { razon_social: true, ruc: true, dv: true },
              },
            },
          },
          compra_cab: {
            select: {
              id: true,
              establecimiento: true,
              punto_expedicion: true,
              numero_factura: true,
              fecha_emision: true,
            },
          },
          moneda: { select: { id: true, codigo: true, descripcion: true, simbolo: true } },
          orden_pago_proveedor_det: {
            select: {
              id: true,
              cuentas_pagar: {
                select: {
                  id: true,
                  compra_id: true,
                  origen_tipo: true,
                  origen_id: true,
                  compra_cab: {
                    select: {
                      id: true,
                      establecimiento: true,
                      punto_expedicion: true,
                      numero_factura: true,
                      fecha_emision: true,
                      total: true,
                    },
                  },
                },
              },
            },
          },
        },
        orderBy: [{ fecha_pago: 'desc' }, { created_at: 'desc' }],
        skip,
        take,
      }),
      this.prisma.orden_pago_proveedor_cab.count({ where }),
    ]);
    const enrichedItems = await this.enrichOrdenesPagoOrigen(items, empresaId);

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

  async getDashboard(
    empresaId: string,
    query?: {
      fecha_desde?: string;
      fecha_hasta?: string;
      origen?: string;
      origen_tipo?: string;
    },
  ) {
    const whereBase: Prisma.orden_pago_proveedor_cabWhereInput = {
      empresa_id: empresaId,
      activo: true,
    };

    if (query?.origen) {
      whereBase.origen = query.origen;
    }
    const origenTipo = this.normalizeOrigenTipo(query?.origen_tipo);
    if (origenTipo) {
      whereBase.orden_pago_proveedor_det = {
        some: {
          cuentas_pagar: this.getCuentaOrigenFilter(origenTipo),
        },
      };
    }

    if (query?.fecha_desde || query?.fecha_hasta) {
      whereBase.fecha_pago = {};
      if (query.fecha_desde) whereBase.fecha_pago.gte = new Date(query.fecha_desde);
      if (query.fecha_hasta) whereBase.fecha_pago.lte = new Date(query.fecha_hasta);
    }

    const [total, pagadas, anuladas, monto] = await this.prisma.$transaction([
      this.prisma.orden_pago_proveedor_cab.count({ where: whereBase }),
      this.prisma.orden_pago_proveedor_cab.count({
        where: {
          ...whereBase,
          anulado: false,
          estado: { in: ['PAGADO', 'aplicado'] },
        },
      }),
      this.prisma.orden_pago_proveedor_cab.count({ where: { ...whereBase, anulado: true } }),
      this.prisma.orden_pago_proveedor_cab.aggregate({
        where: {
          ...whereBase,
          anulado: false,
        },
        _sum: { monto_total: true },
      }),
    ]);

    return {
      total,
      aplicadas: pagadas,
      pagadas,
      anuladas,
      monto_total: this.toNumber(monto._sum.monto_total),
    };
  }

  async findCuentasPagar(
    empresaId: string,
    query?: {
      page?: string;
      limit?: string;
      fecha_tipo?: string;
      fecha_desde?: string;
      fecha_hasta?: string;
      proveedor_id?: string;
      compra_id?: string;
      origen_tipo?: string;
      estado?: string;
      busqueda?: string;
    },
  ) {
    const take = Math.max(1, Math.min(100, Number(query?.limit) || 20));
    const currentPage = Math.max(1, Number(query?.page) || 1);
    const skip = (currentPage - 1) * take;

    const where: Prisma.cuentas_pagarWhereInput = {
      empresa_id: empresaId,
      activo: true,
    };
    const and: Prisma.cuentas_pagarWhereInput[] = [];

    if (query?.proveedor_id) where.proveedor_id = query.proveedor_id;
    if (query?.compra_id) where.compra_id = query.compra_id;

    // Filtro por estado. "vencida" y "pendiente" se resuelven EN VIVO (no por el `estado`
    // guardado, que puede quedar desactualizado): una cuota está vencida si tiene saldo y su
    // vencimiento ya pasó. Se computa en la DB (condiciones indexables, sin filtrar en memoria)
    // usando el MISMO criterio que el KPI "Vencidas" del dashboard, así ambos cuadran.
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const estadoFiltro = query?.estado?.toLowerCase();
    if (estadoFiltro === 'vencida') {
      and.push({
        saldo_pendiente: { gt: 0 },
        OR: [{ estado: 'vencida' }, { fecha_vencimiento: { lt: today } }],
      });
    } else if (estadoFiltro === 'pendiente') {
      // Complemento de "vencida": por cobrar y aún NO vencida.
      and.push({
        saldo_pendiente: { gt: 0 },
        estado: { notIn: ['pagada', 'anulada', 'vencida'] },
        OR: [{ fecha_vencimiento: { gte: today } }, { fecha_vencimiento: null }],
      });
    } else if (estadoFiltro) {
      where.estado = estadoFiltro;
    }
    const origenTipo = this.normalizeOrigenTipo(query?.origen_tipo);
    if (origenTipo) {
      and.push(this.getCuentaOrigenFilter(origenTipo));
    }

    const fechaTipo = String(query?.fecha_tipo || 'vencimiento').toLowerCase() === 'emision' ? 'emision' : 'vencimiento';
    if (query?.fecha_desde || query?.fecha_hasta) {
      const fechaDesde = query.fecha_desde ? this.parseDateStart(query.fecha_desde) : undefined;
      const fechaHasta = query.fecha_hasta ? this.parseDateEnd(query.fecha_hasta) : undefined;

      if (fechaTipo === 'emision') {
        where.fecha_emision = {};
        if (fechaDesde) where.fecha_emision.gte = fechaDesde;
        if (fechaHasta) where.fecha_emision.lte = fechaHasta;
      } else {
        where.fecha_vencimiento = {};
        if (fechaDesde) where.fecha_vencimiento.gte = fechaDesde;
        if (fechaHasta) where.fecha_vencimiento.lte = fechaHasta;
      }
    }

    const busqueda = query?.busqueda?.trim();
    if (busqueda) {
      and.push(await this.buildCuentaPagarBusquedaWhere(empresaId, busqueda));
    }
    if (and.length > 0) {
      where.AND = and;
    }

    const [items, total] = await this.prisma.$transaction([
      this.prisma.cuentas_pagar.findMany({
        where,
        include: {
          moneda: {
            select: { id: true, codigo: true, descripcion: true, simbolo: true },
          },
          proveedores: {
            include: {
              personas: {
                select: {
                  razon_social: true,
                  ruc: true,
                  dv: true,
                },
              },
            },
          },
          compra_cab: {
            select: {
              id: true,
              establecimiento: true,
              punto_expedicion: true,
              numero_factura: true,
              fecha_emision: true,
              total: true,
              estado: true,
              anulado: true,
            },
          },
          orden_pago_proveedor_det: {
            select: {
              id: true,
              orden_pago_id: true,
              monto: true,
              created_at: true,
              orden_pago_proveedor_cab: {
                select: {
                  id: true,
                  numero_orden_pago: true,
                  fecha_pago: true,
                  anulado: true,
                },
              },
            },
            orderBy: { created_at: 'desc' },
          },
        },
        orderBy: [{ fecha_vencimiento: 'asc' }, { created_at: 'desc' }],
        skip,
        take,
        }),
      this.prisma.cuentas_pagar.count({ where }),
    ]);
    const enrichedItems = await this.enrichCuentasPagarOrigen(items, empresaId);

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

  async getCuentasPagarDashboard(
    empresaId: string,
    query?: {
      fecha_tipo?: string;
      fecha_desde?: string;
      fecha_hasta?: string;
      origen_tipo?: string;
    },
  ) {
    const whereBase: Prisma.cuentas_pagarWhereInput = {
      empresa_id: empresaId,
      activo: true,
    };
    const and: Prisma.cuentas_pagarWhereInput[] = [];
    const origenTipo = this.normalizeOrigenTipo(query?.origen_tipo);
    if (origenTipo) {
      and.push(this.getCuentaOrigenFilter(origenTipo));
    }

    const fechaTipo = String(query?.fecha_tipo || 'vencimiento').toLowerCase() === 'emision' ? 'emision' : 'vencimiento';
    if (query?.fecha_desde || query?.fecha_hasta) {
      const fechaDesde = query.fecha_desde ? this.parseDateStart(query.fecha_desde) : undefined;
      const fechaHasta = query.fecha_hasta ? this.parseDateEnd(query.fecha_hasta) : undefined;

      if (fechaTipo === 'emision') {
        whereBase.fecha_emision = {};
        if (fechaDesde) whereBase.fecha_emision.gte = fechaDesde;
        if (fechaHasta) whereBase.fecha_emision.lte = fechaHasta;
      } else {
        whereBase.fecha_vencimiento = {};
        if (fechaDesde) whereBase.fecha_vencimiento.gte = fechaDesde;
        if (fechaHasta) whereBase.fecha_vencimiento.lte = fechaHasta;
      }
    }
    if (and.length > 0) {
      whereBase.AND = and;
    }

    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const [total, pendientes, pagadas, vencidas, sumSaldo, sumPagado] = await this.prisma.$transaction([
      this.prisma.cuentas_pagar.count({ where: whereBase }),
      this.prisma.cuentas_pagar.count({
        where: {
          ...whereBase,
          saldo_pendiente: { gt: 0 },
          estado: { in: ['pendiente', 'parcial', 'vencida'] },
        },
      }),
      this.prisma.cuentas_pagar.count({
        where: {
          ...whereBase,
          estado: 'pagada',
        },
      }),
      this.prisma.cuentas_pagar.count({
        where: {
          ...whereBase,
          saldo_pendiente: { gt: 0 },
          OR: [{ estado: 'vencida' }, { fecha_vencimiento: { lt: today } }],
        },
      }),
      this.prisma.cuentas_pagar.aggregate({
        where: {
          ...whereBase,
          saldo_pendiente: { gt: 0 },
        },
        _sum: { saldo_pendiente: true },
      }),
      this.prisma.cuentas_pagar.aggregate({
        where: whereBase,
        _sum: { monto_pagado: true },
      }),
    ]);

    return {
      total,
      pendientes,
      pagadas,
      vencidas,
      saldo_pendiente_total: this.toNumber(sumSaldo._sum.saldo_pendiente),
      monto_pagado_total: this.toNumber(sumPagado._sum.monto_pagado),
    };
  }

  async getCuentasPendientes(empresaId: string, proveedorId: string) {
    const proveedor = await this.prisma.proveedores.findFirst({
      where: { id: proveedorId, empresa_id: empresaId, activo: true },
      select: { id: true },
    });

    if (!proveedor) {
      throw new NotFoundException('Proveedor no encontrado para la empresa');
    }

    const items = await this.prisma.cuentas_pagar.findMany({
      where: {
        empresa_id: empresaId,
        proveedor_id: proveedorId,
        activo: true,
        estado: { in: ['pendiente', 'parcial', 'vencida'] },
        saldo_pendiente: { gt: 0 },
      },
      include: {
        moneda: { select: { id: true, codigo: true, descripcion: true, simbolo: true } },
        compra_cab: {
          select: {
            id: true,
            establecimiento: true,
            punto_expedicion: true,
            numero_factura: true,
            fecha_emision: true,
            total: true,
          },
        },
      },
      orderBy: [{ fecha_vencimiento: 'asc' }, { created_at: 'asc' }],
    });
    const enrichedItems = await this.enrichCuentasPagarOrigen(items, empresaId);

    const totalSaldo = enrichedItems.reduce((acc, item) => acc + this.toNumber(item.saldo_pendiente), 0);

    return {
      items: enrichedItems,
      total: enrichedItems.length,
      total_saldo: this.round(totalSaldo, 2),
    };
  }

  async anular(id: string, dto: AnularOrdenPagoProveedorDto, empresaId: string, userId?: string) {
    const result = await this.prisma.$transaction(async (tx) => {
      const orden = await tx.orden_pago_proveedor_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        include: {
          orden_pago_proveedor_det: true,
        },
      });

      if (!orden) throw new NotFoundException('Orden de pago no encontrada');
      if (orden.anulado) throw new BadRequestException('La orden de pago ya está anulada');

      // Bloquear si hay cheques de esta orden que ya fueron cobrados/depositados por el banco:
      // esos movimientos de caja deben revertirse manualmente desde Tesorería antes de anular.
      if (this.hasTesoreriaBridgeSupport(tx as any)) {
        const chequesFueraDeCartera = await tx.tes_cheques.findMany({
          where: {
            empresa_id: empresaId,
            origen_tipo: TES_ORIGEN_OP_PROVEEDOR,
            origen_id: id,
            estado: { notIn: ['EN_CARTERA', 'DIFERIDO', 'ANULADO'] },
          },
          select: { numero_cheque: true, estado: true },
        });
        if (chequesFueraDeCartera.length) {
          const detalle = chequesFueraDeCartera
            .map((c) => `Nº ${c.numero_cheque} (${c.estado})`)
            .join(', ');
          throw new BadRequestException(
            `No se puede anular: hay cheques fuera de cartera. Revertilos primero desde Tesorería: ${detalle}`,
          );
        }
      }

      const estadoOrden = String(orden.estado || '').toUpperCase();
      const ordenAplicada = estadoOrden === 'PAGADO' || estadoOrden === 'APLICADO';

      const comprasToRecalculate = new Set<string>();
      const gastosToRecalculate = new Set<string>();

      for (const detalle of orden.orden_pago_proveedor_det) {
        if (ordenAplicada) {
          const cuenta = await tx.cuentas_pagar.findUnique({
            where: { id: detalle.cuenta_pagar_id },
            select: {
              id: true,
              compra_id: true,
              origen_tipo: true,
              origen_id: true,
              monto_pagado: true,
              saldo_pendiente: true,
              monto_original: true,
            },
          });

          if (!cuenta) continue;

          const montoAplicado = this.toNumber(detalle.monto);
          const montoPagadoActual = this.toNumber(cuenta.monto_pagado);
          const saldoActual = this.toNumber(cuenta.saldo_pendiente);
          const montoOriginal = this.toNumber(cuenta.monto_original);

          const nuevoMontoPagado = Math.max(0, montoPagadoActual - montoAplicado);
          const nuevoSaldo = this.round(saldoActual + montoAplicado, 2);

          let nuevoEstado = 'pendiente';
          if (nuevoSaldo <= 0) nuevoEstado = 'pagada';
          else if (nuevoMontoPagado > 0 && nuevoMontoPagado < montoOriginal) nuevoEstado = 'parcial';

          await tx.cuentas_pagar.update({
            where: { id: cuenta.id },
            data: {
              monto_pagado: nuevoMontoPagado,
              saldo_pendiente: nuevoSaldo,
              estado: nuevoEstado,
              updated_at: new Date(),
            },
          });

          if (cuenta.compra_id) {
            comprasToRecalculate.add(cuenta.compra_id);
          }
          const cuentaOrigen = this.resolveCuentaOrigen(cuenta);
          if (cuentaOrigen.origen_tipo === 'gasto' && cuentaOrigen.origen_id) {
            gastosToRecalculate.add(cuentaOrigen.origen_id);
          }

          await tx.pagos_proveedor.updateMany({
            where: {
              empresa_id: empresaId,
              cuenta_pagar_id: detalle.cuenta_pagar_id,
              numero_recibo: orden.numero_orden_pago,
              anulado: false,
            },
            data: {
              anulado: true,
              estado: 'anulado',
              fecha_anulacion: new Date(),
              motivo_anulacion: dto.motivo || 'Anulación de orden de pago',
              updated_at: new Date(),
            },
          });
        }
      }

      await tx.orden_pago_proveedor_cab.update({
        where: { id: orden.id },
        data: {
          anulado: true,
          estado: 'ANULADA',
          fecha_anulacion: new Date(),
          motivo_anulacion: dto.motivo || 'Anulación manual',
          updated_at: new Date(),
          usuario_id: userId || orden.usuario_id,
        },
      });

      for (const compraId of comprasToRecalculate) {
        await this.recalculateCompraEstado(tx, compraId);
        await this.recalculateAutoOrderEstado(tx, compraId);
      }
      for (const gastoId of gastosToRecalculate) {
        await this.recalculateGastoEstado(tx, gastoId);
      }

      return {
        message: 'Orden de pago anulada exitosamente',
        id: orden.id,
      };
    });

    if (result?.id) {
      await this.revertTesoreriaOrdenPago(result.id, empresaId, userId, dto.motivo);
    }

    // Reversión contable — fire and forget
    if (result?.id) {
      this.contabilidadIntegracion.revertirDocumento('orden_pago_proveedor_cab', result.id, userId ?? '').catch((err) => {
        this.logger.error(`Error revirtiendo asiento de orden pago ${result.id}: ${err.message}`);
      });
    }

    return result;
  }

  async reintentarContabilidad(id: string, empresaId: string) {
    const orden = await this.prisma.orden_pago_proveedor_cab.findFirst({
      where: { id, empresa_id: empresaId },
      select: { id: true, anulado: true, estado: true },
    });
    if (!orden) throw new NotFoundException('Orden de pago no encontrada');
    if (orden.anulado) {
      throw new BadRequestException('No se puede contabilizar una orden de pago anulada');
    }
    if (String(orden.estado || '').toUpperCase() !== 'PAGADO') {
      throw new BadRequestException(
        'Solo se contabilizan órdenes de pago en estado PAGADO',
      );
    }

    await this.contabilidadIntegracion.integrarOrdenPago(id);
    return this.findOne(id, empresaId);
  }

  async reintentarTesoreria(id: string, empresaId: string, userId?: string) {
    const orden = await this.prisma.orden_pago_proveedor_cab.findFirst({
      where: { id, empresa_id: empresaId },
      select: { id: true, anulado: true, estado: true },
    });
    if (!orden) throw new NotFoundException('Orden de pago no encontrada');
    if (orden.anulado) {
      throw new BadRequestException('No se puede registrar en Tesorería una orden de pago anulada');
    }
    const estado = String(orden.estado || '').toUpperCase();
    if (estado !== 'PAGADO' && estado !== 'APLICADO') {
      throw new BadRequestException(
        'Solo se registran en Tesorería órdenes de pago en estado PAGADO/APLICADO',
      );
    }

    const tesoreriaSync = await this.syncTesoreriaOrdenPago(id, empresaId, userId);
    const result = await this.findOne(id, empresaId);
    if (tesoreriaSync?.warning) {
      return { ...result, tesoreria_warning: tesoreriaSync.warning };
    }
    return result;
  }

  async enviarAprobacion(id: string, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      const configCompras = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!configCompras.habilitar_orden_pago) {
        throw new BadRequestException(
          'La empresa tiene deshabilitada la orden de pago en Configuración de Compras',
        );
      }
      if (!this.requiereAprobacionOrdenPago(configCompras)) {
        throw new BadRequestException('La configuración actual no requiere aprobación de órdenes de pago');
      }

      const orden = await tx.orden_pago_proveedor_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true, anulado: false },
        select: { id: true, estado: true, usuario_id: true },
      });
      if (!orden) throw new NotFoundException('Orden de pago no encontrada');

      const estado = String(orden.estado || '').toUpperCase();
      if (estado === 'PENDIENTE_APROBACION') {
        throw new BadRequestException('La orden ya se encuentra pendiente de aprobación');
      }
      if (estado === 'PAGADO' || estado === 'APLICADO') {
        throw new BadRequestException('No se puede enviar a aprobación una orden ya ejecutada');
      }
      if (estado !== 'BORRADOR' && estado !== 'RECHAZADA') {
        throw new BadRequestException('Solo se puede enviar a aprobación una orden en BORRADOR o RECHAZADA');
      }

      await tx.orden_pago_proveedor_cab.update({
        where: { id: orden.id },
        data: {
          estado: 'PENDIENTE_APROBACION',
          usuario_id: userId || orden.usuario_id,
          updated_at: new Date(),
        },
      });
    });

    return this.findOne(id, empresaId);
  }

  async aprobar(id: string, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      const configCompras = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!configCompras.habilitar_orden_pago) {
        throw new BadRequestException(
          'La empresa tiene deshabilitada la orden de pago en Configuración de Compras',
        );
      }
      if (!this.requiereAprobacionOrdenPago(configCompras)) {
        throw new BadRequestException('La configuración actual no requiere aprobación de órdenes de pago');
      }

      const orden = await tx.orden_pago_proveedor_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true, anulado: false },
        select: { id: true, estado: true, usuario_id: true },
      });
      if (!orden) throw new NotFoundException('Orden de pago no encontrada');

      const estado = String(orden.estado || '').toUpperCase();
      if (estado !== 'PENDIENTE_APROBACION') {
        throw new BadRequestException('Solo se puede aprobar una orden en estado PENDIENTE_APROBACION');
      }

      await tx.orden_pago_proveedor_cab.update({
        where: { id: orden.id },
        data: {
          estado: 'PENDIENTE',
          usuario_id: userId || orden.usuario_id,
          updated_at: new Date(),
        },
      });
    });

    return this.findOne(id, empresaId);
  }

  async rechazar(id: string, motivo: string | undefined, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      const configCompras = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!configCompras.habilitar_orden_pago) {
        throw new BadRequestException(
          'La empresa tiene deshabilitada la orden de pago en Configuración de Compras',
        );
      }
      if (!this.requiereAprobacionOrdenPago(configCompras)) {
        throw new BadRequestException('La configuración actual no requiere aprobación de órdenes de pago');
      }

      const orden = await tx.orden_pago_proveedor_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true, anulado: false },
        select: { id: true, estado: true, usuario_id: true, observaciones: true },
      });
      if (!orden) throw new NotFoundException('Orden de pago no encontrada');

      const estado = String(orden.estado || '').toUpperCase();
      if (estado !== 'PENDIENTE_APROBACION') {
        throw new BadRequestException('Solo se puede rechazar una orden en estado PENDIENTE_APROBACION');
      }

      const observaciones = this.mergeObservacionWorkflow(orden.observaciones, `Rechazada: ${motivo || 'Sin motivo'}`);
      await tx.orden_pago_proveedor_cab.update({
        where: { id: orden.id },
        data: {
          estado: 'RECHAZADA',
          usuario_id: userId || orden.usuario_id,
          observaciones,
          updated_at: new Date(),
        },
      });
    });

    return this.findOne(id, empresaId);
  }

  async ejecutar(id: string, empresaId: string, userId?: string) {
    const result = await this.prisma.$transaction(async (tx) => {
      const configCompras = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!configCompras.habilitar_orden_pago) {
        throw new BadRequestException(
          'La empresa tiene deshabilitada la orden de pago en Configuración de Compras',
        );
      }

      const orden = await tx.orden_pago_proveedor_cab.findFirst({
        where: {
          id,
          empresa_id: empresaId,
          activo: true,
          anulado: false,
        },
        include: {
          orden_pago_proveedor_det: true,
        },
      });

      if (!orden) throw new NotFoundException('Orden de pago no encontrada');

      const estado = String(orden.estado || '').toUpperCase();
      const requiereAprobacion = this.requiereAprobacionOrdenPago(configCompras);
      if (estado === 'PAGADO' || estado === 'APLICADO') {
        throw new BadRequestException('La orden ya fue ejecutada');
      }
      if (estado === 'PENDIENTE_APROBACION') {
        throw new BadRequestException('La orden aún está pendiente de aprobación');
      }
      if (estado === 'RECHAZADA') {
        throw new BadRequestException('La orden fue rechazada y debe reenviarse a aprobación');
      }
      if (requiereAprobacion && estado === 'BORRADOR') {
        throw new BadRequestException('La orden requiere aprobación previa. Enviá y aprobá la OP antes de ejecutar');
      }
      if (estado !== 'BORRADOR' && estado !== 'PENDIENTE') {
        throw new BadRequestException('Solo se puede ejecutar una orden en estado BORRADOR o PENDIENTE');
      }

      if (!orden.orden_pago_proveedor_det.length) {
        throw new BadRequestException('La orden no tiene detalles para ejecutar');
      }

      const detalleIds = orden.orden_pago_proveedor_det.map((det) => det.cuenta_pagar_id);
      const cuentas = await tx.cuentas_pagar.findMany({
        where: {
          id: { in: detalleIds },
          empresa_id: empresaId,
          proveedor_id: orden.proveedor_id,
          activo: true,
          estado: { not: 'anulada' },
        },
          select: {
            id: true,
            compra_id: true,
            origen_tipo: true,
            origen_id: true,
            moneda_id: true,
            monto_pagado: true,
            saldo_pendiente: true,
          },
        });
      const cuentasById = new Map(cuentas.map((cuenta) => [cuenta.id, cuenta]));
      if (cuentasById.size !== detalleIds.length) {
        throw new BadRequestException(
          'Una o más cuentas de la orden ya no están disponibles para ejecutar el pago',
        );
      }

      const comprasToRecalculate = new Set<string>();
      const gastosToRecalculate = new Set<string>();
      for (const detalle of orden.orden_pago_proveedor_det) {
        const cuenta = cuentasById.get(detalle.cuenta_pagar_id);
        if (!cuenta) {
          throw new BadRequestException(`Cuenta por pagar no encontrada: ${detalle.cuenta_pagar_id}`);
        }

        const saldoPendiente = this.toNumber(cuenta.saldo_pendiente);
        const montoDetalle = this.toNumber(detalle.monto);
        if (saldoPendiente <= 0) {
          throw new BadRequestException(`La cuenta ${detalle.cuenta_pagar_id} ya no tiene saldo pendiente`);
        }
        this.assertMontoPagoValido({
          cuentaId: detalle.cuenta_pagar_id,
          montoDetalle,
          saldoPendiente,
          permitirPagoParcial: Boolean(configCompras.permitir_pago_parcial),
          changedBalanceMessage:
            `La cuenta ${detalle.cuenta_pagar_id} cambió su saldo. Recargá cuentas y generá una nueva orden.`,
        });

        await tx.pagos_proveedor.create({
          data: {
            empresa_id: empresaId,
            cuenta_pagar_id: detalle.cuenta_pagar_id,
            proveedor_id: orden.proveedor_id,
            medio_pago_id: detalle.medio_pago_id,
            moneda_id: orden.moneda_id || cuenta.moneda_id || null,
            usuario_id: userId || orden.usuario_id || null,
            sesion_caja_id: orden.sesion_caja_id || null,
            numero_recibo: orden.numero_orden_pago,
            fecha_pago: orden.fecha_pago || new Date(),
            monto: montoDetalle,
            monto_moneda: this.toNumber(detalle.monto_moneda ?? montoDetalle),
            cotizacion: this.toNumber(detalle.cotizacion ?? orden.cotizacion ?? 1) || 1,
            referencia: detalle.referencia || orden.referencia || null,
            observaciones: detalle.observaciones || orden.observaciones || null,
            estado: 'aplicado',
            anulado: false,
            activo: true,
          },
        });

        const montoPagadoActual = this.toNumber(cuenta.monto_pagado);
        const nuevoSaldo = this.round(Math.max(saldoPendiente - montoDetalle, 0), 2);
        const nuevoMontoPagado = this.round(montoPagadoActual + montoDetalle, 2);
        const nuevoEstado = nuevoSaldo <= 0 ? 'pagada' : 'parcial';

        await tx.cuentas_pagar.update({
          where: { id: detalle.cuenta_pagar_id },
          data: {
            monto_pagado: nuevoMontoPagado,
            saldo_pendiente: nuevoSaldo,
            estado: nuevoEstado,
            updated_at: new Date(),
          },
        });

        if (cuenta.compra_id) comprasToRecalculate.add(cuenta.compra_id);
        const cuentaOrigen = this.resolveCuentaOrigen(cuenta);
        if (cuentaOrigen.origen_tipo === 'gasto' && cuentaOrigen.origen_id) {
          gastosToRecalculate.add(cuentaOrigen.origen_id);
        }
      }

      await tx.orden_pago_proveedor_cab.update({
        where: { id: orden.id },
        data: {
          estado: 'PAGADO',
          usuario_id: userId || orden.usuario_id,
          updated_at: new Date(),
        },
      });

      for (const compraId of comprasToRecalculate) {
        await this.recalculateCompraEstado(tx, compraId);
        await this.recalculateAutoOrderEstado(tx, compraId);
      }
      for (const gastoId of gastosToRecalculate) {
        await this.recalculateGastoEstado(tx, gastoId);
      }

      return {
        message: 'Pago ejecutado exitosamente',
        id: orden.id,
      };
    });

    let tesoreriaSync: TesoreriaSyncResult | null = null;
    if (result?.id) {
      tesoreriaSync = await this.syncTesoreriaOrdenPago(result.id, empresaId, userId);
    }

    // Hook contable — fire and forget
    if (result?.id) {
      this.contabilidadIntegracion.integrarOrdenPago(result.id).catch((err) => {
        this.logger.error(`Error contabilizando orden pago ${result.id}: ${err.message}`);
      });
    }

    if (tesoreriaSync?.warning) {
      return {
        ...result,
        tesoreria_warning: tesoreriaSync.warning,
      };
    }

    return result;
  }

  async generatePdf(id: string, empresaId: string) {
    const orden = await this.prisma.orden_pago_proveedor_cab.findFirst({
      where: { id, empresa_id: empresaId, activo: true },
      include: {
        empresas: {
          select: {
            razon_social: true,
            nombre_fantasia: true,
            ruc: true,
            dv: true,
            celular: true,
            email: true,
            logo: true,
          },
        },
        empresas_sucursales: {
          select: {
            descripcion: true,
            punto_establecimiento: true,
            direccion: true,
            telefono: true,
            email: true,
            logo: true,
            nombre_fantasia: true,
          },
        },
        proveedores: {
          include: {
            personas: {
              select: {
                razon_social: true,
                ruc: true,
                dv: true,
                direccion: true,
                telefono: true,
                email: true,
              },
            },
          },
        },
        moneda: {
          select: {
            codigo: true,
            descripcion: true,
            simbolo: true,
          },
        },
        orden_pago_proveedor_det: {
          include: {
            medio_pago: {
              select: {
                descripcion: true,
              },
            },
            cuentas_pagar: {
              select: {
                id: true,
                compra_id: true,
                origen_tipo: true,
                origen_id: true,
                numero_cuota: true,
                fecha_vencimiento: true,
                monto_original: true,
                compra_cab: {
                  select: {
                    numero_factura: true,
                    establecimiento: true,
                    punto_expedicion: true,
                    fecha_emision: true,
                  },
                },
              },
            },
          },
          orderBy: { created_at: 'asc' },
        },
      },
    });

    if (!orden) {
      throw new NotFoundException('Orden de pago no encontrada');
    }

    const [establecimientoRaw, puntoExpRaw] = String(orden.numero_orden_pago || '').split('-');
    const establecimiento = (establecimientoRaw || orden.empresas_sucursales?.punto_establecimiento || '001')
      .padStart(3, '0')
      .slice(0, 3);
    const puntoExpedicion = (puntoExpRaw || '001').padStart(3, '0').slice(0, 3);

    const empresaRuc = orden.empresas?.ruc || '';
    const empresaDv = orden.empresas?.dv || '';

    const proveedorPersona = orden.proveedores?.personas;
    const proveedorRuc = proveedorPersona?.ruc || '';
    const proveedorDocumento = proveedorRuc ? `${proveedorRuc}-${proveedorPersona?.dv || '0'}` : '';

    const cuentasOrden = orden.orden_pago_proveedor_det
      .map((det) => det.cuentas_pagar)
      .filter(Boolean);
    const cuentasEnriched = await this.enrichCuentasPagarOrigen(cuentasOrden, empresaId);
    const cuentasById = new Map(cuentasEnriched.map((cuenta) => [cuenta.id, cuenta]));

    const detalles = orden.orden_pago_proveedor_det.map((det, index) => {
      const cuentaDetalle = det.cuentas_pagar
        ? cuentasById.get(det.cuentas_pagar.id) || det.cuentas_pagar
        : null;
      const comprobanteCompleto = cuentaDetalle?.documento_ref || 'S/N';
      const cuotaNumero = cuentaDetalle?.numero_cuota || 1;
      const medioDesc = det.medio_pago?.descripcion || 'Pago';
      const monto = this.round(this.toNumber(det.monto), 2);
      const referencia = det.referencia ? ` - Ref: ${det.referencia}` : '';

      return {
        codigo: comprobanteCompleto,
        descripcion: `Documento ${comprobanteCompleto} cuota ${cuotaNumero} (${medioDesc})${referencia}`,
        unidad: 'UNI',
        cantidad: 1,
        precio_unitario: monto,
        descuento: 0,
        tasa_iva: 0,
        total: monto,
        orden: index + 1,
      };
    });

    if (!detalles.length) {
      throw new BadRequestException('La orden de pago no tiene detalles para generar el PDF');
    }

    const totalOperacion = this.round(detalles.reduce((acc, item) => acc + this.toNumber(item.total), 0), 2);
    const formasPago = orden.orden_pago_proveedor_det.map((det) => ({
      tipo: det.medio_pago?.descripcion || 'Pago',
      referencia: det.referencia || '',
      cuenta: det.referencia || '',
      fecha: orden.fecha_pago ? new Date(orden.fecha_pago).toISOString() : null,
      monto: this.round(this.toNumber(det.monto), 2),
      observaciones: det.observaciones || '',
    }));
    const infoLines = [
      orden.referencia ? `Referencia: ${orden.referencia}` : '',
      orden.observaciones ? `Obs: ${orden.observaciones}` : '',
    ].filter(Boolean);

    const estadoRaw = String(orden.estado || '').toUpperCase();
    const esComprobantePago = !orden.anulado && (estadoRaw === 'PAGADO' || estadoRaw === 'APLICADO');

    const payload = {
      tipo: 'view',
      documento_tipo: esComprobantePago ? 'comprobante_pago' : 'orden_pago',
      empresa: {
        razon_social: orden.empresas?.razon_social || '',
        ruc: empresaRuc,
        dv: empresaDv,
        direccion: orden.empresas_sucursales?.direccion || '',
        telefono: orden.empresas_sucursales?.telefono || orden.empresas?.celular || '',
        email: orden.empresas_sucursales?.email || orden.empresas?.email || '',
        // Logo y nombre comercial de la sucursal del documento (si no tiene, los de la empresa).
        nombre_fantasia: nombreFantasiaEmisor(orden.empresas_sucursales, orden.empresas),
        logo_url: logoEmisor(orden.empresas_sucursales, orden.empresas) || undefined,
      },
      cliente: {
        razon_social: proveedorPersona?.razon_social || '',
        ruc: proveedorRuc,
        documento: proveedorDocumento,
        telefono: proveedorPersona?.telefono || '',
        direccion: proveedorPersona?.direccion || '',
        email: proveedorPersona?.email || '',
      },
      numero_orden: orden.numero_orden_pago,
      numero: orden.numero_orden_pago,
      establecimiento,
      punto_expedicion: puntoExpedicion,
      fecha_emision: orden.fecha_pago ? new Date(orden.fecha_pago).toISOString() : new Date().toISOString(),
      estado: orden.anulado ? 'anulado' : orden.estado || 'BORRADOR',
      moneda: String(orden.moneda?.codigo || 'PYG').toUpperCase(),
      cotizacion: this.toNumber(orden.cotizacion || 1),
      detalles,
      formas_pago: formasPago,
      totales: {
        total_operacion: totalOperacion,
        total_bruto: totalOperacion,
        subtotal_exenta: totalOperacion,
        subtotal_iva5: 0,
        subtotal_iva10: 0,
        total_iva5: 0,
        total_iva10: 0,
        total_iva: 0,
        descuento_total: 0,
        total_guaranies:
          String(orden.moneda?.codigo || '').toUpperCase() === 'PYG'
            ? totalOperacion
            : this.round(totalOperacion * this.toNumber(orden.cotizacion || 1), 2),
      },
      info_adicional: infoLines.join(' | '),
    };

    const url = `${envs.apiGeneradorPDF}/api/orden-pago/generate-pdf`;
    let pdfBuffer: Buffer;
    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 de orden de pago: ${errorBody}`);
      }

      const arrayBuffer = await response.arrayBuffer();
      pdfBuffer = Buffer.from(arrayBuffer);
    } 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 de la orden de pago: ${message}`);
    }

    const filename = `orden-pago-${orden.numero_orden_pago}.pdf`;

    return { pdfBuffer, filename };
  }

  // ─── Estado de cuenta del proveedor (CxP) ──────────────────────────────────
  private fmtFechaEC(d?: Date | null): string {
    if (!d) return '-';
    const dt = new Date(d);
    const p = (n: number) => String(n).padStart(2, '0');
    return `${p(dt.getUTCDate())}/${p(dt.getUTCMonth() + 1)}/${dt.getUTCFullYear()}`;
  }

  /**
   * Estado de cuenta de un proveedor: agrupa las cuentas_pagar (cada fila es una
   * cuota) por documento origen (compra/gasto), con saldos y clasificación
   * vencida/al-día. Espeja getEstadoCuentaCliente de CxC.
   */
  async getEstadoCuentaProveedor(empresaId: string, proveedorId: string, usuarioId?: string) {
    // Mismo alcance por sucursal (y rubro del selector) que el resto de Compras.
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresaId);
    const hoy = new Date();
    hoy.setHours(0, 0, 0, 0);

    const proveedor = await this.prisma.proveedores.findFirst({
      where: { id: proveedorId, empresa_id: empresaId },
      include: { personas: { select: { razon_social: true, ruc: true, dv: true, telefono: true, direccion: true } } },
    });
    if (!proveedor) throw new NotFoundException('Proveedor no encontrado');

    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 cuentas = await this.prisma.cuentas_pagar.findMany({
      where: {
        empresa_id: empresaId,
        proveedor_id: proveedorId,
        activo: true,
        estado: { notIn: ['pagada', 'anulada'] },
        saldo_pendiente: { gt: 0 },
      },
      include: {
        compra_cab: {
          select: { establecimiento: true, punto_expedicion: true, numero_factura: true, fecha_emision: true, sucursal_id: true },
        },
        moneda: { select: { codigo: true } },
      },
      orderBy: { fecha_vencimiento: 'asc' },
    });

    // Números de gasto para las cuentas de origen 'gasto' (sin relación directa).
    const gastoIds = cuentas.filter((c) => c.origen_tipo === 'gasto' && c.origen_id).map((c) => c.origen_id as string);
    const gastosMap = new Map<string, { numero_factura: string | null; fecha_emision: Date | null; sucursal_id: string | null }>();
    if (gastoIds.length) {
      const gastos = await this.prisma.gasto_cab.findMany({
        where: { id: { in: gastoIds } },
        select: { id: true, numero_factura: true, fecha_emision: true, sucursal_id: true },
      });
      for (const g of gastos) {
        gastosMap.set(g.id, { numero_factura: g.numero_factura, fecha_emision: g.fecha_emision, sucursal_id: g.sucursal_id });
      }
    }

    // Sucursal de cada deuda: la de la compra o la del gasto que la originó.
    const sucursalDeCuenta = (c: (typeof cuentas)[number]) =>
      c.compra_cab?.sucursal_id ?? (c.origen_tipo === 'gasto' && c.origen_id ? gastosMap.get(c.origen_id)?.sucursal_id : null) ?? null;
    const cuentasVisibles = alcance
      ? cuentas.filter((c) => {
          const suc = sucursalDeCuenta(c);
          return !!suc && alcance.sucursalIds.includes(suc);
        })
      : cuentas;

    type Doc = {
      key: string;
      numero: string;
      tipo_doc: string;
      fecha_emision: string;
      total_factura: number;
      moneda: string;
      cuotas: Array<{ nro_cuota: number; fecha_vencimiento: string; monto_cuota: number; saldo_pendiente: number; dias_vencido: number; estado: string }>;
    };
    const docs = new Map<string, Doc>();
    let totalPendiente = 0;
    let totalVencido = 0;
    let totalAlDia = 0;

    for (const c of cuentasVisibles) {
      const saldo = Number(c.saldo_pendiente) || 0;
      const venc = c.fecha_vencimiento ? new Date(c.fecha_vencimiento) : null;
      const dias = venc ? Math.floor((hoy.getTime() - venc.getTime()) / 86400000) : 0;
      const vencida = dias > 0;
      totalPendiente += saldo;
      if (vencida) totalVencido += saldo;
      else totalAlDia += saldo;

      const esGasto = c.origen_tipo === 'gasto';
      const key = c.compra_id || c.origen_id || c.id;
      if (!docs.has(key)) {
        let numero = '-';
        let fechaEmision: Date | null = null;
        if (c.compra_cab) {
          const est = c.compra_cab.establecimiento;
          const pex = c.compra_cab.punto_expedicion;
          const nro = c.compra_cab.numero_factura;
          numero = [est, pex, nro].filter(Boolean).join('-') || nro || '-';
          fechaEmision = c.compra_cab.fecha_emision;
        } else if (esGasto && c.origen_id && gastosMap.has(c.origen_id)) {
          const g = gastosMap.get(c.origen_id)!;
          numero = g.numero_factura || '-';
          fechaEmision = g.fecha_emision;
        }
        docs.set(key, {
          key,
          numero,
          tipo_doc: esGasto ? 'Gasto' : c.origen_tipo === 'importacion' ? 'Importación' : 'Compra',
          fecha_emision: this.fmtFechaEC(fechaEmision ?? c.fecha_emision),
          total_factura: 0,
          moneda: c.moneda?.codigo || 'PYG',
          cuotas: [],
        });
      }
      const doc = docs.get(key)!;
      doc.total_factura += Number(c.monto_original) || saldo;
      doc.cuotas.push({
        nro_cuota: c.numero_cuota ?? 1,
        fecha_vencimiento: this.fmtFechaEC(venc),
        monto_cuota: Number(c.monto_original) || saldo,
        saldo_pendiente: saldo,
        dias_vencido: vencida ? dias : 0,
        estado: vencida ? 'Vencida' : 'Pendiente',
      });
    }

    // Encabezado: si todas las deudas son de una misma sucursal, logo y nombre
    // comercial de esa sucursal; si mezcla, los de la empresa (como la factura).
    const sucursalEmisora = await sucursalUnicaDeDocumentos(this.prisma, empresaId, {
      sucursalIds: cuentasVisibles.map(sucursalDeCuenta),
    });

    return {
      empresa: {
        razon_social: empresa?.razon_social,
        nombre_fantasia: nombreFantasiaEmisor(sucursalEmisora, empresa),
        ruc: empresa?.ruc ? `${empresa.ruc}-${empresa.dv}` : null,
        celular: empresa?.celular,
        email: empresa?.email,
        logo_url: logoEmisor(sucursalEmisora, empresa),
      },
      proveedor: {
        razon_social: proveedor.personas?.razon_social,
        ruc: proveedor.personas?.ruc ? `${proveedor.personas.ruc}-${proveedor.personas.dv ?? 0}` : null,
        telefono: proveedor.personas?.telefono,
        direccion: proveedor.personas?.direccion,
        cod_cliente: null,
      },
      fecha_generacion: this.fmtFechaEC(new Date()),
      resumen: {
        total_pendiente: totalPendiente,
        total_vencido: totalVencido,
        total_al_dia: totalAlDia,
        facturas_pendientes: docs.size,
        cuotas_pendientes: cuentasVisibles.length,
      },
      facturas: Array.from(docs.values()),
    };
  }

  async generateEstadoCuentaProveedorPdf(
    empresaId: string,
    proveedorId: string,
    options: { tipo_retorno?: string; incluir_todas?: boolean; usuarioId?: string } = {},
  ): Promise<{ pdfBuffer?: Buffer; base64?: string; contentType: string }> {
    const { tipo_retorno = 'view', incluir_todas = false, usuarioId } = options;
    const estado = await this.getEstadoCuentaProveedor(empresaId, proveedorId, usuarioId);
    const payload = { tipo: tipo_retorno, incluir_todas, ...estado };
    const url = `${envs.apiGeneradorPDF}/api/estado-cuenta-proveedor/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}`);
      }
      if (tipo_retorno === 'base64') {
        const json = (await response.json()) as { status: string; pdf_base64?: string; message?: string };
        if (json.status !== 'success') throw new BadRequestException(json.message || 'Error al generar PDF');
        return { base64: json.pdf_base64, contentType: 'application/json' };
      }
      const arrayBuffer = await response.arrayBuffer();
      return { pdfBuffer: Buffer.from(arrayBuffer), contentType: 'application/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 findOne(id: string, empresaId: string, tx?: Tx) {
    const db = tx ?? this.prisma;
    const orden = await db.orden_pago_proveedor_cab.findFirst({
      where: { id, empresa_id: empresaId, activo: true },
      include: {
        proveedores: {
          include: {
            personas: {
              select: {
                razon_social: true,
                ruc: true,
                dv: true,
                telefono: true,
                email: true,
              },
            },
          },
        },
        empresas_sucursales: {
          select: { id: true, descripcion: true, punto_establecimiento: true },
        },
        compra_cab: {
          select: {
            id: true,
            establecimiento: true,
            punto_expedicion: true,
            numero_factura: true,
            fecha_emision: true,
            estado: true,
          },
        },
        moneda: {
          select: { id: true, codigo: true, descripcion: true, simbolo: true },
        },
        orden_pago_proveedor_det: {
          include: {
            medio_pago: {
              select: { id: true, descripcion: true },
            },
            cuentas_pagar: {
              select: {
                id: true,
                compra_id: true,
                origen_tipo: true,
                origen_id: true,
                numero_cuota: true,
                fecha_vencimiento: true,
                compra_cab: {
                  select: {
                    id: true,
                    establecimiento: true,
                    punto_expedicion: true,
                    numero_factura: true,
                    fecha_emision: true,
                  },
                },
              },
            },
          },
          orderBy: { created_at: 'asc' },
        },
      },
    });

    if (!orden) {
      throw new NotFoundException('Orden de pago no encontrada');
    }

    const cuentas = orden.orden_pago_proveedor_det
      .map((det) => det.cuentas_pagar)
      .filter(Boolean);
    const cuentasEnriched = await this.enrichCuentasPagarOrigen(cuentas, empresaId);
    const cuentasById = new Map(cuentasEnriched.map((cuenta) => [cuenta.id, cuenta]));

    const ordenEnriched = {
      ...orden,
      orden_pago_proveedor_det: orden.orden_pago_proveedor_det.map((det) => ({
        ...det,
        cuentas_pagar: det.cuentas_pagar
          ? cuentasById.get(det.cuentas_pagar.id) || det.cuentas_pagar
          : det.cuentas_pagar,
      })),
    };

    const [result] = await this.enrichOrdenesPagoOrigen([ordenEnriched], empresaId);
    return result;
  }

  private getDefaultConfigCompras(): ConfigComprasAP {
    return {
      habilitar_requisicion_compra: false,
      modo_flujo_compra: 'directo',
      afectar_stock_en_compra_directa: true,
      habilitar_orden_compra: true,
      habilitar_recepcion_compra: true,
      habilitar_matching_3_vias: true,
      habilitar_orden_pago: true,
      requiere_aprobacion: false,
      permite_pago_sin_aprobacion: true,
      generar_cxp_automatico: true,
      permitir_edicion_cxp: true,
      permitir_pago_parcial: false,
      usar_workflow: false,
      workflow_obligatorio: false,
      aplicar_pago_al_guardar: true,
      incluir_contado_en_orden_pago: false,
      permitir_editar_gasto_pagado: false,
    };
  }

  private async getOrCreateConfigCompras(tx: Tx, empresaId: string): Promise<ConfigComprasAP> {
    const defaults = this.getDefaultConfigCompras();
    let config = await tx.config_compras.findUnique({
      where: { empresa_id: empresaId },
      select: {
        habilitar_requisicion_compra: true,
        modo_flujo_compra: true,
        afectar_stock_en_compra_directa: true,
        habilitar_orden_compra: true,
        habilitar_recepcion_compra: true,
        habilitar_matching_3_vias: true,
        habilitar_orden_pago: true,
        requiere_aprobacion: true,
        permite_pago_sin_aprobacion: true,
        generar_cxp_automatico: true,
        permitir_edicion_cxp: true,
        permitir_pago_parcial: true,
        usar_workflow: true,
        workflow_obligatorio: true,
        aplicar_pago_al_guardar: true,
        incluir_contado_en_orden_pago: true,
        permitir_editar_gasto_pagado: true,
      },
    });

    if (!config) {
      config = await tx.config_compras.create({
        data: {
          empresa_id: empresaId,
          ...defaults,
        },
        select: {
          habilitar_requisicion_compra: true,
          modo_flujo_compra: true,
          afectar_stock_en_compra_directa: true,
          habilitar_orden_compra: true,
          habilitar_recepcion_compra: true,
          habilitar_matching_3_vias: true,
          habilitar_orden_pago: true,
          requiere_aprobacion: true,
          permite_pago_sin_aprobacion: true,
          generar_cxp_automatico: true,
          permitir_edicion_cxp: true,
          permitir_pago_parcial: true,
          usar_workflow: true,
          workflow_obligatorio: true,
          aplicar_pago_al_guardar: true,
          incluir_contado_en_orden_pago: true,
          permitir_editar_gasto_pagado: true,
        },
      });
    }

    return this.normalizeConfigCompras({
      habilitar_requisicion_compra: Boolean(config.habilitar_requisicion_compra),
      modo_flujo_compra: this.normalizeModoFlujoCompra(config.modo_flujo_compra),
      afectar_stock_en_compra_directa: Boolean(config.afectar_stock_en_compra_directa ?? true),
      habilitar_orden_compra: Boolean(config.habilitar_orden_compra ?? true),
      habilitar_recepcion_compra: Boolean(config.habilitar_recepcion_compra ?? true),
      habilitar_matching_3_vias: Boolean(config.habilitar_matching_3_vias ?? true),
      habilitar_orden_pago: Boolean(config.habilitar_orden_pago ?? true),
      requiere_aprobacion: Boolean(config.requiere_aprobacion),
      permite_pago_sin_aprobacion: Boolean(config.permite_pago_sin_aprobacion ?? true),
      generar_cxp_automatico: Boolean(config.generar_cxp_automatico ?? true),
      permitir_edicion_cxp: Boolean(config.permitir_edicion_cxp ?? true),
      permitir_pago_parcial: Boolean(config.permitir_pago_parcial),
      usar_workflow: Boolean(config.usar_workflow),
      workflow_obligatorio: Boolean(config.workflow_obligatorio),
      aplicar_pago_al_guardar: Boolean(config.aplicar_pago_al_guardar ?? true),
      incluir_contado_en_orden_pago: Boolean(config.incluir_contado_en_orden_pago ?? false),
      permitir_editar_gasto_pagado: Boolean(config.permitir_editar_gasto_pagado ?? false),
    });
  }

  private normalizeModoFlujoCompra(value: string | null | undefined): ModoFlujoCompra {
    if (value === 'requisicion_opcional' || value === 'requisicion_obligatoria') {
      return value;
    }
    return 'directo';
  }

  private normalizeConfigCompras(raw: ConfigComprasAP): ConfigComprasAP {
    const next: ConfigComprasAP = {
      ...raw,
      modo_flujo_compra: this.normalizeModoFlujoCompra(raw.modo_flujo_compra),
    };

    if (!next.habilitar_requisicion_compra) {
      next.modo_flujo_compra = 'directo';
    }
    if (next.modo_flujo_compra === 'requisicion_obligatoria') {
      next.habilitar_requisicion_compra = true;
      next.habilitar_orden_compra = true;
    }
    if (!next.habilitar_recepcion_compra || !next.habilitar_orden_compra) {
      next.habilitar_matching_3_vias = false;
    }
    if (next.workflow_obligatorio) {
      next.usar_workflow = true;
      next.requiere_aprobacion = true;
    }
    if (next.usar_workflow) {
      next.requiere_aprobacion = true;
    }
    if (!next.afectar_stock_en_compra_directa && !next.habilitar_recepcion_compra) {
      throw new BadRequestException(
        'Configuración inválida: si no afecta stock en compra directa, la recepción de compra debe estar habilitada',
      );
    }

    return next;
  }

  private requiereAprobacionOrdenPago(config: ConfigComprasAP): boolean {
    return Boolean(
      config.workflow_obligatorio ||
      ((config.requiere_aprobacion || config.usar_workflow) && !config.permite_pago_sin_aprobacion),
    );
  }

  private assertMontoPagoValido(params: {
    cuentaId: string;
    montoDetalle: number;
    saldoPendiente: number;
    permitirPagoParcial: boolean;
    changedBalanceMessage?: string;
  }) {
    const { cuentaId, montoDetalle, saldoPendiente, permitirPagoParcial, changedBalanceMessage } = params;
    if (montoDetalle <= 0) {
      throw new BadRequestException(`Pago inválido para cuenta ${cuentaId}: el monto debe ser mayor a cero`);
    }

    if (permitirPagoParcial) {
      if (montoDetalle - saldoPendiente > 0.01) {
        throw new BadRequestException(
          `Pago inválido para cuenta ${cuentaId}: no puede superar el saldo pendiente (${saldoPendiente})`,
        );
      }
      return;
    }

    if (!this.equalsAmount(montoDetalle, saldoPendiente, 0.01)) {
      throw new BadRequestException(
        changedBalanceMessage ||
          `Pago inválido para cuenta ${cuentaId}: debe ser exacto al saldo pendiente (${saldoPendiente})`,
      );
    }
  }

  private mergeObservacionWorkflow(current: string | null | undefined, note: string): string {
    const base = String(current || '').trim();
    const normalizedNote = String(note || '').trim();
    if (!normalizedNote) return base;
    return base ? `${base} | ${normalizedNote}` : normalizedNote;
  }

  private async validateSesionCaja(
    tx: Tx,
    params: {
      empresaId: string;
      sucursalId: string;
      sesionCajaId: string | null;
      requiereCaja: boolean;
    },
  ) {
    if (params.requiereCaja && !params.sesionCajaId) {
      throw new BadRequestException(
        'La sucursal exige sesión de caja abierta para registrar pagos a proveedores',
      );
    }

    if (!params.sesionCajaId) return;

    const sesion = await tx.sesiones_caja.findFirst({
      where: {
        id: params.sesionCajaId,
        empresa_id: params.empresaId,
        sucursal_id: params.sucursalId,
        estado: 'ABIERTA',
      },
      select: { id: true },
    });

    if (!sesion) {
      throw new BadRequestException('La sesión de caja indicada no está abierta para la sucursal seleccionada');
    }
  }

  private async getNextNumeroOrdenPago(tx: Tx, empresaId: string, sucursalId: string): Promise<NumeracionOrdenPago> {
    const rows = await tx.$queryRaw<
      Array<{
        id: string;
        numero_actual: number;
        numero_final: number | null;
        punto_expedicion: string | null;
        punto_establecimiento: string | null;
      }>
    >`
      SELECT
        nd.id,
        nd.numero_actual,
        nd.numero_final,
        epe.punto_expedicion,
        es.punto_establecimiento
      FROM numeraciones_documento nd
      INNER JOIN tipo_documento td ON td.id = nd.tipo_documento_id
      INNER JOIN empresas_puntos_expedicion epe ON epe.id = nd.punto_expedicion_id
      INNER JOIN empresas_sucursales es ON es.id = epe.sucursal_id
      WHERE td.codigo = 400
        AND nd.empresa_id = ${empresaId}::uuid
        AND epe.sucursal_id = ${sucursalId}::uuid
        AND nd.active = true
      ORDER BY nd.created_at ASC
      LIMIT 1
      FOR UPDATE
    `;

    if (!rows.length) {
      throw new NotFoundException(
        'No se encontró una numeración activa para Orden de pago proveedor (código 400) en la sucursal seleccionada',
      );
    }

    const numeracion = rows[0];
    const numeroActual = Number(numeracion.numero_actual || 0);
    const numeroFinal = numeracion.numero_final === null ? null : Number(numeracion.numero_final);

    if (numeroFinal !== null && numeroActual > numeroFinal) {
      throw new BadRequestException('Se alcanzó el número final de la numeración de orden de pago');
    }

    const dest = (numeracion.punto_establecimiento || '001').padStart(3, '0').slice(0, 3);
    const pexp = (numeracion.punto_expedicion || '001').padStart(3, '0').slice(0, 3);
    const sec = String(numeroActual).padStart(7, '0');
    const numeroOrden = `${dest}-${pexp}-${sec}`;

    await tx.numeraciones_documento.update({
      where: { id: numeracion.id },
      data: {
        numero_actual: numeroActual + 1,
      },
    });

    return {
      numeracionId: numeracion.id,
      numeroOrden,
    };
  }

  private async recalculateCompraEstado(tx: Tx, compraId: string) {
    const cuentas = await tx.cuentas_pagar.findMany({
      where: {
        compra_id: compraId,
        activo: true,
        estado: { not: 'anulada' },
      },
      select: {
        monto_original: true,
        saldo_pendiente: true,
      },
    });

    if (!cuentas.length) return;

    const totalOriginal = cuentas.reduce((acc, c) => acc + this.toNumber(c.monto_original), 0);
    const totalSaldo = cuentas.reduce((acc, c) => acc + this.toNumber(c.saldo_pendiente), 0);

    let estado = 'pendiente';
    if (totalSaldo <= 0.0001) estado = 'pagada';
    else if (totalSaldo < totalOriginal) estado = 'parcial';

    await tx.compra_cab.update({
      where: { id: compraId },
      data: {
        estado,
        updated_at: new Date(),
      },
    });
  }

  private async recalculateAutoOrderEstado(tx: Tx, compraId: string) {
    const cuentas = await tx.cuentas_pagar.findMany({
      where: {
        compra_id: compraId,
        activo: true,
        estado: { not: 'anulada' },
      },
      select: {
        saldo_pendiente: true,
      },
    });

    if (!cuentas.length) return;

    const totalSaldo = cuentas.reduce((acc, c) => acc + this.toNumber(c.saldo_pendiente), 0);

    const nuevoEstado = totalSaldo <= 0.0001 ? 'PAGADO' : 'BORRADOR';

    await tx.orden_pago_proveedor_cab.updateMany({
      where: {
        compra_id: compraId,
        origen: 'compra_auto',
        activo: true,
        anulado: false,
      },
      data: {
        estado: nuevoEstado,
        updated_at: new Date(),
      },
    });
  }

  private async recalculateGastoEstado(tx: Tx, gastoId: string) {
    const cuentas = await tx.cuentas_pagar.findMany({
      where: {
        origen_tipo: 'gasto',
        origen_id: gastoId,
        activo: true,
        estado: { not: 'anulada' },
      },
      select: {
        monto_original: true,
        saldo_pendiente: true,
      },
    });

    if (!cuentas.length) return;

    const totalOriginal = cuentas.reduce((acc, c) => acc + this.toNumber(c.monto_original), 0);
    const totalSaldo = cuentas.reduce((acc, c) => acc + this.toNumber(c.saldo_pendiente), 0);

    let estado = 'registrado';
    if (totalSaldo <= 0.0001) estado = 'pagada';
    else if (totalSaldo < totalOriginal) estado = 'parcial';

    await tx.gasto_cab.updateMany({
      where: {
        id: gastoId,
        anulado: false,
      },
      data: {
        estado,
        updated_at: new Date(),
      },
    });
  }

  // Público: reutilizado por ComprasService y MarangatuSyncService para sus
  // órdenes de pago auto-generadas (contado sin diferir a OP), además de la
  // ejecución manual de OP de este mismo servicio.
  async syncTesoreriaOrdenPago(
    ordenId: string,
    empresaId: string,
    userId?: string,
  ): Promise<TesoreriaSyncResult> {
    if (!this.hasTesoreriaBridgeSupport(this.prisma as any)) {
      return {
        warning:
          'La orden quedó pagada, pero no se registró en Tesorería porque el puente OP->Tesorería no está disponible en este entorno.',
      };
    }

    try {
      await this.prisma.$transaction(async (tx) => {
        const orden = await tx.orden_pago_proveedor_cab.findFirst({
          where: { id: ordenId, empresa_id: empresaId, activo: true, anulado: false },
          select: {
            id: true,
            numero_orden_pago: true,
            fecha_pago: true,
            monto_total: true,
            referencia: true,
            estado: true,
            orden_pago_proveedor_det: {
              select: {
                medio_pago_id: true,
                monto: true,
                medio_pago: { select: { descripcion: true, codigo: true } },
                numero_cheque: true,
                banco_emisor: true,
                fecha_emision_cheque: true,
                fecha_vencimiento_cheque: true,
                titular_cheque: true,
                cuenta_bancaria_id: true,
                numero_operacion: true,
              },
            },
          },
        });

        if (!orden) return;
        const estado = String(orden.estado || '').toUpperCase();
        if (estado !== 'PAGADO' && estado !== 'APLICADO') return;

        // Idempotencia: no re-sincronizar si ya existe un movimiento de caja O un cheque
        // emitido para esta orden. Un pago con cheque (Modelo obligación pendiente) no genera
        // movimiento, así que hay que chequear ambas tablas para evitar duplicados.
        const movimientoExistente = await tx.tes_movimientos.findFirst({
          where: {
            empresa_id: empresaId,
            origen_tipo: TES_ORIGEN_OP_PROVEEDOR,
            origen_id: orden.id,
            estado: { not: 'ANULADO' },
          },
          select: { id: true },
        });
        const chequeExistente = await tx.tes_cheques.findFirst({
          where: {
            empresa_id: empresaId,
            origen_tipo: TES_ORIGEN_OP_PROVEEDOR,
            origen_id: orden.id,
            estado: { not: 'ANULADO' },
          },
          select: { id: true },
        });
        if (movimientoExistente || chequeExistente) return;

        const categoriaId = await this.getCategoriaTesoreriaPagoProveedor(tx, empresaId);
        const fechaPago = orden.fecha_pago ? new Date(orden.fecha_pago) : new Date();
        const baseDesc = `Pago proveedor OP ${orden.numero_orden_pago || orden.id.slice(0, 8)}`;
        const referencia = orden.referencia || orden.numero_orden_pago || null;

        type GrupoSync = {
          monto: number;
          medioPagoId: string | null;
          medioDescripcion: string;
          cuentaBancariaId: string | null;
          esCheque: boolean;
          numeroCheque: string | null;
          bancoEmisor: string | null;
          fechaEmisionCheque: Date | null;
          fechaVencimientoCheque: Date | null;
          titularCheque: string | null;
          numeroOperacion: string | null;
        };

        // Detecta si un medio de pago es cheque por código o descripción
        const isMedioChequeSync = (det: { medio_pago?: { codigo?: number | null; descripcion?: string | null } | null }) => {
          const codigo = Number(det.medio_pago?.codigo ?? 0);
          const desc = (det.medio_pago?.descripcion ?? '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
          return codigo === 2 || desc.includes('cheque');
        };

        // Agrupar líneas por (medio_pago_id, cuenta_bancaria_id) para crear un movimiento por
        // combinación. Excepción: los CHEQUES nunca se agrupan — cada cheque es un instrumento
        // individual (con su propio número, banco, titular y fechas) y genera su propio registro.
        const grupos = new Map<string, GrupoSync>();
        let chequeIdx = 0;
        for (const det of orden.orden_pago_proveedor_det ?? []) {
          const m = this.round(this.toNumber(det.monto), 2);
          if (m <= 0) continue;
          const esCheque = isMedioChequeSync(det);
          const key = esCheque
            ? `cheque::${chequeIdx++}::${det.numero_cheque ?? ''}`
            : `${det.medio_pago_id ?? ''}::${det.cuenta_bancaria_id ?? ''}`;
          const existing = grupos.get(key);
          if (existing) {
            existing.monto = this.round(existing.monto + m, 2);
          } else {
            grupos.set(key, {
              monto: m,
              medioPagoId: det.medio_pago_id ?? null,
              medioDescripcion: det.medio_pago?.descripcion || 'Pago',
              cuentaBancariaId: det.cuenta_bancaria_id ?? null,
              esCheque,
              numeroCheque: det.numero_cheque ?? null,
              bancoEmisor: det.banco_emisor ?? null,
              fechaEmisionCheque: det.fecha_emision_cheque ? new Date(det.fecha_emision_cheque) : null,
              fechaVencimientoCheque: det.fecha_vencimiento_cheque ? new Date(det.fecha_vencimiento_cheque) : null,
              titularCheque: det.titular_cheque ?? null,
              numeroOperacion: det.numero_operacion ?? null,
            });
          }
        }
        if (grupos.size === 0) {
          const total = this.round(this.toNumber(orden.monto_total), 2);
          if (total > 0) {
            grupos.set('fallback::', {
              monto: total,
              medioPagoId: null,
              medioDescripcion: 'Pago',
              cuentaBancariaId: null,
              esCheque: false,
              numeroCheque: null,
              bancoEmisor: null,
              fechaEmisionCheque: null,
              fechaVencimientoCheque: null,
              titularCheque: null,
              numeroOperacion: null,
            });
          }
        }
        if (grupos.size === 0) return;

        for (const [, g] of grupos) {
          if (g.monto <= 0) continue;

          // Resolver cuenta de tesorería: prioridad al seleccionado por el usuario, luego regla.
          let cuentaId: string | null = null;
          if (g.cuentaBancariaId) {
            const cuentaDirecta = await tx.tes_cuentas.findFirst({
              where: { id: g.cuentaBancariaId, empresa_id: empresaId, activo: true },
              select: { id: true },
            });
            cuentaId = cuentaDirecta?.id ?? null;
          }
          if (!cuentaId) {
            const cuentaInfo = await this.getCuentaTesoreriaPagoProveedor(tx, empresaId, g.medioPagoId);
            if (!cuentaInfo.id) {
              throw new Error(cuentaInfo.warning || 'No se encontró cuenta de Tesorería para pago a proveedor');
            }
            cuentaId = cuentaInfo.id;
          }

          const descripcion = `${baseDesc} — ${g.medioDescripcion}`;

          // Cheque emitido: es una OBLIGACIÓN PENDIENTE, no un egreso de caja inmediato.
          // El saldo del banco NO se descuenta acá; recién baja cuando se le da "Debitar"
          // al cheque en Tesorería (cuando el banco efectivamente lo cobra). Esto evita el
          // doble descuento y respeta el flujo de cheques diferidos.
          if (g.esCheque) {
            const numeroCheque = g.numeroCheque?.trim() || `OP-${orden.id.slice(0, 8)}`;
            // Diferido si la fecha de cobro/vencimiento es futura.
            const inicioHoy = new Date();
            inicioHoy.setHours(0, 0, 0, 0);
            const esDiferido =
              g.fechaVencimientoCheque instanceof Date && g.fechaVencimientoCheque > inicioHoy;
            await tx.tes_cheques.create({
              data: {
                empresa_id: empresaId,
                tipo: 'EMITIDO',
                estado: esDiferido ? 'DIFERIDO' : 'EN_CARTERA',
                numero_cheque: numeroCheque,
                banco_emisor: g.bancoEmisor || null,
                titular: g.titularCheque || null,
                cuenta_bancaria_origen: g.numeroOperacion?.trim() || null,
                monto: g.monto,
                moneda: 'PYG',
                fecha_emision: g.fechaEmisionCheque || fechaPago,
                fecha_vencimiento: g.fechaVencimientoCheque || null,
                cuenta_id: cuentaId,
                movimiento_id: null,
                origen_tipo: TES_ORIGEN_OP_PROVEEDOR,
                origen_id: orden.id,
                referencia: descripcion,
                registrado_por: userId || null,
              },
            });
            continue;
          }

          // Resto de medios (efectivo, transferencia, depósito, tarjeta): egreso inmediato.
          const movimiento = await tx.tes_movimientos.create({
            data: {
              empresa_id: empresaId,
              cuenta_id: cuentaId,
              tipo: 'EGRESO',
              estado: 'CONFIRMADO',
              fecha: fechaPago,
              monto: g.monto,
              descripcion,
              referencia: g.numeroOperacion || referencia,
              origen_tipo: TES_ORIGEN_OP_PROVEEDOR,
              origen_id: orden.id,
              created_by: userId || null,
              confirmado_por: userId || null,
              confirmado_en: new Date(),
            },
          });

          await tx.tes_movimiento_det.create({
            data: { movimiento_id: movimiento.id, categoria_id: categoriaId, monto: g.monto, descripcion },
          });

          await tx.tes_cuentas.update({
            where: { id: cuentaId },
            data: { saldo_actual: { decrement: g.monto } },
          });
        }
      });
    } catch (error) {
      if (this.isMissingTableError(error)) {
        this.logger.warn(`Puente OP->Tesorería omitido por tablas faltantes (${ordenId})`);
        return {
          warning:
            'La orden quedó pagada, pero no se registró en Tesorería porque faltan tablas de Tesorería en la base actual.',
        };
      }
      const message = (error as Error).message || 'Error desconocido';
      this.logger.error(`Error al sincronizar OP ${ordenId} con Tesorería: ${message}`);
      return {
        warning: `La orden quedó pagada, pero no se pudo registrar en Tesorería. Motivo: ${message}`,
      };
    }

    return {};
  }

  private async revertTesoreriaOrdenPago(ordenId: string, empresaId: string, userId?: string, motivo?: string) {
    if (!this.hasTesoreriaBridgeSupport(this.prisma as any)) {
      return;
    }

    try {
      await this.prisma.$transaction(async (tx) => {
        // 1) Anular los cheques emitidos pendientes generados por esta orden.
        //    Sólo se pueden anular cheques aún no acreditados (EN_CARTERA / DIFERIDO);
        //    si alguno ya fue DEBITADO, el banco ya cobró y debe revertirse desde Tesorería.
        const chequesPendientes = await tx.tes_cheques.findMany({
          where: {
            empresa_id: empresaId,
            origen_tipo: TES_ORIGEN_OP_PROVEEDOR,
            origen_id: ordenId,
            estado: { in: ['EN_CARTERA', 'DIFERIDO'] },
          },
          select: { id: true },
        });
        if (chequesPendientes.length) {
          await tx.tes_cheques.updateMany({
            where: { id: { in: chequesPendientes.map((c) => c.id) } },
            data: {
              estado: 'ANULADO',
              motivo: motivo || 'Anulación de orden de pago proveedor',
            },
          });
        }

        // 2) Revertir los movimientos de caja confirmados (medios distintos a cheque).
        const movimientos = await tx.tes_movimientos.findMany({
          where: {
            empresa_id: empresaId,
            origen_tipo: TES_ORIGEN_OP_PROVEEDOR,
            origen_id: ordenId,
            estado: 'CONFIRMADO',
          },
          select: { id: true, cuenta_id: true, monto: true },
        });

        if (!movimientos.length) return;

        await tx.tes_movimientos.updateMany({
          where: { id: { in: movimientos.map((mov) => mov.id) } },
          data: {
            estado: 'ANULADO',
            motivo_anulacion: motivo || 'Anulación de orden de pago proveedor',
            anulado_por: userId || null,
            anulado_en: new Date(),
          },
        });

        const montosByCuenta = new Map<string, number>();
        for (const mov of movimientos) {
          const prev = montosByCuenta.get(mov.cuenta_id) || 0;
          montosByCuenta.set(mov.cuenta_id, this.round(prev + this.toNumber(mov.monto), 2));
        }

        for (const [cuentaId, monto] of montosByCuenta.entries()) {
          if (monto <= 0) continue;
          await tx.tes_cuentas.update({
            where: { id: cuentaId },
            data: { saldo_actual: { increment: monto } },
          });
        }
      });
    } catch (error) {
      if (this.isMissingTableError(error)) {
        this.logger.warn(`Reversa Tesorería omitida por tablas faltantes (${ordenId})`);
        return;
      }
      this.logger.error(`Error al revertir movimiento de Tesorería para OP ${ordenId}: ${(error as Error).message}`);
    }
  }

  private async getCuentaTesoreriaPagoProveedor(
    tx: Tx,
    empresaId: string,
    medioPagoId?: string | null,
  ): Promise<{ id: string | null; tipo_tesoreria?: string | null; warning?: string }> {
    // Regla específica por medio de pago; si no hay, cae a la regla default (medio_pago_id NULL).
    const reglaMedio = medioPagoId
      ? await tx.tes_reglas.findFirst({
          where: { empresa_id: empresaId, tipo_operacion: TES_REGLA_PAGO_PROVEEDOR, medio_pago_id: medioPagoId },
          select: { activo: true, cuenta_id: true, tipo_tesoreria: true },
        })
      : null;
    const regla =
      reglaMedio ??
      (await tx.tes_reglas.findFirst({
        where: { empresa_id: empresaId, tipo_operacion: TES_REGLA_PAGO_PROVEEDOR, medio_pago_id: null },
        select: { activo: true, cuenta_id: true, tipo_tesoreria: true },
      }));

    if (!regla) {
      return {
        id: null,
        warning:
          'No existe una regla de Tesorería para "Pago a proveedor". Configurá la cuenta en Configuración > Tesorería > Reglas.',
      };
    }

    if (!regla.activo) {
      return {
        id: null,
        warning:
          'La regla de Tesorería para "Pago a proveedor" está inactiva. Activala en Configuración > Tesorería > Reglas.',
      };
    }

    if (!regla.cuenta_id) {
      return {
        id: null,
        warning:
          'La regla de Tesorería para "Pago a proveedor" no tiene cuenta asignada. Configurá la cuenta en Configuración > Tesorería > Reglas.',
      };
    }

    const cuenta = await tx.tes_cuentas.findFirst({
      where: {
        id: regla.cuenta_id,
        empresa_id: empresaId,
        activo: true,
      },
      select: { id: true },
    });

    if (!cuenta) {
      this.logger.warn(
        `Regla ${TES_REGLA_PAGO_PROVEEDOR} configurada con cuenta inválida o inactiva para empresa ${empresaId}`,
      );
      return {
        id: null,
        warning:
          'La cuenta configurada en Tesorería para "Pago a proveedor" está inactiva o no existe. Revisala en Configuración > Tesorería > Reglas.',
      };
    }

    return { id: cuenta.id, tipo_tesoreria: regla.tipo_tesoreria };
  }

  private async getCategoriaTesoreriaPagoProveedor(tx: Tx, empresaId: string): Promise<string> {
    const categoriaPagoProveedor = await tx.tes_categorias.findFirst({
      where: {
        codigo_interno: TES_CATEGORIA_CODIGO_PAGO_PROVEEDOR,
        is_sistema: true,
      },
      select: { id: true },
    });
    if (categoriaPagoProveedor) return categoriaPagoProveedor.id;

    const categoriaEgresoOtro = await tx.tes_categorias.findFirst({
      where: {
        codigo_interno: TES_CATEGORIA_CODIGO_EGRESO_OTRO,
        is_sistema: true,
      },
      select: { id: true },
    });
    if (categoriaEgresoOtro) return categoriaEgresoOtro.id;

    const categoriaEmpresa = await tx.tes_categorias.findFirst({
      where: {
        empresa_id: empresaId,
        nombre: 'Pago a Proveedor',
        tipo: 'EGRESO',
      },
      select: { id: true },
    });
    if (categoriaEmpresa) return categoriaEmpresa.id;

    const creada = await tx.tes_categorias.create({
      data: {
        empresa_id: empresaId,
        nombre: 'Pago a Proveedor',
        descripcion: 'Egreso generado automáticamente desde orden de pago proveedor',
        tipo: 'EGRESO',
        requiere_comprobante: true,
        is_sistema: false,
        activo: true,
      },
      select: { id: true },
    });
    return creada.id;
  }

  private toNumber(value: Prisma.Decimal | number | string | null | undefined): number {
    const n = Number(value || 0);
    return Number.isFinite(n) ? n : 0;
  }

  private round(value: number, decimals = 2): number {
    const factor = 10 ** decimals;
    return Math.round((value + Number.EPSILON) * factor) / factor;
  }

  private equalsAmount(a: number, b: number, epsilon = 0.01): boolean {
    return Math.abs(this.round(a, 2) - this.round(b, 2)) <= epsilon;
  }

  private parseDateStart(value: string): Date {
    const normalized = /^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T00:00:00.000` : value;
    const date = new Date(normalized);
    if (Number.isNaN(date.getTime())) {
      throw new BadRequestException(`Fecha inválida: ${value}`);
    }
    date.setHours(0, 0, 0, 0);
    return date;
  }

  private parseDateEnd(value: string): Date {
    const normalized = /^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T23:59:59.999` : value;
    const date = new Date(normalized);
    if (Number.isNaN(date.getTime())) {
      throw new BadRequestException(`Fecha inválida: ${value}`);
    }
    date.setHours(23, 59, 59, 999);
    return date;
  }

  private normalizeOrigenTipo(value?: string): 'compra' | 'gasto' | null {
    const normalized = String(value || '').trim().toLowerCase();
    if (normalized === 'compra' || normalized === 'gasto') return normalized;
    return null;
  }

  private getCuentaOrigenFilter(origenTipo: 'compra' | 'gasto'): Prisma.cuentas_pagarWhereInput {
    if (origenTipo === 'compra') {
      return {
        OR: [
          { origen_tipo: 'compra' },
          { origen_tipo: null, compra_id: { not: null } },
        ],
      };
    }

    return { origen_tipo: 'gasto' };
  }

  private resolveCuentaOrigen(cuenta: {
    compra_id?: string | null;
    origen_tipo?: string | null;
    origen_id?: string | null;
  }): { origen_tipo: 'compra' | 'gasto' | null; origen_id: string | null } {
    const tipo = this.normalizeOrigenTipo(cuenta.origen_tipo || undefined);
    if (tipo === 'compra') {
      return { origen_tipo: 'compra', origen_id: cuenta.origen_id || cuenta.compra_id || null };
    }
    if (tipo === 'gasto') {
      return { origen_tipo: 'gasto', origen_id: cuenta.origen_id || null };
    }
    if (cuenta.compra_id) {
      return { origen_tipo: 'compra', origen_id: cuenta.compra_id };
    }
    return { origen_tipo: null, origen_id: null };
  }

  private async buildCuentaPagarBusquedaWhere(
    empresaId: string,
    busqueda: string,
  ): Promise<Prisma.cuentas_pagarWhereInput> {
    const [compras, gastos] = await Promise.all([
      this.prisma.compra_cab.findMany({
        where: {
          empresa_id: empresaId,
          activo: true,
          OR: [
            { numero_factura: { contains: busqueda, mode: 'insensitive' } },
            { timbrado_proveedor: { contains: busqueda, mode: 'insensitive' } },
          ],
        },
        select: { id: true },
        take: 200,
      }),
      this.findGastosByBusquedaSafe(empresaId, busqueda),
    ]);

    const compraIds = compras.map((item) => item.id);
    const gastoIds = gastos.map((item) => item.id);

    const terms: Prisma.cuentas_pagarWhereInput[] = [
      {
        proveedores: {
          personas: {
            razon_social: { contains: busqueda, mode: 'insensitive' },
          },
        },
      },
      {
        proveedores: {
          personas: {
            ruc: { contains: busqueda, mode: 'insensitive' },
          },
        },
      },
    ];

    if (compraIds.length > 0) {
      terms.push({
        OR: [
          { origen_tipo: 'compra', origen_id: { in: compraIds } },
          { origen_tipo: null, compra_id: { in: compraIds } },
        ],
      });
    }
    if (gastoIds.length > 0) {
      terms.push({
        origen_tipo: 'gasto',
        origen_id: { in: gastoIds },
      });
    }

    return { OR: terms };
  }

  private async enrichCuentasPagarOrigen(items: any[], empresaId: string) {
    if (!Array.isArray(items) || items.length === 0) return items;

    const gastoIds = Array.from(
      new Set(
        items
          .map((item) => this.resolveCuentaOrigen(item))
          .filter((ref): ref is { origen_tipo: 'gasto'; origen_id: string } => ref.origen_tipo === 'gasto' && !!ref.origen_id)
          .map((ref) => ref.origen_id),
      ),
    );

    const gastos = gastoIds.length ? await this.findGastosByIdsSafe(empresaId, gastoIds) : [];
    const gastosMap = new Map(gastos.map((gasto) => [gasto.id, gasto]));

    return items.map((item) => {
      const origen = this.resolveCuentaOrigen(item);
      let documentoRef = 'S/N';
      let fechaOrigen: Date | null = null;
      let montoOrigen: number | null = null;

      if (origen.origen_tipo === 'compra') {
        const compra = item.compra_cab;
        if (compra) {
          documentoRef = this.formatCompraComprobante(compra);
          fechaOrigen = compra.fecha_emision ?? null;
          montoOrigen = this.toNumber(compra.total);
        } else if (origen.origen_id) {
          documentoRef = origen.origen_id.slice(0, 8);
        }
      } else if (origen.origen_tipo === 'gasto') {
        const gasto = origen.origen_id ? gastosMap.get(origen.origen_id) : null;
        if (gasto) {
          documentoRef = this.formatCompraComprobante(gasto);
          fechaOrigen = gasto.fecha_emision ?? null;
          montoOrigen = this.toNumber(gasto.total);
        } else if (origen.origen_id) {
          documentoRef = origen.origen_id.slice(0, 8);
        }
      }

      return {
        ...item,
        origen_tipo: origen.origen_tipo,
        origen_id: origen.origen_id,
        documento_ref: documentoRef,
        fecha_origen: fechaOrigen,
        monto_origen: montoOrigen,
      };
    });
  }

  private async enrichOrdenesPagoOrigen(items: any[], empresaId: string) {
    if (!Array.isArray(items) || items.length === 0) return items;

    const cuentas = items.flatMap((item) =>
      (item.orden_pago_proveedor_det || [])
        .map((det: any) => det.cuentas_pagar)
        .filter(Boolean),
    );

    const cuentasEnriched = await this.enrichCuentasPagarOrigen(cuentas, empresaId);
    const cuentasById = new Map(cuentasEnriched.map((cuenta: any) => [cuenta.id, cuenta]));

    return items.map((item) => {
      const detalles = (item.orden_pago_proveedor_det || []).map((det: any) => ({
        ...det,
        cuentas_pagar: det.cuentas_pagar
          ? cuentasById.get(det.cuentas_pagar.id) || det.cuentas_pagar
          : det.cuentas_pagar,
      }));

      const refs = detalles
        .map((det: any) => det.cuentas_pagar)
        .filter((cuenta: any) => cuenta?.origen_tipo && cuenta?.origen_id);
      const uniqueKeys = Array.from(
        new Set(refs.map((cuenta: any) => `${cuenta.origen_tipo}:${cuenta.origen_id}`)),
      );

      let ordenOrigenTipo: string | null = null;
      let ordenOrigenId: string | null = null;
      let documentoRef: string | null = null;
      let fechaOrigen: Date | null = null;

      if (uniqueKeys.length === 1 && refs.length > 0) {
        ordenOrigenTipo = refs[0].origen_tipo;
        ordenOrigenId = refs[0].origen_id;
        documentoRef = refs[0].documento_ref ?? null;
        fechaOrigen = refs[0].fecha_origen ?? null;
      } else if (uniqueKeys.length > 1) {
        ordenOrigenTipo = 'mixto';
        documentoRef = 'MULTI';
      }

      return {
        ...item,
        orden_pago_proveedor_det: detalles,
        origen_tipo: ordenOrigenTipo,
        origen_id: ordenOrigenId,
        documento_ref: documentoRef,
        fecha_origen: fechaOrigen,
      };
    });
  }

  private async findGastosByBusquedaSafe(empresaId: string, busqueda: string) {
    try {
      return await this.prisma.gasto_cab.findMany({
        where: {
          empresa_id: empresaId,
          activo: true,
          OR: [
            { numero_factura: { contains: busqueda, mode: 'insensitive' } },
            { ruc: { contains: busqueda, mode: 'insensitive' } },
            { descripcion: { contains: busqueda, mode: 'insensitive' } },
          ],
        },
        select: { id: true },
        take: 200,
      });
    } catch (error) {
      if (this.isMissingTableError(error)) return [];
      throw error;
    }
  }

  private async findGastosByIdsSafe(empresaId: string, ids: string[]) {
    try {
      return await this.prisma.gasto_cab.findMany({
        where: {
          empresa_id: empresaId,
          id: { in: ids },
        },
        select: {
          id: true,
          establecimiento: true,
          punto_expedicion: true,
          numero_factura: true,
          fecha_emision: true,
          total: true,
          estado: true,
          anulado: true,
        },
      });
    } catch (error) {
      if (this.isMissingTableError(error)) return [];
      throw error;
    }
  }

  private isMissingTableError(error: unknown) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      return error.code === 'P2021';
    }
    const message = String((error as any)?.message || '');
    return message.includes('does not exist in the current database');
  }

  private hasTesoreriaBridgeSupport(db: any): boolean {
    return Boolean(
      db?.tes_movimientos &&
      db?.tes_cuentas &&
      db?.tes_reglas &&
      db?.tes_categorias &&
      db?.tes_movimiento_det,
    );
  }

  private formatCompraComprobante(
    compra?: { establecimiento?: string | null; punto_expedicion?: string | null; numero_factura?: string | null } | null,
  ): string {
    if (!compra) return 'S/N';
    const establecimiento = String(compra.establecimiento || '').trim();
    const punto = String(compra.punto_expedicion || '').trim();
    const numero = String(compra.numero_factura || '').trim();

    if (establecimiento && punto && numero) {
      return `${establecimiento.padStart(3, '0')}-${punto.padStart(3, '0')}-${numero.padStart(7, '0')}`;
    }

    if (numero) return numero;
    return 'S/N';
  }

}
