import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { OrdenesCompraService } from 'src/ordenes-compra/ordenes-compra.service';
import { AnularRecepcionCompraDto } from './dto/anular-recepcion-compra.dto';
import { CreateRecepcionCompraDto } from './dto/create-recepcion-compra.dto';

type Tx = Prisma.TransactionClient;

@Injectable()
export class RecepcionesCompraService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly ordenesCompraService: OrdenesCompraService,
    private readonly auditService: AuditService,
  ) {}

  async create(dto: CreateRecepcionCompraDto, empresaId: string, userId?: string) {
    this.validatePayload(dto, false);

    const createMeta = await this.prisma.$transaction(async (tx) => {
      const configCompras = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!(configCompras.habilitar_recepcion_compra ?? true)) {
        throw new BadRequestException(
          'La empresa tiene deshabilitada la Recepción de Compra en Configuración de Compras',
        );
      }
      const afectarStockEnRecepcion = !(configCompras.afectar_stock_en_compra_directa ?? true);
      const ordenCompraHabilitada = Boolean(configCompras.habilitar_orden_compra ?? true);

      if (!ordenCompraHabilitada) {
        return this.createRecepcionLibre(tx, dto, empresaId, userId, afectarStockEnRecepcion);
      }

      this.validatePayload(dto, true);
      const ordenCompraId = dto.cabecera.orden_compra_id;

      const orden = await tx.orden_compra_cab.findFirst({
        where: {
          id: ordenCompraId,
          empresa_id: empresaId,
          activo: true,
        },
        include: {
          orden_compra_det: {
            select: {
              id: true,
              producto_id: true,
              cantidad: true,
              cantidad_recibida: true,
            },
          },
        },
      });

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

      const estadoOrden = String(orden.estado || '').toLowerCase();
      if (estadoOrden === 'pendiente_aprobacion') {
        throw new BadRequestException('La orden de compra requiere aprobación antes de registrar recepción');
      }
      if (estadoOrden === 'cancelada') {
        throw new BadRequestException('No se puede recepcionar una orden cancelada');
      }
      if (estadoOrden === 'completada') {
        throw new BadRequestException('La orden de compra ya está completada');
      }

      const detallesMap = new Map(orden.orden_compra_det.map((det) => [det.id, det]));
      const cantidadesPorDetalle = new Map<string, number>();
      for (const det of dto.detalles) {
        const ordenDetId = det.orden_compra_det_id;
        const existing = cantidadesPorDetalle.get(ordenDetId) || 0;
        cantidadesPorDetalle.set(ordenDetId, this.round(existing + this.toNumber(det.cantidad_recibida), 4));
      }

      for (const [ordenDetId, cantidadRecepcion] of cantidadesPorDetalle.entries()) {
        const ordenDet = detallesMap.get(ordenDetId);
        if (!ordenDet) {
          throw new BadRequestException('Hay una línea de recepción que no pertenece a la orden seleccionada');
        }

        const cantidadOrdenada = this.toNumber(ordenDet.cantidad);
        const cantidadRecibida = this.toNumber(ordenDet.cantidad_recibida);
        const restante = this.round(cantidadOrdenada - cantidadRecibida, 4);
        if (cantidadRecepcion <= 0) {
          throw new BadRequestException('La cantidad recepcionada debe ser mayor a cero en cada línea');
        }
        if (cantidadRecepcion - restante > 0.0001) {
          throw new BadRequestException(
            `La recepción excede lo ordenado en una de las líneas. Restante permitido: ${restante}`,
          );
        }
      }

      const recepcion = await tx.recepcion_compra_cab.create({
        data: {
          empresa_id: empresaId,
          sucursal_id: orden.sucursal_id || null,
          deposito_id: afectarStockEnRecepcion
            ? await this.resolveDepositoRecepcion(
                tx,
                empresaId,
                dto.cabecera.deposito_id || null,
                orden.sucursal_id || null,
              )
            : null,
          proveedor_id: orden.proveedor_id,
          orden_compra_id: orden.id,
          usuario_id: userId || null,
          fecha_recepcion: dto.cabecera.fecha_recepcion ? new Date(dto.cabecera.fecha_recepcion) : new Date(),
          estado: 'aplicada',
          observaciones: dto.cabecera.observaciones || null,
          activo: true,
          anulado: false,
        },
      });

      for (const detalle of dto.detalles) {
        const ordenDetId = detalle.orden_compra_det_id;
        const ordenDet = detallesMap.get(ordenDetId);
        if (!ordenDet) {
          throw new BadRequestException('Hay una línea de recepción que no pertenece a la orden seleccionada');
        }

        const cantidad = this.round(this.toNumber(detalle.cantidad_recibida), 4);
        await tx.recepcion_compra_det.create({
          data: {
            recepcion_id: recepcion.id,
            orden_compra_det_id: ordenDetId,
            producto_id: ordenDet.producto_id || null,
            cantidad_recibida: cantidad,
            observaciones: detalle.observaciones || null,
          },
        });
      }

      for (const [ordenDetId, cantidadRecepcion] of cantidadesPorDetalle.entries()) {
        await tx.orden_compra_det.update({
          where: { id: ordenDetId },
          data: {
            cantidad_recibida: { increment: cantidadRecepcion },
            updated_at: new Date(),
          },
        });
      }

      if (afectarStockEnRecepcion && recepcion.deposito_id) {
        await this.aplicarStockRecepcion(tx, {
          empresaId,
          recepcionId: recepcion.id,
          depositoId: recepcion.deposito_id,
          detalles: dto.detalles.map((det) => ({
            producto_id: detallesMap.get(det.orden_compra_det_id)?.producto_id || null,
            cantidad_recibida: this.round(this.toNumber(det.cantidad_recibida), 4),
          })),
          usuarioId: userId,
        });
      }

      await this.ordenesCompraService.recalculateOrdenCompraEstado(tx, orden.id);
      return {
        recepcionId: recepcion.id,
        ordenCompraId: orden.id,
        numeroOrden: String(orden.numero_orden || orden.id || ''),
        libre: false,
      };
    });

    await this.logRecepcionHistorial({
      empresaId,
      userId: userId || null,
      recepcionId: createMeta.recepcionId,
      action: 'CREATE',
      descripcion: createMeta.ordenCompraId
        ? `Recepción registrada para OC ${createMeta.numeroOrden || createMeta.ordenCompraId}`
        : 'Recepción libre registrada sin orden de compra',
      newValue: {
        orden_compra_id: createMeta.ordenCompraId || null,
        libre: createMeta.libre,
      },
    });

    if (createMeta.ordenCompraId) {
      await this.logOrdenCompraHistorial({
        empresaId,
        userId: userId || null,
        ordenCompraId: createMeta.ordenCompraId,
        action: 'RECEPCION_CREATE',
        descripcion: `Recepción registrada para OC ${createMeta.numeroOrden || createMeta.ordenCompraId}`,
        newValue: { recepcion_id: createMeta.recepcionId },
      });
    }

    return this.findOne(createMeta.recepcionId, empresaId);
  }

  async update(id: string, dto: CreateRecepcionCompraDto, empresaId: string, userId?: string) {
    this.validatePayload(dto, false);

    const updateMeta = await this.prisma.$transaction(async (tx) => {
      const configCompras = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!(configCompras.habilitar_recepcion_compra ?? true)) {
        throw new BadRequestException(
          'La empresa tiene deshabilitada la Recepción de Compra en Configuración de Compras',
        );
      }
      const afectarStockEnRecepcion = !(configCompras.afectar_stock_en_compra_directa ?? true);

      const recepcion = await tx.recepcion_compra_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        include: {
          recepcion_compra_det: {
            select: {
              orden_compra_det_id: true,
              producto_id: true,
              cantidad_recibida: true,
            },
          },
        },
      });

      if (!recepcion) throw new NotFoundException('Recepción de compra no encontrada');
      if (recepcion.anulado || String(recepcion.estado || '').toLowerCase() === 'anulada') {
        throw new BadRequestException('No se puede editar una recepción anulada');
      }

      if (!recepcion.orden_compra_id) {
        return this.updateRecepcionLibre(tx, recepcion, dto, empresaId, userId, afectarStockEnRecepcion);
      }

      this.validatePayload(dto, true);
      if (dto.cabecera.orden_compra_id !== recepcion.orden_compra_id) {
        throw new BadRequestException('No se puede cambiar la orden de compra en una recepción existente');
      }
      if (recepcion.deposito_id && dto.cabecera.deposito_id && dto.cabecera.deposito_id !== recepcion.deposito_id) {
        throw new BadRequestException('No se puede cambiar el depósito de una recepción existente');
      }

      const orden = await tx.orden_compra_cab.findFirst({
        where: {
          id: recepcion.orden_compra_id,
          empresa_id: empresaId,
          activo: true,
        },
        include: {
          orden_compra_det: {
            select: {
              id: true,
              producto_id: true,
              cantidad: true,
              cantidad_recibida: true,
            },
          },
        },
      });

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

      const estadoOrden = String(orden.estado || '').toLowerCase();
      if (estadoOrden === 'pendiente_aprobacion') {
        throw new BadRequestException('La orden de compra requiere aprobación antes de registrar recepción');
      }
      if (estadoOrden === 'cancelada') {
        throw new BadRequestException('No se puede recepcionar una orden cancelada');
      }

      const detallesOrdenMap = new Map(orden.orden_compra_det.map((det) => [det.id, det]));
      const cantidadesOriginales = new Map<string, number>();
      for (const det of recepcion.recepcion_compra_det) {
        const ordenDetId = det.orden_compra_det_id;
        const prev = cantidadesOriginales.get(ordenDetId) || 0;
        cantidadesOriginales.set(ordenDetId, this.round(prev + this.toNumber(det.cantidad_recibida), 4));
      }

      const cantidadesNuevas = new Map<string, number>();
      for (const det of dto.detalles) {
        const ordenDetId = det.orden_compra_det_id;
        const cantidad = this.round(this.toNumber(det.cantidad_recibida), 4);
        if (cantidad <= 0) {
          throw new BadRequestException('La cantidad recepcionada debe ser mayor a cero en cada línea');
        }
        const prev = cantidadesNuevas.get(ordenDetId) || 0;
        cantidadesNuevas.set(ordenDetId, this.round(prev + cantidad, 4));
      }

      const detalleIds = Array.from(
        new Set([...Array.from(cantidadesOriginales.keys()), ...Array.from(cantidadesNuevas.keys())]),
      );
      const compraDetRows = detalleIds.length
        ? await tx.compra_det.findMany({
            where: {
              orden_compra_det_id: { in: detalleIds },
              compra_cab: {
                empresa_id: empresaId,
                activo: true,
                anulado: false,
              },
            },
            select: {
              orden_compra_det_id: true,
              cantidad: true,
            },
          })
        : [];
      const facturadaMap = new Map<string, number>();
      for (const row of compraDetRows) {
        const key = String(row.orden_compra_det_id || '');
        const prev = facturadaMap.get(key) || 0;
        facturadaMap.set(key, this.round(prev + this.toNumber(row.cantidad), 4));
      }

      for (const ordenDetId of detalleIds) {
        const ordenDet = detallesOrdenMap.get(ordenDetId);
        if (!ordenDet) {
          throw new BadRequestException('Hay una línea de recepción que no pertenece a la orden seleccionada');
        }
        const cantidadOrdenada = this.round(this.toNumber(ordenDet.cantidad), 4);
        const cantidadRecibidaActual = this.round(this.toNumber(ordenDet.cantidad_recibida), 4);
        const cantidadOriginal = this.round(this.toNumber(cantidadesOriginales.get(ordenDetId)), 4);
        const cantidadNueva = this.round(this.toNumber(cantidadesNuevas.get(ordenDetId)), 4);
        const nuevaCantidadRecibida = this.round(cantidadRecibidaActual - cantidadOriginal + cantidadNueva, 4);
        const facturada = this.round(this.toNumber(facturadaMap.get(ordenDetId)), 4);

        if (nuevaCantidadRecibida < -0.0001) {
          throw new BadRequestException('La cantidad resultante no puede ser negativa en una línea');
        }
        if (nuevaCantidadRecibida - cantidadOrdenada > 0.0001) {
          throw new BadRequestException(
            `La edición excede lo ordenado en una línea. Máximo permitido: ${cantidadOrdenada}`,
          );
        }
        if (facturada - nuevaCantidadRecibida > 0.0001) {
          throw new BadRequestException(
            `No se puede editar recepción: una línea ya fue facturada por ${this.round(facturada, 4)} unidad(es)`,
          );
        }
      }

      for (const ordenDetId of detalleIds) {
        const ordenDet = detallesOrdenMap.get(ordenDetId);
        const cantidadRecibidaActual = this.round(this.toNumber(ordenDet.cantidad_recibida), 4);
        const cantidadOriginal = this.round(this.toNumber(cantidadesOriginales.get(ordenDetId)), 4);
        const cantidadNueva = this.round(this.toNumber(cantidadesNuevas.get(ordenDetId)), 4);
        const nuevaCantidadRecibida = this.round(
          Math.max(cantidadRecibidaActual - cantidadOriginal + cantidadNueva, 0),
          4,
        );
        await tx.orden_compra_det.update({
          where: { id: ordenDetId },
          data: {
            cantidad_recibida: nuevaCantidadRecibida,
            updated_at: new Date(),
          },
        });
      }

      await tx.recepcion_compra_det.deleteMany({
        where: { recepcion_id: recepcion.id },
      });
      await tx.recepcion_compra_det.createMany({
        data: dto.detalles.map((det) => ({
          recepcion_id: recepcion.id,
          orden_compra_det_id: det.orden_compra_det_id,
          producto_id: detallesOrdenMap.get(det.orden_compra_det_id)?.producto_id || null,
          cantidad_recibida: this.round(this.toNumber(det.cantidad_recibida), 4),
          observaciones: det.observaciones || null,
        })),
      });

      await tx.recepcion_compra_cab.update({
        where: { id: recepcion.id },
        data: {
          fecha_recepcion: dto.cabecera.fecha_recepcion
            ? new Date(dto.cabecera.fecha_recepcion)
            : recepcion.fecha_recepcion,
          observaciones: dto.cabecera.observaciones || null,
          usuario_id: userId || recepcion.usuario_id,
          updated_at: new Date(),
        },
      });

      if (afectarStockEnRecepcion && recepcion.deposito_id) {
        await this.revertirStockRecepcion(tx, {
          recepcionId: recepcion.id,
          depositoId: recepcion.deposito_id,
          detalles: recepcion.recepcion_compra_det.map((det) => ({
            producto_id: det.producto_id || null,
            cantidad_recibida: this.round(this.toNumber(det.cantidad_recibida), 4),
          })),
        });
        await this.aplicarStockRecepcion(tx, {
          empresaId,
          recepcionId: recepcion.id,
          depositoId: recepcion.deposito_id,
          detalles: dto.detalles.map((det) => ({
            producto_id: detallesOrdenMap.get(det.orden_compra_det_id)?.producto_id || null,
            cantidad_recibida: this.round(this.toNumber(det.cantidad_recibida), 4),
          })),
          usuarioId: userId,
        });
      }

      await this.ordenesCompraService.recalculateOrdenCompraEstado(tx, orden.id);
      return {
        recepcionId: recepcion.id,
        ordenCompraId: orden.id,
        numeroOrden: String(orden.numero_orden || orden.id || ''),
        libre: false,
      };
    });

    await this.logRecepcionHistorial({
      empresaId,
      userId: userId || null,
      recepcionId: updateMeta.recepcionId,
      action: 'UPDATE',
      descripcion: updateMeta.ordenCompraId
        ? `Recepción actualizada para OC ${updateMeta.numeroOrden || updateMeta.ordenCompraId}`
        : 'Recepción libre actualizada',
      newValue: {
        orden_compra_id: updateMeta.ordenCompraId || null,
        libre: updateMeta.libre,
      },
    });

    if (updateMeta.ordenCompraId) {
      await this.logOrdenCompraHistorial({
        empresaId,
        userId: userId || null,
        ordenCompraId: updateMeta.ordenCompraId,
        action: 'RECEPCION_UPDATE',
        descripcion: `Recepción actualizada para OC ${updateMeta.numeroOrden || updateMeta.ordenCompraId}`,
        newValue: { recepcion_id: updateMeta.recepcionId },
      });
    }

    return this.findOne(updateMeta.recepcionId, empresaId);
  }

  async findAll(
    empresaId: string,
    query?: {
      page?: string;
      limit?: string;
      fecha_desde?: string;
      fecha_hasta?: string;
      proveedor_id?: string;
      orden_compra_id?: string;
      estado?: string;
      search?: 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.recepcion_compra_cabWhereInput = {
      empresa_id: empresaId,
      activo: true,
    };

    if (query?.proveedor_id) where.proveedor_id = query.proveedor_id;
    if (query?.orden_compra_id) where.orden_compra_id = query.orden_compra_id;
    if (query?.estado) where.estado = query.estado;
    if (query?.fecha_desde || query?.fecha_hasta) {
      where.fecha_recepcion = {};
      if (query?.fecha_desde) where.fecha_recepcion.gte = this.parseDateBoundary(query.fecha_desde, false);
      if (query?.fecha_hasta) where.fecha_recepcion.lte = this.parseDateBoundary(query.fecha_hasta, true);
    }
    if (query?.search?.trim()) {
      const q = query.search.trim();
      where.OR = [
        { orden_compra_cab: { numero_orden: { contains: q, mode: 'insensitive' } } },
        { observaciones: { contains: q, mode: 'insensitive' } },
        { proveedores: { personas: { razon_social: { contains: q, mode: 'insensitive' } } } },
        { proveedores: { personas: { ruc: { contains: q, mode: 'insensitive' } } } },
      ];
    }

    const [data, total] = await this.prisma.$transaction([
      this.prisma.recepcion_compra_cab.findMany({
        where,
        include: {
          orden_compra_cab: {
            select: {
              id: true,
              numero_orden: true,
              estado: true,
              fecha_emision: true,
            },
          },
          proveedores: {
            select: {
              id: true,
              persona_id: true,
              personas: { select: { razon_social: true, ruc: true, dv: true } },
            },
          },
          recepcion_compra_det: {
            select: {
              id: true,
              orden_compra_det_id: true,
              producto_id: true,
              cantidad_recibida: true,
            },
            orderBy: { created_at: 'asc' },
          },
        },
        orderBy: [{ fecha_recepcion: 'desc' }, { created_at: 'desc' }],
        skip,
        take,
      }),
      this.prisma.recepcion_compra_cab.count({ where }),
    ]);

    // KPIs sobre TODO el filtro (no la página): conteo por estado + total de líneas.
    const [estadoGroups, lineas] = await Promise.all([
      this.prisma.recepcion_compra_cab.groupBy({ by: ['estado'], where, _count: { _all: true } }),
      this.prisma.recepcion_compra_det.count({ where: { recepcion_compra_cab: where } }),
    ]);
    const stats = { total, aplicada: 0, anulada: 0, lineas };
    for (const g of estadoGroups) {
      const e = String(g.estado ?? '').toLowerCase();
      const c = g._count._all;
      if (e === 'aplicada') stats.aplicada = c;
      else if (e === 'anulada') stats.anulada = c;
    }

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

  async findOne(id: string, empresaId: string) {
    const recepcion = await this.prisma.recepcion_compra_cab.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        orden_compra_cab: {
          select: {
            id: true,
            numero_orden: true,
            estado: true,
            fecha_emision: true,
          },
        },
        proveedores: {
          select: {
            id: true,
            persona_id: true,
            personas: {
              select: {
                razon_social: true,
                ruc: true,
                dv: true,
                telefono: true,
                email: true,
              },
            },
          },
        },
        recepcion_compra_det: {
          include: {
            orden_compra_det: {
              select: {
                id: true,
                descripcion: true,
                cantidad: true,
                cantidad_recibida: true,
                precio_unitario: true,
              },
            },
            productos: {
              select: {
                id: true,
                descripcion: true,
                cod_producto: true,
              },
            },
          },
          orderBy: { created_at: 'asc' },
        },
      },
    });

    if (!recepcion) throw new NotFoundException('Recepción de compra no encontrada');
    return recepcion;
  }

  async anular(id: string, dto: AnularRecepcionCompraDto, empresaId: string, userId?: string) {
    const anularMeta = await this.prisma.$transaction(async (tx) => {
      const configCompras = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!(configCompras.habilitar_recepcion_compra ?? true)) {
        throw new BadRequestException(
          'La empresa tiene deshabilitada la Recepción de Compra en Configuración de Compras',
        );
      }

      const recepcion = await tx.recepcion_compra_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        include: {
          recepcion_compra_det: {
            select: {
              id: true,
              orden_compra_det_id: true,
              producto_id: true,
              cantidad_recibida: true,
            },
          },
          orden_compra_cab: {
            select: { id: true, estado: true, numero_orden: true },
          },
        },
      });

      if (!recepcion) throw new NotFoundException('Recepción de compra no encontrada');
      if (recepcion.anulado || String(recepcion.estado || '').toLowerCase() === 'anulada') {
        throw new BadRequestException('La recepción de compra ya está anulada');
      }
      if (!recepcion.orden_compra_cab?.id) {
        await tx.recepcion_compra_cab.update({
          where: { id: recepcion.id },
          data: {
            estado: 'anulada',
            anulado: true,
            activo: false,
            fecha_anulacion: new Date(),
            motivo_anulacion: dto?.motivo || null,
            updated_at: new Date(),
          },
        });

        if (recepcion.deposito_id) {
          await this.revertirStockRecepcion(tx, {
            recepcionId: recepcion.id,
            depositoId: recepcion.deposito_id,
            detalles: recepcion.recepcion_compra_det.map((det) => ({
              producto_id: det.producto_id || null,
              cantidad_recibida: this.round(this.toNumber(det.cantidad_recibida), 4),
            })),
          });
        }

        return {
          recepcionId: recepcion.id,
          ordenCompraId: null,
          numeroOrden: null,
          motivo: dto?.motivo || null,
          libre: true,
        };
      }

      const cantidadesRecepcion = new Map<string, number>();
      for (const det of recepcion.recepcion_compra_det) {
        if (!det.orden_compra_det_id) {
          throw new BadRequestException('La recepción vinculada contiene una línea sin detalle de orden');
        }
        const prev = cantidadesRecepcion.get(det.orden_compra_det_id) || 0;
        cantidadesRecepcion.set(det.orden_compra_det_id, this.round(prev + this.toNumber(det.cantidad_recibida), 4));
      }
      const detalleIds = Array.from(cantidadesRecepcion.keys());

      const [detallesOrden, compraDetRows] = await Promise.all([
        tx.orden_compra_det.findMany({
          where: { id: { in: detalleIds } },
          select: { id: true, cantidad_recibida: true },
        }),
        tx.compra_det.findMany({
          where: {
            orden_compra_det_id: { in: detalleIds },
            compra_cab: {
              empresa_id: empresaId,
              activo: true,
              anulado: false,
            },
          },
          select: {
            orden_compra_det_id: true,
            cantidad: true,
          },
        }),
      ]);

      const recibidaActualMap = new Map(detallesOrden.map((det) => [det.id, this.toNumber(det.cantidad_recibida)]));
      const facturadaMap = new Map<string, number>();
      for (const det of compraDetRows) {
        const key = String(det.orden_compra_det_id || '');
        const prev = facturadaMap.get(key) || 0;
        facturadaMap.set(key, this.round(prev + this.toNumber(det.cantidad), 4));
      }

      for (const [ordenDetId, cantidadRecepcion] of cantidadesRecepcion.entries()) {
        const recibidaActual = this.toNumber(recibidaActualMap.get(ordenDetId));
        const nuevaRecibida = this.round(Math.max(recibidaActual - cantidadRecepcion, 0), 4);
        const facturada = this.toNumber(facturadaMap.get(ordenDetId));
        if (facturada - nuevaRecibida > 0.0001) {
          throw new BadRequestException(
            `No se puede anular recepción: una línea ya fue facturada por ${this.round(facturada, 4)} unidad(es)`,
          );
        }
      }

      for (const [ordenDetId, cantidadRecepcion] of cantidadesRecepcion.entries()) {
        await tx.orden_compra_det.update({
          where: { id: ordenDetId },
          data: {
            cantidad_recibida: { decrement: cantidadRecepcion },
            updated_at: new Date(),
          },
        });
      }

      await tx.recepcion_compra_cab.update({
        where: { id: recepcion.id },
        data: {
          estado: 'anulada',
          anulado: true,
          activo: false,
          fecha_anulacion: new Date(),
          motivo_anulacion: dto?.motivo || null,
          updated_at: new Date(),
        },
      });

      if (recepcion.deposito_id) {
        await this.revertirStockRecepcion(tx, {
          recepcionId: recepcion.id,
          depositoId: recepcion.deposito_id,
          detalles: recepcion.recepcion_compra_det.map((det) => ({
            producto_id: det.producto_id || null,
            cantidad_recibida: this.round(this.toNumber(det.cantidad_recibida), 4),
          })),
        });
      }

      await this.ordenesCompraService.recalculateOrdenCompraEstado(tx, recepcion.orden_compra_cab.id);
      return {
        recepcionId: recepcion.id,
        ordenCompraId: recepcion.orden_compra_cab.id,
        numeroOrden: String(recepcion.orden_compra_cab.numero_orden || recepcion.orden_compra_cab.id || ''),
        motivo: dto?.motivo || null,
        libre: false,
      };
    });

    await this.logRecepcionHistorial({
      empresaId,
      userId: userId || null,
      recepcionId: anularMeta.recepcionId,
      action: 'CANCEL',
      descripcion: anularMeta.ordenCompraId
        ? anularMeta.motivo
          ? `Recepción anulada para OC ${anularMeta.numeroOrden || anularMeta.ordenCompraId}. Motivo: ${anularMeta.motivo}`
          : `Recepción anulada para OC ${anularMeta.numeroOrden || anularMeta.ordenCompraId}`
        : anularMeta.motivo
          ? `Recepción libre anulada. Motivo: ${anularMeta.motivo}`
          : 'Recepción libre anulada',
      oldValue: { estado: 'aplicada' },
      newValue: {
        estado: 'anulada',
        orden_compra_id: anularMeta.ordenCompraId || null,
        motivo: anularMeta.motivo || null,
        libre: anularMeta.libre,
      },
    });

    if (anularMeta.ordenCompraId) {
      await this.logOrdenCompraHistorial({
        empresaId,
        userId: userId || null,
        ordenCompraId: anularMeta.ordenCompraId,
        action: 'RECEPCION_CANCEL',
        descripcion: anularMeta.motivo
          ? `Recepción anulada para OC ${anularMeta.numeroOrden || anularMeta.ordenCompraId}. Motivo: ${anularMeta.motivo}`
          : `Recepción anulada para OC ${anularMeta.numeroOrden || anularMeta.ordenCompraId}`,
        newValue: { recepcion_id: anularMeta.recepcionId, motivo: anularMeta.motivo || null },
      });
    }

    return this.findOne(id, empresaId);
  }

  private async logOrdenCompraHistorial(input: {
    empresaId: string;
    userId?: string | null;
    ordenCompraId: string;
    action: string;
    descripcion: string;
    oldValue?: Record<string, unknown>;
    newValue?: Record<string, unknown>;
  }) {
    try {
      await this.auditService.log({
        empresa_id: input.empresaId,
        user_id: input.userId || null,
        action: input.action,
        entity_type: 'orden_compra',
        entity_id: input.ordenCompraId,
        descripcion: input.descripcion,
        old_value: input.oldValue,
        new_value: input.newValue,
      });
    } catch {
      // no-op: no bloquear flujo operativo por auditoría
    }
  }

  private async logRecepcionHistorial(input: {
    empresaId: string;
    userId?: string | null;
    recepcionId: string;
    action: string;
    descripcion: string;
    oldValue?: Record<string, unknown>;
    newValue?: Record<string, unknown>;
  }) {
    try {
      await this.auditService.log({
        empresa_id: input.empresaId,
        user_id: input.userId || null,
        action: input.action,
        entity_type: 'recepcion_compra',
        entity_id: input.recepcionId,
        descripcion: input.descripcion,
        old_value: input.oldValue,
        new_value: input.newValue,
      });
    } catch {
      // no-op: no bloquear flujo operativo por auditoría
    }
  }

  private async createRecepcionLibre(
    tx: Tx,
    dto: CreateRecepcionCompraDto,
    empresaId: string,
    userId: string | undefined,
    afectarStockEnRecepcion: boolean,
  ) {
    this.validateRecepcionLibrePayload(dto);

    const proveedor = await tx.proveedores.findFirst({
      where: {
        id: dto.cabecera.proveedor_id,
        empresa_id: empresaId,
        activo: true,
        tipo_entidad: { not: 'ACREEDOR_VARIO' },
      },
      select: { id: true },
    });
    if (!proveedor) {
      throw new BadRequestException('proveedor_id inválido para la empresa');
    }

    const deposito = await tx.depositos.findFirst({
      where: {
        id: dto.cabecera.deposito_id,
        empresa_id: empresaId,
        active: true,
      },
      select: { id: true, sucursal_id: true },
    });
    if (!deposito) {
      throw new BadRequestException('deposito_id inválido para la empresa');
    }

    const productoIds = Array.from(
      new Set(dto.detalles.map((detalle) => String(detalle.producto_id || '')).filter(Boolean)),
    );
    const productos = await tx.productos.findMany({
      where: {
        id: { in: productoIds },
        empresa_id: empresaId,
        active: true,
        deleted: false,
      },
      select: { id: true },
    });
    const productosValidos = new Set(productos.map((producto) => producto.id));
    if (productoIds.some((productoId) => !productosValidos.has(productoId))) {
      throw new BadRequestException('Hay productos inválidos o inactivos en la recepción');
    }

    const recepcion = await tx.recepcion_compra_cab.create({
      data: {
        empresa_id: empresaId,
        sucursal_id: deposito.sucursal_id,
        deposito_id: deposito.id,
        proveedor_id: proveedor.id,
        orden_compra_id: null,
        usuario_id: userId || null,
        fecha_recepcion: dto.cabecera.fecha_recepcion ? new Date(dto.cabecera.fecha_recepcion) : new Date(),
        estado: 'aplicada',
        observaciones: dto.cabecera.observaciones || null,
        activo: true,
        anulado: false,
      },
    });

    await tx.recepcion_compra_det.createMany({
      data: dto.detalles.map((detalle) => ({
        recepcion_id: recepcion.id,
        orden_compra_det_id: null,
        producto_id: detalle.producto_id,
        cantidad_recibida: this.round(this.toNumber(detalle.cantidad_recibida), 4),
        observaciones: detalle.observaciones || null,
      })),
    });

    if (afectarStockEnRecepcion) {
      await this.aplicarStockRecepcion(tx, {
        empresaId,
        recepcionId: recepcion.id,
        depositoId: deposito.id,
        detalles: dto.detalles.map((detalle) => ({
          producto_id: detalle.producto_id,
          cantidad_recibida: this.round(this.toNumber(detalle.cantidad_recibida), 4),
        })),
        usuarioId: userId,
      });
    }

    return {
      recepcionId: recepcion.id,
      ordenCompraId: null,
      numeroOrden: null,
      libre: true,
    };
  }

  private async updateRecepcionLibre(
    tx: Tx,
    recepcion: any,
    dto: CreateRecepcionCompraDto,
    empresaId: string,
    userId: string | undefined,
    afectarStockEnRecepcion: boolean,
  ) {
    this.validateRecepcionLibrePayload(dto);

    const proveedor = await tx.proveedores.findFirst({
      where: {
        id: dto.cabecera.proveedor_id,
        empresa_id: empresaId,
        activo: true,
        tipo_entidad: { not: 'ACREEDOR_VARIO' },
      },
      select: { id: true },
    });
    if (!proveedor) {
      throw new BadRequestException('proveedor_id inválido para la empresa');
    }

    const deposito = await tx.depositos.findFirst({
      where: {
        id: dto.cabecera.deposito_id,
        empresa_id: empresaId,
        active: true,
      },
      select: { id: true, sucursal_id: true },
    });
    if (!deposito) {
      throw new BadRequestException('deposito_id inválido para la empresa');
    }

    const productoIds = Array.from(
      new Set(dto.detalles.map((detalle) => String(detalle.producto_id || '')).filter(Boolean)),
    );
    const productos = await tx.productos.findMany({
      where: {
        id: { in: productoIds },
        empresa_id: empresaId,
        active: true,
        deleted: false,
      },
      select: { id: true },
    });
    const productosValidos = new Set(productos.map((producto) => producto.id));
    if (productoIds.some((productoId) => !productosValidos.has(productoId))) {
      throw new BadRequestException('Hay productos inválidos o inactivos en la recepción');
    }

    if (recepcion.deposito_id) {
      await this.revertirStockRecepcion(tx, {
        recepcionId: recepcion.id,
        depositoId: recepcion.deposito_id,
        detalles: recepcion.recepcion_compra_det.map((detalle: any) => ({
          producto_id: detalle.producto_id || null,
          cantidad_recibida: this.round(this.toNumber(detalle.cantidad_recibida), 4),
        })),
      });
    }

    await tx.recepcion_compra_det.deleteMany({ where: { recepcion_id: recepcion.id } });
    await tx.recepcion_compra_det.createMany({
      data: dto.detalles.map((detalle) => ({
        recepcion_id: recepcion.id,
        orden_compra_det_id: null,
        producto_id: detalle.producto_id,
        cantidad_recibida: this.round(this.toNumber(detalle.cantidad_recibida), 4),
        observaciones: detalle.observaciones || null,
      })),
    });

    await tx.recepcion_compra_cab.update({
      where: { id: recepcion.id },
      data: {
        proveedor_id: proveedor.id,
        sucursal_id: deposito.sucursal_id,
        deposito_id: deposito.id,
        fecha_recepcion: dto.cabecera.fecha_recepcion
          ? new Date(dto.cabecera.fecha_recepcion)
          : recepcion.fecha_recepcion,
        observaciones: dto.cabecera.observaciones || null,
        usuario_id: userId || recepcion.usuario_id,
        updated_at: new Date(),
      },
    });

    if (afectarStockEnRecepcion) {
      await this.aplicarStockRecepcion(tx, {
        empresaId,
        recepcionId: recepcion.id,
        depositoId: deposito.id,
        detalles: dto.detalles.map((detalle) => ({
          producto_id: detalle.producto_id,
          cantidad_recibida: this.round(this.toNumber(detalle.cantidad_recibida), 4),
        })),
        usuarioId: userId,
      });
    }

    return {
      recepcionId: recepcion.id,
      ordenCompraId: null,
      numeroOrden: null,
      libre: true,
    };
  }

  private validatePayload(dto: CreateRecepcionCompraDto, requiereOrdenCompra = true) {
    if (requiereOrdenCompra && !dto?.cabecera?.orden_compra_id) {
      throw new BadRequestException('cabecera.orden_compra_id es obligatorio');
    }
    if (!dto?.detalles?.length) {
      throw new BadRequestException('Debe enviar al menos un detalle de recepción');
    }
    for (const detalle of dto.detalles) {
      if (this.toNumber(detalle.cantidad_recibida) <= 0) {
        throw new BadRequestException('La cantidad recepcionada debe ser mayor a cero en cada línea');
      }
      if (requiereOrdenCompra && !detalle.orden_compra_det_id) {
        throw new BadRequestException('orden_compra_det_id es obligatorio en cada línea');
      }
    }
  }

  private validateRecepcionLibrePayload(dto: CreateRecepcionCompraDto) {
    this.validatePayload(dto, false);
    if (dto.cabecera.orden_compra_id) {
      throw new BadRequestException('La empresa no utiliza Orden de Compra; registrá la recepción como libre');
    }
    if (!dto.cabecera.proveedor_id) {
      throw new BadRequestException('cabecera.proveedor_id es obligatorio para una recepción libre');
    }
    if (!dto.cabecera.deposito_id) {
      throw new BadRequestException('cabecera.deposito_id es obligatorio para una recepción libre');
    }
    if (dto.detalles.some((detalle) => !detalle.producto_id)) {
      throw new BadRequestException('producto_id es obligatorio en cada línea de una recepción libre');
    }
  }

  private async getOrCreateConfigCompras(tx: Tx, 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 resolveDepositoRecepcion(
    tx: Tx,
    empresaId: string,
    depositoId: string | null,
    sucursalId: string | null,
  ) {
    if (depositoId) {
      const deposito = await tx.depositos.findFirst({
        where: { id: depositoId, empresa_id: empresaId, active: true },
        select: { id: true },
      });
      if (!deposito) {
        throw new BadRequestException('deposito_id inválido para la empresa');
      }
      return deposito.id;
    }

    if (!sucursalId) {
      throw new BadRequestException('No se pudo resolver el depósito de recepción. Informá cabecera.deposito_id');
    }

    const principal = await tx.depositos.findFirst({
      where: {
        empresa_id: empresaId,
        sucursal_id: sucursalId,
        active: true,
        es_principal: true,
      },
      select: { id: true },
      orderBy: { created_at: 'asc' },
    });
    if (principal) return principal.id;

    const fallback = await tx.depositos.findFirst({
      where: { empresa_id: empresaId, sucursal_id: sucursalId, active: true },
      select: { id: true },
      orderBy: { created_at: 'asc' },
    });
    if (!fallback) {
      throw new BadRequestException(
        'No existe depósito activo para la sucursal de la orden. Configurá un depósito o enviá cabecera.deposito_id',
      );
    }
    return fallback.id;
  }

  private async aplicarStockRecepcion(
    tx: Tx,
    params: {
      empresaId: string;
      recepcionId: string;
      depositoId: string;
      detalles: Array<{ producto_id: string | null; cantidad_recibida: number }>;
      usuarioId?: string;
    },
  ) {
    const tipoMovimientoId = await this.resolveTipoMovimientoEntradaId(tx);

    const cantidadesPorProducto = new Map<string, number>();
    for (const det of params.detalles) {
      const productoId = det.producto_id;
      if (!productoId) continue;
      const prev = cantidadesPorProducto.get(productoId) || 0;
      cantidadesPorProducto.set(productoId, this.round(prev + this.toNumber(det.cantidad_recibida), 4));
    }

    const productoIds = Array.from(cantidadesPorProducto.keys());
    if (!productoIds.length) return;

    const productos = await tx.productos.findMany({
      where: { id: { in: productoIds } },
      select: { id: true, maneja_inventario: true },
    });
    const inventarioByProducto = new Map(productos.map((prod) => [prod.id, Boolean(prod.maneja_inventario)]));

    for (const [productoId, cantidad] of cantidadesPorProducto.entries()) {
      if (cantidad <= 0) continue;
      if (!inventarioByProducto.get(productoId)) continue;

      await tx.stock_deposito.upsert({
        where: {
          deposito_id_producto_id: {
            deposito_id: params.depositoId,
            producto_id: productoId,
          },
        },
        create: {
          deposito_id: params.depositoId,
          producto_id: productoId,
          cantidad_disponible: cantidad,
          cantidad_reservada: 0,
          stock_minimo: 0,
        },
        update: {
          cantidad_disponible: { increment: cantidad },
          updated_at: new Date(),
        },
      });

      await tx.movimientos_inventario.create({
        data: {
          deposito_origen_id: params.depositoId,
          producto_id: productoId,
          tipo_movimiento_id: tipoMovimientoId,
          cantidad,
          documento_origen: 'recepcion_compra',
          documento_id: params.recepcionId,
          observaciones: 'Ingreso por recepción de compra',
          usuario_id: params.usuarioId || null,
        },
      });
    }
  }

  private async revertirStockRecepcion(
    tx: Tx,
    params: {
      recepcionId: string;
      depositoId: string;
      detalles: Array<{ producto_id: string | null; cantidad_recibida: number }>;
    },
  ) {
    const tieneMovimientos = await tx.movimientos_inventario.count({
      where: {
        documento_id: params.recepcionId,
        documento_origen: 'recepcion_compra',
      },
    });
    if (tieneMovimientos === 0) return;

    const tipoMovimientoId = await this.resolveTipoMovimientoSalidaId(tx);
    const cantidadesPorProducto = new Map<string, number>();
    for (const det of params.detalles) {
      if (!det.producto_id) continue;
      const prev = cantidadesPorProducto.get(det.producto_id) || 0;
      cantidadesPorProducto.set(det.producto_id, this.round(prev + this.toNumber(det.cantidad_recibida), 4));
    }

    for (const [productoId, cantidad] of cantidadesPorProducto.entries()) {
      if (cantidad <= 0) continue;
      await tx.stock_deposito.upsert({
        where: {
          deposito_id_producto_id: {
            deposito_id: params.depositoId,
            producto_id: productoId,
          },
        },
        create: {
          deposito_id: params.depositoId,
          producto_id: productoId,
          cantidad_disponible: -Math.abs(cantidad),
          cantidad_reservada: 0,
          stock_minimo: 0,
        },
        update: {
          cantidad_disponible: { decrement: Math.abs(cantidad) },
          updated_at: new Date(),
        },
      });

      await tx.movimientos_inventario.create({
        data: {
          deposito_origen_id: params.depositoId,
          producto_id: productoId,
          tipo_movimiento_id: tipoMovimientoId,
          cantidad: Math.abs(cantidad),
          documento_origen: 'recepcion_compra_reversa',
          documento_id: params.recepcionId,
          observaciones: 'Reversión de recepción de compra',
        },
      });
    }
  }

  private async resolveTipoMovimientoEntradaId(tx: Tx) {
    const tipo = await tx.tipo_movimiento_inventario.findFirst({
      where: {
        OR: [
          { codigo: 'COMPRA' },
          { codigo: 'ENTRADA' },
          { descripcion: { contains: 'Compra', mode: 'insensitive' } },
          { descripcion: { contains: 'Entrada', mode: 'insensitive' } },
        ],
      },
      select: { id: true },
    });

    if (!tipo) throw new NotFoundException('No se encontró tipo de movimiento de inventario de entrada/compra');
    return tipo.id;
  }

  private async resolveTipoMovimientoSalidaId(tx: Tx) {
    const tipo = await tx.tipo_movimiento_inventario.findFirst({
      where: {
        OR: [
          { codigo: 'SALIDA' },
          { descripcion: { contains: 'Salida', mode: 'insensitive' } },
          { descripcion: { contains: 'Ajuste', mode: 'insensitive' } },
        ],
      },
      select: { id: true },
    });

    if (!tipo) throw new NotFoundException('No se encontró tipo de movimiento de inventario de salida');
    return tipo.id;
  }

  private parseDateBoundary(value: string, endOfDay: boolean): Date {
    const parts = String(value || '')
      .split('-')
      .map(Number);
    if (parts.length === 3 && parts.every((part) => Number.isFinite(part))) {
      const [year, month, day] = parts;
      return endOfDay ? new Date(year, month - 1, day, 23, 59, 59, 999) : new Date(year, month - 1, day, 0, 0, 0, 0);
    }
    const parsed = new Date(value);
    if (Number.isNaN(parsed.getTime())) {
      return endOfDay ? new Date(9999, 0, 1, 23, 59, 59, 999) : new Date(1970, 0, 1);
    }
    if (endOfDay) parsed.setHours(23, 59, 59, 999);
    else parsed.setHours(0, 0, 0, 0);
    return parsed;
  }

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

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