import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { GastosService } from 'src/gastos/gastos.service';
import { OrdenesCompraService } from 'src/ordenes-compra/ordenes-compra.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateGastoDto } from 'src/gastos/dto/create-gasto.dto';
import { CreateOrdenCompraDto } from 'src/ordenes-compra/dto/create-orden-compra.dto';
import { CreateRequisicionDto, RequisicionItemDto } from './dto/create-requisicion.dto';
import {
  AprobarRequisicionDto,
  CancelarRequisicionDto,
  GenerarDesdeRequisicionGastoDto,
  GenerarDesdeRequisicionOrdenCompraDto,
  GenerarOrdenesCompraDistribuidasDto,
  RechazarRequisicionDto,
  UpsertRequisicionAprobacionConfigDto,
  UpsertRequisicionPresupuestoDto,
} from './dto/requisicion-config.dto';
import { UpdateRequisicionDto } from './dto/update-requisicion.dto';

type Tx = Prisma.TransactionClient;

type NormalizedItem = {
  centro_costo_id: string | null;
  producto_id: string | null;
  tipo_gasto_id: string | null;
  descripcion: string;
  cantidad_solicitada: number;
  precio_unitario: number;
  porcentaje_iva: number;
  subtotal_estimado: number;
};

type CentroCostoLite = {
  id: string;
  codigo: string;
  descripcion: string;
};

@Injectable()
export class RequisicionesService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly ordenesCompraService: OrdenesCompraService,
    private readonly gastosService: GastosService,
  ) {}

  async create(dto: CreateRequisicionDto, empresaId: string, userId?: string) {
    this.validateCreatePayload(dto);
    const requisicionId = await this.prisma.$transaction(async (tx) => {
      await this.ensureRequisicionesHabilitadas(tx, empresaId);
      await this.validateCabeceraRefs(tx, empresaId, dto.cabecera.sucursal_id);
      const items = await this.normalizeItems(tx, empresaId, dto.cabecera.origen_tipo, dto.items);
      const totalEstimado = this.round(items.reduce((acc, item) => acc + item.subtotal_estimado, 0), 2);
      const numero = await this.getNextNumeroRequisicion(tx, empresaId);
      const fecha = dto.cabecera.fecha_requisicion ? new Date(dto.cabecera.fecha_requisicion) : new Date();

      const requisicion = await tx.requisicion_cab.create({
        data: {
          empresa_id: empresaId,
          sucursal_id: dto.cabecera.sucursal_id || null,
          solicitante_id: userId || null,
          numero_requisicion: numero,
          origen_tipo: dto.cabecera.origen_tipo,
          fecha_requisicion: fecha,
          estado: 'borrador',
          descripcion: dto.cabecera.descripcion || null,
          total_estimado: totalEstimado,
          total_ejecutado: 0,
          activo: true,
        },
      });

      if (items.length > 0) {
        await tx.requisicion_det.createMany({
          data: items.map((item) => ({
            requisicion_id: requisicion.id,
            centro_costo_id: item.centro_costo_id,
            producto_id: item.producto_id,
            tipo_gasto_id: item.tipo_gasto_id,
            descripcion: item.descripcion,
            cantidad_solicitada: item.cantidad_solicitada,
            cantidad_ejecutada: 0,
            precio_unitario: item.precio_unitario,
            porcentaje_iva: item.porcentaje_iva,
            subtotal_estimado: item.subtotal_estimado,
            monto_ejecutado: 0,
            estado: 'pendiente',
          })),
        });
      }

      await this.logHistorial(tx, requisicion.id, {
        accion: 'creada',
        estado_anterior: null,
        estado_nuevo: 'borrador',
        comentario: 'Requisición creada',
        usuario_id: userId || null,
      });

      return requisicion.id;
    });

    return this.findOne(requisicionId, empresaId, userId);
  }

  async findAll(
    empresaId: string,
    userId: string,
    query?: {
      page?: string;
      limit?: string;
      origen_tipo?: string;
      estado?: string;
      search?: string;
      fecha_desde?: string;
      fecha_hasta?: string;
    },
  ) {
    const page = Math.max(1, Number(query?.page) || 1);
    const limit = Math.min(Math.max(Number(query?.limit) || 20, 1), 100);
    const skip = (page - 1) * limit;

    const where: Prisma.requisicion_cabWhereInput = {
      empresa_id: empresaId,
      activo: true,
    };
    if (query?.origen_tipo) where.origen_tipo = query.origen_tipo;
    if (query?.estado) where.estado = query.estado;
    if (query?.fecha_desde || query?.fecha_hasta) {
      where.fecha_requisicion = {};
      if (query?.fecha_desde) where.fecha_requisicion.gte = this.parseDateBoundary(query.fecha_desde, false);
      if (query?.fecha_hasta) where.fecha_requisicion.lte = this.parseDateBoundary(query.fecha_hasta, true);
    }
    if (query?.search?.trim()) {
      const search = query.search.trim();
      where.OR = [
        { numero_requisicion: { contains: search, mode: 'insensitive' } },
        { descripcion: { contains: search, mode: 'insensitive' } },
        { requisicion_det: { some: { descripcion: { contains: search, mode: 'insensitive' } } } },
      ];
    }

    const [data, total] = await this.prisma.$transaction([
      this.prisma.requisicion_cab.findMany({
        where,
        skip,
        take: limit,
        include: {
          solicitante: { select: { id: true, username: true, nombres: true, apellidos: true } },
          aprobador_final: { select: { id: true, username: true, nombres: true, apellidos: true } },
          requisicion_det: {
            select: {
              id: true,
              centro_costo_id: true,
              producto_id: true,
              tipo_gasto_id: true,
              descripcion: true,
              cantidad_solicitada: true,
              cantidad_ejecutada: true,
              precio_unitario: true,
              subtotal_estimado: true,
              monto_ejecutado: true,
              estado: true,
              cont_centros_costo: { select: { id: true, codigo: true, descripcion: true } },
            },
            orderBy: { created_at: 'asc' },
          },
          _count: {
            select: {
              orden_compra_cab: true,
            },
          },
        },
        orderBy: [{ fecha_requisicion: 'desc' }, { created_at: 'desc' }],
      }),
      this.prisma.requisicion_cab.count({ where }),
    ]);

    const canViewAmounts = await this.hasComprasEditarPermission(this.prisma, empresaId, userId);
    const sanitizedData = data.map((row) => {
      const sanitized = this.sanitizeMonetaryFields(row, canViewAmounts) as Record<string, any>;
      return {
        ...sanitized,
        ordenes_compra_generadas: Number(row?._count?.orden_compra_cab || 0),
      };
    });

    // KPIs sobre TODO el filtro (no la página): conteo por estado.
    const estadoGroups = await this.prisma.requisicion_cab.groupBy({
      by: ['estado'],
      where,
      _count: { _all: true },
    });
    const stats = {
      total,
      borrador: 0,
      pendiente_aprobacion: 0,
      aprobada: 0,
      ejecutada_parcial: 0,
      ejecutada_total: 0,
      cancelada: 0,
      rechazada: 0,
    };
    for (const g of estadoGroups) {
      const e = String(g.estado ?? '').toLowerCase();
      const c = g._count._all;
      if (e === 'borrador') stats.borrador = c;
      else if (e === 'pendiente_aprobacion') stats.pendiente_aprobacion = c;
      else if (e === 'aprobada') stats.aprobada = c;
      else if (e === 'ejecutada_parcial') stats.ejecutada_parcial = c;
      else if (e === 'ejecutada_total') stats.ejecutada_total = c;
      else if (e === 'cancelada') stats.cancelada = c;
      else if (e === 'rechazada') stats.rechazada = c;
    }

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

  async findOne(id: string, empresaId: string, userId: string) {
    const requisicion = await this.prisma.requisicion_cab.findFirst({
      where: { id, empresa_id: empresaId, activo: true },
      include: {
        empresas_sucursales: { select: { id: true, descripcion: true, punto_establecimiento: true } },
        solicitante: { select: { id: true, username: true, nombres: true, apellidos: true } },
        aprobador_final: { select: { id: true, username: true, nombres: true, apellidos: true } },
        requisicion_det: {
          include: {
            productos: { select: { id: true, descripcion: true, cod_producto: true } },
            tipo_gasto: { select: { id: true, descripcion: true } },
            cont_centros_costo: { select: { id: true, codigo: true, descripcion: true } },
          },
          orderBy: { created_at: 'asc' },
        },
        requisicion_aprobacion: {
          include: {
            perfiles: { select: { id: true, perfil: true, desc_perfil: true } },
            usuario: { select: { id: true, username: true, nombres: true, apellidos: true } },
          },
          orderBy: { nivel: 'asc' },
        },
        requisicion_historial: {
          include: {
            usuario: { select: { id: true, username: true, nombres: true, apellidos: true } },
          },
          orderBy: { created_at: 'desc' },
          take: 50,
        },
      },
    });

    if (!requisicion) {
      throw new NotFoundException('Requisición no encontrada');
    }
    const canViewAmounts = await this.hasComprasEditarPermission(this.prisma, empresaId, userId);
    return this.sanitizeMonetaryFields(requisicion, canViewAmounts);
  }

  async update(id: string, dto: UpdateRequisicionDto, empresaId: string, userId?: string) {
    if (dto.items && dto.items.length === 0) {
      throw new BadRequestException('items no puede ser vacío');
    }

    await this.prisma.$transaction(async (tx) => {
      await this.ensureRequisicionesHabilitadas(tx, empresaId);
      const existing = await tx.requisicion_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
      });
      if (!existing) throw new NotFoundException('Requisición no encontrada');
      if (existing.estado !== 'borrador') {
        throw new BadRequestException('Solo se pueden editar requisiciones en estado borrador');
      }

      const patchCab: Prisma.requisicion_cabUncheckedUpdateInput = {
        updated_at: new Date(),
      };
      if (dto.cabecera?.sucursal_id !== undefined) {
        await this.validateCabeceraRefs(tx, empresaId, dto.cabecera.sucursal_id);
        patchCab.sucursal_id = dto.cabecera.sucursal_id || null;
      }
      if (dto.cabecera?.descripcion !== undefined) patchCab.descripcion = dto.cabecera.descripcion || null;
      if (dto.cabecera?.fecha_requisicion !== undefined) {
        patchCab.fecha_requisicion = dto.cabecera.fecha_requisicion
          ? new Date(dto.cabecera.fecha_requisicion)
          : null;
      }

      if (dto.items && dto.items.length > 0) {
        const origenTipo = dto.cabecera?.origen_tipo || (existing.origen_tipo as 'compra' | 'gasto');
        const items = await this.normalizeItems(tx, empresaId, origenTipo, dto.items);
        const totalEstimado = this.round(items.reduce((acc, item) => acc + item.subtotal_estimado, 0), 2);
        patchCab.total_estimado = totalEstimado;

        await tx.requisicion_det.deleteMany({ where: { requisicion_id: id } });
        await tx.requisicion_det.createMany({
          data: items.map((item) => ({
            requisicion_id: id,
            centro_costo_id: item.centro_costo_id,
            producto_id: item.producto_id,
            tipo_gasto_id: item.tipo_gasto_id,
            descripcion: item.descripcion,
            cantidad_solicitada: item.cantidad_solicitada,
            cantidad_ejecutada: 0,
            precio_unitario: item.precio_unitario,
            porcentaje_iva: item.porcentaje_iva,
            subtotal_estimado: item.subtotal_estimado,
            monto_ejecutado: 0,
            estado: 'pendiente',
          })),
        });
      }

      await tx.requisicion_cab.update({
        where: { id },
        data: patchCab,
      });

      await this.logHistorial(tx, id, {
        accion: 'actualizada',
        estado_anterior: 'borrador',
        estado_nuevo: 'borrador',
        comentario: 'Requisición actualizada',
        usuario_id: userId || null,
      });
    });

    return this.findOne(id, empresaId, userId || '');
  }

  async enviarAprobacion(id: string, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      await this.ensureRequisicionesHabilitadas(tx, empresaId);
      const req = await tx.requisicion_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        include: { requisicion_det: true },
      });
      if (!req) throw new NotFoundException('Requisición no encontrada');
      if (req.estado !== 'borrador' && req.estado !== 'rechazada') {
        throw new BadRequestException('Solo se puede enviar a aprobación una requisición en borrador o rechazada');
      }
      if (!req.requisicion_det.length) {
        throw new BadRequestException('La requisición no tiene ítems');
      }

      const hasItemLibreCompra =
        String(req.origen_tipo) === 'compra' && req.requisicion_det.some((item) => !item.producto_id);
      const total = this.round(this.toNumber(req.total_estimado), 2);
      const configs = hasItemLibreCompra
        ? await this.resolveConfigsForNecesidad(tx, empresaId, String(req.origen_tipo))
        : await this.resolveConfigsForMonto(tx, empresaId, String(req.origen_tipo), total);
      if (!configs.length) {
        throw new BadRequestException(
          hasItemLibreCompra
            ? `No hay nivel 1 activo de aprobación configurado para ${req.origen_tipo}`
            : `No hay niveles de aprobación configurados para ${req.origen_tipo} y monto ${total}`,
        );
      }

      await tx.requisicion_aprobacion.deleteMany({ where: { requisicion_id: id } });
      await tx.requisicion_aprobacion.createMany({
        data: configs.map((cfg) => ({
          requisicion_id: id,
          config_id: cfg.id,
          nivel: cfg.nivel,
          perfil_id: cfg.perfil_id,
          estado: 'pendiente',
        })),
      });

      await tx.requisicion_cab.update({
        where: { id },
        data: {
          estado: 'pendiente_aprobacion',
          motivo_rechazo: null,
          updated_at: new Date(),
        },
      });

      await this.logHistorial(tx, id, {
        accion: 'enviada_aprobacion',
        estado_anterior: String(req.estado),
        estado_nuevo: 'pendiente_aprobacion',
        comentario: hasItemLibreCompra
          ? 'Requisición enviada a aprobación por necesidad (ítem libre, nivel 1)'
          : 'Requisición enviada a aprobación',
        usuario_id: userId || null,
      });
    });

    return this.findOne(id, empresaId, userId || '');
  }

  async aprobar(id: string, empresaId: string, userId: string, dto?: AprobarRequisicionDto) {
    await this.prisma.$transaction(async (tx) => {
      await this.ensureRequisicionesHabilitadas(tx, empresaId);
      const req = await tx.requisicion_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        include: {
          requisicion_det: true,
          requisicion_aprobacion: { orderBy: { nivel: 'asc' } },
        },
      });
      if (!req) throw new NotFoundException('Requisición no encontrada');
      if (req.estado !== 'pendiente_aprobacion') {
        throw new BadRequestException('La requisición no está pendiente de aprobación');
      }

      const pendiente = req.requisicion_aprobacion.find((ap) => String(ap.estado) === 'pendiente');
      if (!pendiente) {
        throw new BadRequestException('No hay niveles pendientes de aprobación');
      }

      await this.assertComprasEditarPermission(tx, empresaId, userId);
      await this.assertUserHasPerfil(tx, userId, pendiente.perfil_id);

      await tx.requisicion_aprobacion.update({
        where: { id: pendiente.id },
        data: {
          estado: 'aprobada',
          aprobado_por: userId,
          fecha_resuelta: new Date(),
          comentario: dto?.comentario || null,
          updated_at: new Date(),
        },
      });

      const remaining = req.requisicion_aprobacion.filter((ap) => ap.id !== pendiente.id);
      const hasPendings = remaining.some((ap) => String(ap.estado) === 'pendiente');

      if (!hasPendings) {
        await tx.requisicion_cab.update({
          where: { id },
          data: {
            estado: 'aprobada',
            aprobador_final_id: userId,
            fecha_aprobacion_final: new Date(),
            updated_at: new Date(),
          },
        });

        await this.logHistorial(tx, id, {
          accion: 'aprobada',
          estado_anterior: 'pendiente_aprobacion',
          estado_nuevo: 'aprobada',
          comentario: dto?.comentario || 'Requisición aprobada en nivel final',
          usuario_id: userId,
        });
      } else {
        await this.logHistorial(tx, id, {
          accion: 'aprobada_nivel',
          estado_anterior: 'pendiente_aprobacion',
          estado_nuevo: 'pendiente_aprobacion',
          comentario: `Aprobado nivel ${pendiente.nivel}`,
          usuario_id: userId,
        });
      }
    });

    return this.findOne(id, empresaId, userId);
  }

  async rechazar(id: string, empresaId: string, userId: string, dto?: RechazarRequisicionDto) {
    await this.prisma.$transaction(async (tx) => {
      await this.ensureRequisicionesHabilitadas(tx, empresaId);
      const req = await tx.requisicion_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        include: {
          requisicion_aprobacion: { orderBy: { nivel: 'asc' } },
        },
      });
      if (!req) throw new NotFoundException('Requisición no encontrada');
      if (req.estado !== 'pendiente_aprobacion') {
        throw new BadRequestException('Solo se puede rechazar requisiciones pendientes de aprobación');
      }

      const pendiente = req.requisicion_aprobacion.find((ap) => String(ap.estado) === 'pendiente');
      if (!pendiente) {
        throw new BadRequestException('No hay niveles pendientes para rechazar');
      }

      await this.assertComprasEditarPermission(tx, empresaId, userId);
      await this.assertUserHasPerfil(tx, userId, pendiente.perfil_id);

      await tx.requisicion_aprobacion.updateMany({
        where: {
          requisicion_id: id,
          estado: 'pendiente',
        },
        data: {
          estado: 'rechazada',
          aprobado_por: userId,
          fecha_resuelta: new Date(),
          comentario: dto?.motivo || 'Rechazada',
          updated_at: new Date(),
        },
      });

      await tx.requisicion_cab.update({
        where: { id },
        data: {
          estado: 'rechazada',
          motivo_rechazo: dto?.motivo || null,
          updated_at: new Date(),
        },
      });

      await this.logHistorial(tx, id, {
        accion: 'rechazada',
        estado_anterior: 'pendiente_aprobacion',
        estado_nuevo: 'rechazada',
        comentario: dto?.motivo || 'Rechazada en aprobación',
        usuario_id: userId,
      });
    });

    return this.findOne(id, empresaId, userId);
  }

  async cancelar(id: string, empresaId: string, userId: string, dto?: CancelarRequisicionDto) {
    await this.prisma.$transaction(async (tx) => {
      await this.ensureRequisicionesHabilitadas(tx, empresaId);
      const req = await tx.requisicion_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        include: { requisicion_det: true },
      });
      if (!req) throw new NotFoundException('Requisición no encontrada');
      if (req.estado === 'cancelada') throw new BadRequestException('La requisición ya está cancelada');
      if (req.estado === 'ejecutada_total') {
        throw new BadRequestException('No se puede cancelar una requisición ya ejecutada totalmente');
      }

      await tx.requisicion_cab.update({
        where: { id },
        data: {
          estado: 'cancelada',
          motivo_cancelacion: dto?.motivo || null,
          updated_at: new Date(),
        },
      });

      await tx.requisicion_aprobacion.updateMany({
        where: { requisicion_id: id, estado: 'pendiente' },
        data: {
          estado: 'rechazada',
          comentario: 'Cancelada',
          updated_at: new Date(),
        },
      });

      await this.logHistorial(tx, id, {
        accion: 'cancelada',
        estado_anterior: String(req.estado),
        estado_nuevo: 'cancelada',
        comentario: dto?.motivo || 'Requisición cancelada',
        usuario_id: userId,
      });
    });

    return this.findOne(id, empresaId, userId);
  }

  async getAprobacionesConfig(empresaId: string) {
    return this.prisma.requisicion_aprobacion_config.findMany({
      where: { empresa_id: empresaId },
      include: {
        perfiles: { select: { id: true, perfil: true, desc_perfil: true } },
      },
      orderBy: [{ origen_tipo: 'asc' }, { nivel: 'asc' }],
    });
  }

  async upsertAprobacionesConfig(empresaId: string, dto: UpsertRequisicionAprobacionConfigDto) {
    await this.prisma.$transaction(async (tx) => {
      for (const item of dto.items || []) {
        await this.validatePerfilEmpresa(tx, empresaId, item.perfil_id);
      }

      await tx.requisicion_aprobacion_config.deleteMany({
        where: { empresa_id: empresaId },
      });

      if ((dto.items || []).length > 0) {
        await tx.requisicion_aprobacion_config.createMany({
          data: dto.items.map((item) => ({
            empresa_id: empresaId,
            origen_tipo: item.origen_tipo,
            nivel: item.nivel,
            perfil_id: item.perfil_id,
            monto_desde: this.round(item.monto_desde ?? 0, 2),
            monto_hasta: item.monto_hasta == null ? null : this.round(item.monto_hasta, 2),
            activo: item.activo ?? true,
          })),
        });
      }
    });

    return this.getAprobacionesConfig(empresaId);
  }

  async aplicarConfiguracionRecomendada(empresaId: string) {
    const RECOMMENDED_PROFILES = [
      { perfil: 'Compras Jr', desc_perfil: 'Compras Junior' },
      { perfil: 'Compras Sr', desc_perfil: 'Compras Senior' },
      { perfil: 'Gte Compras', desc_perfil: 'Gerente de Compras' },
      { perfil: 'Dir Financiero', desc_perfil: 'Director Financiero' },
    ];

    const RECOMMENDED_LEVELS = [
      { perfilDesc: 'Compras Junior', monto_desde: 0, monto_hasta: 1_000_000 },
      { perfilDesc: 'Compras Senior', monto_desde: 1_000_001, monto_hasta: 5_000_000 },
      { perfilDesc: 'Gerente de Compras', monto_desde: 5_000_001, monto_hasta: 50_000_000 },
      { perfilDesc: 'Director Financiero', monto_desde: 50_000_001, monto_hasta: null },
    ];

    const perfilesPorDesc = await this.prisma.$transaction(async (tx) => {
      const map = new Map<string, string>();
      for (const def of RECOMMENDED_PROFILES) {
        let existente = await tx.perfiles.findFirst({
          where: { empresa_id: empresaId, desc_perfil: def.desc_perfil },
          select: { id: true },
        });
        if (!existente) {
          existente = await tx.perfiles.findFirst({
            where: { empresa_id: empresaId, perfil: def.perfil },
            select: { id: true },
          });
        }
        if (!existente) {
          const creado = await tx.perfiles.create({
            data: {
              empresa_id: empresaId,
              perfil: def.perfil,
              desc_perfil: def.desc_perfil,
            },
            select: { id: true },
          });
          existente = creado;
        }
        map.set(def.desc_perfil, existente.id);
      }

      await tx.requisicion_aprobacion_config.deleteMany({
        where: { empresa_id: empresaId },
      });

      const data: Prisma.requisicion_aprobacion_configCreateManyInput[] = [];
      for (const origen of ['compra', 'gasto'] as const) {
        RECOMMENDED_LEVELS.forEach((lvl, idx) => {
          const perfilId = map.get(lvl.perfilDesc);
          if (!perfilId) return;
          data.push({
            empresa_id: empresaId,
            origen_tipo: origen,
            nivel: idx + 1,
            perfil_id: perfilId,
            monto_desde: this.round(lvl.monto_desde, 2),
            monto_hasta: lvl.monto_hasta == null ? null : this.round(lvl.monto_hasta, 2),
            activo: true,
          });
        });
      }

      if (data.length > 0) {
        await tx.requisicion_aprobacion_config.createMany({ data });
      }

      return map;
    });

    return {
      perfiles_aplicados: Array.from(perfilesPorDesc.entries()).map(([desc_perfil, id]) => ({
        id,
        desc_perfil,
      })),
      config: await this.getAprobacionesConfig(empresaId),
    };
  }

  async getPresupuestos(
    empresaId: string,
    query?: {
      periodo_id?: string;
      centro_costo_id?: string;
      origen_tipo?: string;
    },
  ) {
    return this.prisma.requisicion_presupuesto.findMany({
      where: {
        empresa_id: empresaId,
        ...(query?.periodo_id ? { periodo_id: query.periodo_id } : {}),
        ...(query?.centro_costo_id ? { centro_costo_id: query.centro_costo_id } : {}),
        ...(query?.origen_tipo ? { origen_tipo: query.origen_tipo } : {}),
      },
      include: {
        cont_periodos: {
          select: { id: true, numero: true, mes: true, anio: true, estado: true },
        },
        cont_centros_costo: {
          select: { id: true, codigo: true, descripcion: true, activo: true },
        },
      },
      orderBy: [{ origen_tipo: 'asc' }, { created_at: 'desc' }],
    });
  }

  async upsertPresupuestos(empresaId: string, dto: UpsertRequisicionPresupuestoDto) {
    await this.prisma.$transaction(async (tx) => {
      for (const item of dto.items || []) {
        await this.validatePresupuestoRefs(tx, empresaId, item.periodo_id, item.centro_costo_id);
        const existing = await tx.requisicion_presupuesto.findFirst({
          where: {
            empresa_id: empresaId,
            periodo_id: item.periodo_id,
            centro_costo_id: item.centro_costo_id,
            origen_tipo: item.origen_tipo,
          },
        });

        if (existing) {
          await tx.requisicion_presupuesto.update({
            where: { id: existing.id },
            data: {
              monto_asignado: this.round(item.monto_asignado, 2),
              updated_at: new Date(),
            },
          });
        } else {
          await tx.requisicion_presupuesto.create({
            data: {
              empresa_id: empresaId,
              periodo_id: item.periodo_id,
              centro_costo_id: item.centro_costo_id,
              origen_tipo: item.origen_tipo,
              monto_asignado: this.round(item.monto_asignado, 2),
              monto_comprometido: 0,
              monto_ejecutado: 0,
              activo: true,
            },
          });
        }
      }
    });

    return this.getPresupuestos(empresaId);
  }

  async crearOrdenCompraDesdeRequisicion(
    requisicionId: string,
    empresaId: string,
    dto: GenerarDesdeRequisicionOrdenCompraDto,
    userId?: string,
  ) {
    await this.ensureRequisicionesHabilitadas(this.prisma, empresaId);
    const req = await this.prisma.requisicion_cab.findFirst({
      where: { id: requisicionId, empresa_id: empresaId, activo: true },
      include: {
        requisicion_det: true,
      },
    });
    if (!req) throw new NotFoundException('Requisición no encontrada');
    if (req.origen_tipo !== 'compra') {
      throw new BadRequestException('La requisición no es de tipo compra');
    }
    if (!['aprobada', 'ejecutada_parcial'].includes(String(req.estado))) {
      throw new BadRequestException('Solo se puede generar orden de compra desde requisiciones aprobadas');
    }

    const pendingItems = req.requisicion_det.filter((item) => {
      const qtyPending = this.round(this.toNumber(item.cantidad_solicitada) - this.toNumber(item.cantidad_ejecutada), 4);
      return qtyPending > 0.0001;
    });
    if (!pendingItems.length) {
      throw new BadRequestException('La requisición no tiene ítems pendientes para generar OC');
    }

    const payload: CreateOrdenCompraDto = {
      cabecera: {
        sucursal_id: dto.sucursal_id,
        proveedor_id: dto.proveedor_id,
        moneda_id: dto.moneda_id,
        fecha_emision: dto.fecha_emision,
        fecha_entrega: dto.fecha_entrega,
        observaciones: dto.observaciones || req.descripcion || undefined,
        requisicion_id: requisicionId,
      },
      items: pendingItems.map((item) => ({
        centro_costo_id: item.centro_costo_id || undefined,
        requisicion_det_id: item.id,
        producto_id: item.producto_id || undefined,
        descripcion: item.descripcion || 'Ítem de requisición',
        cantidad: this.round(
          this.toNumber(item.cantidad_solicitada) - this.toNumber(item.cantidad_ejecutada),
          4,
        ),
        precio_unitario: this.toNumber(item.precio_unitario),
        porcentaje_iva: this.toNumber(item.porcentaje_iva) || 10,
      })),
    };

    const ordenCreada = await this.ordenesCompraService.create(payload, empresaId, userId);
    try {
      await this.prisma.$transaction(async (tx) => {
        await this.logHistorial(tx, requisicionId, {
          accion: 'oc_creada',
          estado_anterior: String(req.estado || ''),
          estado_nuevo: String(req.estado || ''),
          comentario: `Orden de compra ${String((ordenCreada as any)?.numero_orden || (ordenCreada as any)?.id || '').trim() || 'generada'} creada desde requisición`,
          usuario_id: userId || null,
          datos: {
            orden_compra_id: (ordenCreada as any)?.id || null,
            numero_orden: (ordenCreada as any)?.numero_orden || null,
            origen: 'requisicion',
          },
        });
      });
    } catch (_) {
      // no-op: no bloquear creación operativa por un fallo de historial
    }
    return ordenCreada;
  }

  async generarOrdenesCompraDistribuidas(
    requisicionId: string,
    empresaId: string,
    dto: GenerarOrdenesCompraDistribuidasDto,
    userId?: string,
  ) {
    await this.ensureRequisicionesHabilitadas(this.prisma, empresaId);
    const req = await this.prisma.requisicion_cab.findFirst({
      where: { id: requisicionId, empresa_id: empresaId, activo: true },
      include: { requisicion_det: true },
    });
    if (!req) throw new NotFoundException('Requisición no encontrada');
    if (req.origen_tipo !== 'compra') {
      throw new BadRequestException('La requisición no es de tipo compra');
    }
    if (!['aprobada', 'ejecutada_parcial'].includes(String(req.estado))) {
      throw new BadRequestException('Solo se puede generar OC desde requisiciones aprobadas');
    }
    if (!dto.asignaciones?.length) {
      throw new BadRequestException('Debés asignar al menos una línea a un proveedor');
    }

    const detsById = new Map(req.requisicion_det.map((det) => [det.id, det]));
    const usadoPorLinea = new Map<string, number>();

    for (const asig of dto.asignaciones) {
      const det = detsById.get(asig.requisicion_det_id);
      if (!det) {
        throw new BadRequestException(
          `Línea ${asig.requisicion_det_id} no pertenece a la requisición`,
        );
      }
      const pendiente = this.round(
        this.toNumber(det.cantidad_solicitada) - this.toNumber(det.cantidad_ejecutada),
        4,
      );
      if (pendiente <= 0.0001) {
        throw new BadRequestException(
          `La línea "${det.descripcion ?? det.id}" no tiene cantidad pendiente`,
        );
      }
      const cantidadAsignada = asig.cantidad
        ? this.round(this.toNumber(asig.cantidad), 4)
        : pendiente;
      if (cantidadAsignada <= 0) {
        throw new BadRequestException(
          `Cantidad asignada inválida para línea "${det.descripcion ?? det.id}"`,
        );
      }
      const yaUsado = usadoPorLinea.get(det.id) ?? 0;
      const totalUsado = this.round(yaUsado + cantidadAsignada, 4);
      if (totalUsado > pendiente + 0.0001) {
        throw new BadRequestException(
          `La distribución para "${det.descripcion ?? det.id}" supera la cantidad pendiente (${pendiente})`,
        );
      }
      usadoPorLinea.set(det.id, totalUsado);
    }

    const grupos = new Map<string, GenerarOrdenesCompraDistribuidasDto['asignaciones']>();
    for (const asig of dto.asignaciones) {
      const lista = grupos.get(asig.proveedor_id) ?? [];
      lista.push(asig);
      grupos.set(asig.proveedor_id, lista);
    }

    const ocsCreadas: Array<{ proveedor_id: string; orden_compra_id: string }> = [];

    for (const [proveedorId, asignaciones] of grupos.entries()) {
      const items = asignaciones.map((asig) => {
        const det = detsById.get(asig.requisicion_det_id)!;
        const pendiente = this.round(
          this.toNumber(det.cantidad_solicitada) - this.toNumber(det.cantidad_ejecutada),
          4,
        );
        const cantidad = asig.cantidad
          ? this.round(this.toNumber(asig.cantidad), 4)
          : pendiente;
        return {
          centro_costo_id: asig.centro_costo_id || det.centro_costo_id || undefined,
          requisicion_det_id: det.id,
          producto_id: det.producto_id || undefined,
          descripcion: det.descripcion || 'Ítem de requisición',
          cantidad,
          precio_unitario: this.toNumber(asig.precio_unitario ?? det.precio_unitario),
          porcentaje_iva:
            this.toNumber(asig.porcentaje_iva ?? det.porcentaje_iva) || 10,
        };
      });

      const payload: CreateOrdenCompraDto = {
        cabecera: {
          sucursal_id: dto.sucursal_id,
          proveedor_id: proveedorId,
          moneda_id: dto.moneda_id,
          fecha_emision: dto.fecha_emision,
          fecha_entrega: dto.fecha_entrega,
          observaciones: dto.observaciones || req.descripcion || undefined,
          requisicion_id: requisicionId,
        },
        items,
      };

      const ordenCreada = await this.ordenesCompraService.create(payload, empresaId, userId);
      ocsCreadas.push({
        proveedor_id: proveedorId,
        orden_compra_id: (ordenCreada as { id?: string })?.id ?? '',
      });
    }

    try {
      await this.prisma.$transaction(async (tx) => {
        await this.logHistorial(tx, requisicionId, {
          accion: 'oc_creada',
          estado_anterior: String(req.estado || ''),
          estado_nuevo: String(req.estado || ''),
          comentario: `${ocsCreadas.length} orden(es) de compra generada(s) desde requisición`,
          usuario_id: userId || null,
          datos: {
            total_ordenes_creadas: ocsCreadas.length,
            ordenes: ocsCreadas,
            origen: 'requisicion_distribuida',
          },
        });
      });
    } catch (_) {
      // no-op: no bloquear creación operativa por un fallo de historial
    }

    return {
      requisicion_id: requisicionId,
      total_ordenes_creadas: ocsCreadas.length,
      ordenes: ocsCreadas,
    };
  }

  async crearGastoDesdeRequisicion(
    requisicionId: string,
    empresaId: string,
    dto: GenerarDesdeRequisicionGastoDto,
    userId?: string,
  ) {
    await this.ensureRequisicionesHabilitadas(this.prisma, empresaId);
    const req = await this.prisma.requisicion_cab.findFirst({
      where: { id: requisicionId, empresa_id: empresaId, activo: true },
      include: { requisicion_det: true },
    });
    if (!req) throw new NotFoundException('Requisición no encontrada');
    if (req.origen_tipo !== 'gasto') {
      throw new BadRequestException('La requisición no es de tipo gasto');
    }
    if (!['aprobada', 'ejecutada_parcial'].includes(String(req.estado))) {
      throw new BadRequestException('Solo se puede generar gasto desde requisiciones aprobadas');
    }

    const pendingItems = req.requisicion_det.filter((item) => {
      const qtyPending = this.round(this.toNumber(item.cantidad_solicitada) - this.toNumber(item.cantidad_ejecutada), 4);
      return qtyPending > 0.0001;
    });
    if (!pendingItems.length) {
      throw new BadRequestException('La requisición no tiene ítems pendientes para generar gasto');
    }

    const payload: CreateGastoDto = {
      cabecera: {
        proveedor_id: dto.proveedor_id,
        sucursal_id: dto.sucursal_id,
        moneda_id: dto.moneda_id,
        forma_pago: dto.forma_pago ?? 'CONTADO',
        fecha_emision: dto.fecha_emision,
        descripcion: dto.descripcion || req.descripcion || undefined,
        requisicion_id: requisicionId,
      },
      items: pendingItems.map((item) => ({
        requisicion_det_id: item.id,
        producto_id: item.producto_id || undefined,
        tipo_gasto_id: item.tipo_gasto_id || undefined,
        descripcion: item.descripcion || 'Ítem de requisición',
        cantidad: this.round(
          this.toNumber(item.cantidad_solicitada) - this.toNumber(item.cantidad_ejecutada),
          4,
        ),
        precio_unitario: this.toNumber(item.precio_unitario),
        porcentaje_iva: this.toNumber(item.porcentaje_iva) || 10,
      })),
    };

    return this.gastosService.create(payload, empresaId, userId);
  }

  private validateCreatePayload(dto: CreateRequisicionDto) {
    if (!dto.cabecera) throw new BadRequestException('cabecera es obligatoria');
    if (!dto.items || dto.items.length === 0) throw new BadRequestException('items es obligatorio');
  }

  private async validateCabeceraRefs(tx: Tx, empresaId: string, sucursalId?: string) {
    if (sucursalId) {
      const sucursal = await tx.empresas_sucursales.findFirst({
        where: { id: sucursalId, empresa_id: empresaId, active: true },
        select: { id: true },
      });
      if (!sucursal) {
        throw new BadRequestException('sucursal_id inválida para la empresa');
      }
    }
  }

  private async normalizeItems(
    tx: Tx,
    empresaId: string,
    origenTipo: 'compra' | 'gasto',
    items: RequisicionItemDto[],
  ): Promise<NormalizedItem[]> {
    const normalized: NormalizedItem[] = [];
    for (const item of items) {
      const centroCostoId = String(item.centro_costo_id || '').trim() || null;
      if (centroCostoId) {
        await this.validateCentroCostoRef(tx, empresaId, centroCostoId);
      }

      if (origenTipo === 'compra' && !item.producto_id && !item.descripcion?.trim()) {
        throw new BadRequestException('En requisiciones de compra cada ítem requiere producto o descripción');
      }
      if (origenTipo === 'gasto' && !item.tipo_gasto_id && !item.descripcion?.trim()) {
        throw new BadRequestException('En requisiciones de gasto cada ítem requiere tipo_gasto o descripción');
      }

      let productoRef: { precio_referencial: number; porcentaje_iva: number } | null = null;
      if (item.producto_id) {
        productoRef = await this.resolveProductoReferencia(tx, empresaId, item.producto_id);
      }
      if (item.tipo_gasto_id) {
        await this.validateTipoGastoRef(tx, empresaId, item.tipo_gasto_id);
      }

      const cantidad = this.round(this.toNumber(item.cantidad_solicitada), 4);
      const precio =
        item.precio_unitario !== undefined
          ? this.round(this.toNumber(item.precio_unitario), 4)
          : this.round(this.toNumber(productoRef?.precio_referencial ?? 0), 4);
      const porcentajeIva =
        item.porcentaje_iva !== undefined
          ? this.round(this.toNumber(item.porcentaje_iva), 2)
          : this.round(this.toNumber(productoRef?.porcentaje_iva ?? 10), 2);
      const subtotal = this.round(cantidad * precio, 2);

      if (cantidad <= 0) {
        throw new BadRequestException('cantidad_solicitada debe ser mayor a 0');
      }
      if (precio < 0) {
        throw new BadRequestException('precio_unitario no puede ser negativo');
      }

      normalized.push({
        centro_costo_id: centroCostoId,
        producto_id: item.producto_id || null,
        tipo_gasto_id: item.tipo_gasto_id || null,
        descripcion: item.descripcion?.trim() || 'Ítem de requisición',
        cantidad_solicitada: cantidad,
        precio_unitario: precio,
        porcentaje_iva: porcentajeIva,
        subtotal_estimado: subtotal,
      });
    }
    return normalized;
  }

  private async validateCentroCostoRef(tx: Tx, empresaId: string, centroCostoId: string) {
    const centro = await tx.cont_centros_costo.findFirst({
      where: { id: centroCostoId, empresa_id: empresaId, activo: true },
      select: { id: true },
    });
    if (!centro) {
      throw new BadRequestException('El centro de costo seleccionado no existe o no está activo para la empresa');
    }
  }

  private async resolveProductoReferencia(tx: Tx, empresaId: string, productoId: string) {
    const producto = await tx.productos.findFirst({
      where: { id: productoId, empresa_id: empresaId, active: true, deleted: false },
      select: {
        id: true,
        precio_costo: true,
        precio: true,
        porcentaje_iva: true,
      },
    });
    if (!producto) {
      throw new BadRequestException('El producto seleccionado no existe o no está activo para la empresa');
    }
    const precioCosto = this.toNumber(producto.precio_costo);
    const precioLista = this.toNumber(producto.precio);
    const precioReferencial = precioCosto > 0 ? precioCosto : precioLista > 0 ? precioLista : 0;
    const porcentajeIva = this.toNumber(producto.porcentaje_iva);
    return {
      precio_referencial: precioReferencial,
      porcentaje_iva: porcentajeIva > 0 ? porcentajeIva : 10,
    };
  }

  private async validateTipoGastoRef(tx: Tx, empresaId: string, tipoGastoId: string) {
    const tipo = await tx.tipo_gasto.findFirst({
      where: { id: tipoGastoId, activo: true, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
      select: { id: true },
    });
    if (!tipo) {
      throw new BadRequestException('El tipo de gasto seleccionado no existe o no está activo para la empresa');
    }
  }

  private async resolveConfigsForMonto(tx: Tx, empresaId: string, origenTipo: string, monto: number) {
    const configs = await tx.requisicion_aprobacion_config.findMany({
      where: { empresa_id: empresaId, origen_tipo: origenTipo, activo: true },
      orderBy: { nivel: 'asc' },
    });

    return configs.filter((cfg) => {
      const desde = this.toNumber(cfg.monto_desde);
      const hasta = cfg.monto_hasta == null ? null : this.toNumber(cfg.monto_hasta);
      if (monto < desde) return false;
      if (hasta != null && monto > hasta) return false;
      return true;
    });
  }

  private async resolveConfigsForNecesidad(tx: Tx, empresaId: string, origenTipo: string) {
    return tx.requisicion_aprobacion_config.findMany({
      where: {
        empresa_id: empresaId,
        origen_tipo: origenTipo,
        activo: true,
        nivel: 1,
      },
      orderBy: { nivel: 'asc' },
    });
  }

  private async assertUserHasPerfil(tx: Tx, userId: string, perfilId: string) {
    const hasPerfil = await tx.usuario_perfiles.findFirst({
      where: { user_id: userId, perfil_id: perfilId },
      select: { id: true },
    });
    if (!hasPerfil) {
      throw new BadRequestException('El usuario no tiene el rol/perfil requerido para aprobar este nivel');
    }
  }

  private async assertComprasEditarPermission(tx: Tx, empresaId: string, userId: string) {
    const allowed = await this.hasComprasEditarPermission(tx, empresaId, userId);
    if (!allowed) {
      throw new ForbiddenException('No tiene permiso EDITAR en el módulo COMPRAS');
    }
  }

  private async hasComprasEditarPermission(
    tx: Pick<Tx, 'usuario' | 'usuario_perfiles'> | Pick<PrismaService, 'usuario' | 'usuario_perfiles'>,
    empresaId: string,
    userId: string,
  ) {
    if (!userId) return false;

    const user = await tx.usuario.findFirst({
      where: { id: userId, empresa_id: empresaId },
      select: { is_superadmin: true },
    });
    if (!user) return false;
    if (Boolean(user.is_superadmin)) return true;

    const permission = await tx.usuario_perfiles.findFirst({
      where: {
        user_id: userId,
        perfiles: {
          OR: [{ empresa_id: empresaId }, { empresa_id: null }],
          perfiles_privilegios: {
            some: {
              active: true,
              modulos: { codigo: 'COMPRAS' },
              privilegios: { codigo: 'EDITAR' },
            },
          },
        },
      },
      select: { id: true },
    });

    return Boolean(permission);
  }

  private sanitizeMonetaryFields<T>(requisicion: T, canViewAmounts: boolean): T {
    if (canViewAmounts || !requisicion || typeof requisicion !== 'object') return requisicion;
    const clone = JSON.parse(JSON.stringify(requisicion)) as Record<string, any>;

    if ('total_estimado' in clone) clone.total_estimado = null;
    if ('total_ejecutado' in clone) clone.total_ejecutado = null;
    if (Array.isArray(clone.requisicion_det)) {
      clone.requisicion_det = clone.requisicion_det.map((det: Record<string, any>) => ({
        ...det,
        precio_unitario: null,
        subtotal_estimado: null,
        monto_ejecutado: null,
      }));
    }
    return clone as T;
  }

  private async ensureRequisicionesHabilitadas(
    tx: Pick<Tx, 'config_compras'> | Pick<PrismaService, 'config_compras'>,
    empresaId: string,
  ) {
    const config = await this.getOrCreateConfigCompras(tx, empresaId);
    if (!Boolean(config.habilitar_requisicion_compra)) {
      throw new BadRequestException(
        'La empresa tiene deshabilitadas las requisiciones en Configuración de Compras',
      );
    }
  }

  private async getOrCreateConfigCompras(
    tx: Pick<Tx, 'config_compras'> | Pick<PrismaService, 'config_compras'>,
    empresaId: string,
  ) {
    const existing = await tx.config_compras.findUnique({
      where: { empresa_id: empresaId },
    });
    if (existing) return existing;

    return tx.config_compras.create({
      data: {
        empresa_id: empresaId,
        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,
      },
    });
  }

  private async applyBudgetCommit(
    tx: Tx,
    req: Prisma.requisicion_cabGetPayload<{ include: { requisicion_det: true } }>,
  ) {
    const period = await this.resolvePeriodoByFecha(tx, req.empresa_id, req.fecha_requisicion || new Date());
    if (!period) {
      throw new BadRequestException('No existe período contable para la fecha de la requisición');
    }

    const grouped = new Map<string, number>();
    for (const det of req.requisicion_det) {
      if (!det.centro_costo_id) continue;
      const key = String(det.centro_costo_id);
      const prev = grouped.get(key) || 0;
      grouped.set(key, this.round(prev + this.toNumber(det.subtotal_estimado), 2));
    }

    const centrosCosto = await tx.cont_centros_costo.findMany({
      where: {
        empresa_id: req.empresa_id,
        id: { in: Array.from(grouped.keys()) },
      },
      select: { id: true, codigo: true, descripcion: true },
    });
    const centrosById = new Map<string, CentroCostoLite>(
      centrosCosto.map((centro) => [String(centro.id), centro]),
    );
    const periodoLabel = this.formatPeriodoLabel(period);

    for (const [centroCostoId, amount] of grouped.entries()) {
      const centroLabel = this.formatCentroCostoLabel(centrosById.get(String(centroCostoId)));
      const presupuesto = await tx.requisicion_presupuesto.findFirst({
        where: {
          empresa_id: req.empresa_id,
          periodo_id: period.id,
          centro_costo_id: centroCostoId,
          origen_tipo: req.origen_tipo,
          activo: true,
        },
      });

      if (!presupuesto) {
        throw new BadRequestException(
          `No hay presupuesto activo para el centro de costo "${centroLabel}" en el período ${periodoLabel}. ` +
            'Solución: solicitá al administrador que cargue el presupuesto de ese centro y período en la configuración de presupuestos de requisiciones.',
        );
      }

      const disponible = this.round(
        this.toNumber(presupuesto.monto_asignado) - this.toNumber(presupuesto.monto_comprometido),
        2,
      );
      if (amount - disponible > 0.0001) {
        throw new BadRequestException(
          `Presupuesto insuficiente para el centro de costo "${centroLabel}" en el período ${periodoLabel}. ` +
            `Disponible: ${this.formatMoney(disponible)}. ` +
            'Solución: aumentá el presupuesto asignado de ese centro/período o reducí el monto de la requisición.',
        );
      }
    }

    for (const [centroCostoId, amount] of grouped.entries()) {
      await tx.requisicion_presupuesto.updateMany({
        where: {
          empresa_id: req.empresa_id,
          periodo_id: period.id,
          centro_costo_id: centroCostoId,
          origen_tipo: req.origen_tipo,
          activo: true,
        },
        data: {
          monto_comprometido: { increment: amount },
          updated_at: new Date(),
        },
      });
    }
  }

  private async releaseBudgetCommit(
    tx: Tx,
    req: Prisma.requisicion_cabGetPayload<{ include: { requisicion_det: true } }>,
  ) {
    const period = await this.resolvePeriodoByFecha(tx, req.empresa_id, req.fecha_requisicion || new Date());
    if (!period) return;

    const grouped = new Map<string, number>();
    for (const det of req.requisicion_det) {
      if (!det.centro_costo_id) continue;
      const key = String(det.centro_costo_id);
      const prev = grouped.get(key) || 0;
      grouped.set(key, this.round(prev + this.toNumber(det.subtotal_estimado), 2));
    }

    for (const [centroCostoId, amount] of grouped.entries()) {
      const presupuesto = await tx.requisicion_presupuesto.findFirst({
        where: {
          empresa_id: req.empresa_id,
          periodo_id: period.id,
          centro_costo_id: centroCostoId,
          origen_tipo: req.origen_tipo,
          activo: true,
        },
      });
      if (!presupuesto) continue;

      const nextComprometido = Math.max(0, this.round(this.toNumber(presupuesto.monto_comprometido) - amount, 2));
      await tx.requisicion_presupuesto.update({
        where: { id: presupuesto.id },
        data: {
          monto_comprometido: nextComprometido,
          updated_at: new Date(),
        },
      });
    }
  }

  private async resolvePeriodoByFecha(tx: Tx, empresaId: string, fecha: Date) {
    return tx.cont_periodos.findFirst({
      where: {
        empresa_id: empresaId,
        fecha_inicio: { lte: fecha },
        fecha_fin: { gte: fecha },
      },
      orderBy: [{ anio: 'desc' }, { mes: 'desc' }],
      select: { id: true, anio: true, mes: true },
    });
  }

  private async validatePerfilEmpresa(tx: Tx, empresaId: string, perfilId: string) {
    const perfil = await tx.perfiles.findFirst({
      where: { id: perfilId, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
      select: { id: true },
    });
    if (!perfil) {
      throw new BadRequestException('El perfil de aprobación configurado no existe para la empresa');
    }
  }

  private async validatePresupuestoRefs(tx: Tx, empresaId: string, periodoId: string, centroCostoId: string) {
    const [periodo, centro] = await Promise.all([
      tx.cont_periodos.findFirst({
        where: { id: periodoId, empresa_id: empresaId },
        select: { id: true },
      }),
      tx.cont_centros_costo.findFirst({
        where: { id: centroCostoId, empresa_id: empresaId },
        select: { id: true },
      }),
    ]);

    if (!periodo) {
      throw new BadRequestException('El período de presupuesto seleccionado no existe para la empresa');
    }
    if (!centro) {
      throw new BadRequestException('El centro de costo del presupuesto no existe para la empresa');
    }
  }

  private formatPeriodoLabel(period: { anio: number; mes: number }) {
    const monthNames = [
      'enero',
      'febrero',
      'marzo',
      'abril',
      'mayo',
      'junio',
      'julio',
      'agosto',
      'septiembre',
      'octubre',
      'noviembre',
      'diciembre',
    ];
    const monthName = monthNames[Math.max(0, Math.min(11, Number(period.mes) - 1))] || `mes ${period.mes}`;
    return `${monthName} ${period.anio}`;
  }

  private formatCentroCostoLabel(centro: CentroCostoLite | undefined) {
    if (!centro) return `Centro no identificado`;
    const codigo = String(centro.codigo || '').trim();
    const descripcion = String(centro.descripcion || '').trim();
    if (codigo && descripcion) return `${codigo} - ${descripcion}`;
    return descripcion || codigo || `Centro no identificado`;
  }

  private formatMoney(value: number) {
    return `Gs. ${this.round(this.toNumber(value), 2).toLocaleString('es-PY', {
      minimumFractionDigits: 0,
      maximumFractionDigits: 2,
    })}`;
  }

  private async getNextNumeroRequisicion(tx: Tx, empresaId: string) {
    const year = new Date().getFullYear();
    const prefix = `REQ-${year}-`;
    const currentCount = await tx.requisicion_cab.count({
      where: {
        empresa_id: empresaId,
        numero_requisicion: { startsWith: prefix },
      },
    });
    const seq = String(currentCount + 1).padStart(6, '0');
    return `${prefix}${seq}`;
  }

  private async logHistorial(
    tx: Tx,
    requisicionId: string,
    input: {
      accion: string;
      estado_anterior: string | null;
      estado_nuevo: string | null;
      comentario?: string | null;
      usuario_id?: string | null;
      datos?: Prisma.InputJsonValue;
    },
  ) {
    await tx.requisicion_historial.create({
      data: {
        requisicion_id: requisicionId,
        accion: input.accion,
        estado_anterior: input.estado_anterior,
        estado_nuevo: input.estado_nuevo,
        comentario: input.comentario || null,
        usuario_id: input.usuario_id || null,
        datos: input.datos ?? Prisma.JsonNull,
      },
    });
  }

  private parseDateBoundary(value: string, endOfDay: boolean): Date {
    const trimmed = String(value || '').trim();
    if (!trimmed) return new Date();
    const hasTime = trimmed.includes('T');
    const date = hasTime
      ? new Date(trimmed)
      : new Date(`${trimmed}T${endOfDay ? '23:59:59.999' : '00:00:00.000'}Z`);
    if (Number.isNaN(date.getTime())) {
      throw new BadRequestException(`Fecha inválida: ${value}`);
    }
    return date;
  }

  private toNumber(value: unknown) {
    if (value == null) return 0;
    const num = Number(value);
    return Number.isFinite(num) ? num : 0;
  }

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