import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';

type Tx = Prisma.TransactionClient;

export type FifoConsumedLote = {
  lote_id: string;
  numero_lote: string;
  cantidad: number;
  costo_unitario: number;
  costo_total: number;
  fecha_vencimiento: Date | null;
};

export type FifoResult = {
  lotes_consumidos: FifoConsumedLote[];
  cantidad_total: number;
  costo_total: number;
  costo_unitario_promedio: number;
  /**
   * Cantidad que NO pudo cubrirse con lotes. > 0 cuando el producto tiene stock
   * cargado que no pertenece a ningún lote (típico al activar lotes sobre un
   * catálogo con stock previo, o si una remisión/ajuste descontó stock sin tocar
   * lotes). El llamador decide qué hacer: hoy se costea el faltante con
   * `productos.precio_costo` en vez de bloquear la venta.
   */
  cantidad_sin_lote: number;
};

@Injectable()
export class FifoService {
  async deductFifo(tx: Tx, empresaId: string, productoId: string, depositoId: string, cantidad: number): Promise<FifoResult> {
    const qty = this.toNumber(cantidad);
    if (qty <= 0) {
      return {
        lotes_consumidos: [],
        cantidad_total: 0,
        costo_total: 0,
        costo_unitario_promedio: 0,
        cantidad_sin_lote: 0,
      };
    }

    const sources = await tx.stock_lote.findMany({
      where: {
        deposito_id: depositoId,
        active: true,
        cantidad_disponible: { gt: 0 },
        lotes_producto: {
          empresa_id: empresaId,
          producto_id: productoId,
          activo: true,
          cantidad_disponible: { gt: 0 },
        },
      },
      include: {
        lotes_producto: {
          select: {
            id: true,
            numero_lote: true,
            costo_unitario: true,
            cantidad_disponible: true,
            fecha_vencimiento: true,
            fecha_fabricacion: true,
            created_at: true,
          },
        },
      },
      orderBy: [
        { lotes_producto: { fecha_fabricacion: 'asc' } },
        { lotes_producto: { created_at: 'asc' } },
        { created_at: 'asc' },
      ],
    });

    let remaining = qty;
    const consumed: FifoConsumedLote[] = [];

    for (const source of sources) {
      if (remaining <= 0.0001) break;

      const availableStock = this.toNumber(source.cantidad_disponible);
      const availableLote = this.toNumber(source.lotes_producto.cantidad_disponible);
      const available = Math.min(availableStock, availableLote);
      if (available <= 0) continue;

      const consumeQty = Math.min(remaining, available);
      const costoUnitario = this.toNumber(source.lotes_producto.costo_unitario);
      const costoTotal = this.round(costoUnitario * consumeQty, 4);

      const nextStockQty = this.round(availableStock - consumeQty, 4);
      const nextLoteQty = this.round(availableLote - consumeQty, 4);

      await tx.stock_lote.update({
        where: { id: source.id },
        data: {
          cantidad_disponible: nextStockQty,
          active: nextStockQty > 0,
          updated_at: new Date(),
        },
      });

      await tx.lotes_producto.update({
        where: { id: source.lote_id },
        data: {
          cantidad_disponible: nextLoteQty,
          activo: nextLoteQty > 0,
          updated_at: new Date(),
        },
      });

      consumed.push({
        lote_id: source.lote_id,
        numero_lote: source.lotes_producto.numero_lote,
        cantidad: this.round(consumeQty, 4),
        costo_unitario: this.round(costoUnitario, 4),
        costo_total: costoTotal,
        fecha_vencimiento: source.lotes_producto.fecha_vencimiento ?? null,
      });

      remaining = this.round(remaining - consumeQty, 4);
    }

    // Lo que no se pudo cubrir con lotes NO bloquea la venta: se informa al
    // llamador para que lo costee con `productos.precio_costo`. Bloquear obligaría
    // a migrar todo el stock previo a lotes antes de activar el módulo, y dejaría
    // el POS trabado ante cualquier desfase entre `stock_deposito` y los lotes.
    const cantidadSinLote = remaining > 0.0001 ? this.round(remaining, 4) : 0;

    const costoTotal = this.round(consumed.reduce((acc, row) => acc + row.costo_total, 0), 4);
    const cantidadConLote = this.round(qty - cantidadSinLote, 4);
    const costoUnitarioPromedio = cantidadConLote > 0 ? this.round(costoTotal / cantidadConLote, 4) : 0;

    return {
      lotes_consumidos: consumed,
      cantidad_total: cantidadConLote,
      costo_total: costoTotal,
      costo_unitario_promedio: costoUnitarioPromedio,
      cantidad_sin_lote: cantidadSinLote,
    };
  }

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

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