import { Injectable } from '@nestjs/common';
import { and, eq, or } from 'drizzle-orm';
import Decimal from 'decimal.js';
import { PackLevel } from '@wms/shared';
import { Tx } from '@/common/db/db.service';
import { skus, skuPacks, skuBarcodes, locations, warehouses, storers, lpns } from '@/db/schema';
import { NotFoundError, ValidationError, DomainError } from '@/common/errors/domain-error';
import { D } from '@/common/util';

export type SkuRow = typeof skus.$inferSelect;
export type LocationRow = typeof locations.$inferSelect;
export type PackRow = typeof skuPacks.$inferSelect;

/**
 * Resolution helpers shared by every operational module: barcode -> SKU,
 * code -> location, pack-level quantity -> base quantity, etc.
 * All functions take a transaction so they participate in the caller's unit of work.
 */
@Injectable()
export class LookupService {
  async warehouse(tx: Tx, warehouseId: string) {
    const [w] = await tx.select().from(warehouses).where(eq(warehouses.id, warehouseId)).limit(1);
    if (!w) throw new NotFoundError('Warehouse', warehouseId);
    return w;
  }

  async warehouseByCode(tx: Tx, code: string) {
    const [w] = await tx.select().from(warehouses).where(eq(warehouses.code, code)).limit(1);
    if (!w) throw new NotFoundError('Warehouse', code);
    return w;
  }

  async storer(tx: Tx, storerId: string) {
    const [s] = await tx.select().from(storers).where(eq(storers.id, storerId)).limit(1);
    if (!s) throw new NotFoundError('Storer', storerId);
    return s;
  }

  async storerByCode(tx: Tx, code: string) {
    const [s] = await tx.select().from(storers).where(eq(storers.code, code)).limit(1);
    if (!s) throw new NotFoundError('Storer', code);
    return s;
  }

  async sku(tx: Tx, skuId: string): Promise<SkuRow> {
    const [s] = await tx.select().from(skus).where(eq(skus.id, skuId)).limit(1);
    if (!s) throw new NotFoundError('SKU', skuId);
    return s;
  }

  async skuByCode(tx: Tx, storerId: string, code: string): Promise<SkuRow> {
    const [s] = await tx.select().from(skus).where(and(eq(skus.storerId, storerId), eq(skus.code, code))).limit(1);
    if (!s) throw new NotFoundError('SKU', code);
    return s;
  }

  /**
   * Resolves a scanned value to a SKU and pack level. Order of precedence:
   * sku_barcodes (explicit), sku_packs.barcode, then SKU code (optionally within a storer).
   */
  async skuByBarcode(tx: Tx, barcode: string, storerId?: string): Promise<{ sku: SkuRow; packLevel: PackLevel }> {
    const [b] = await tx.select().from(skuBarcodes).where(eq(skuBarcodes.barcode, barcode)).limit(1);
    if (b) {
      const sku = await this.sku(tx, b.skuId);
      if (storerId && sku.storerId !== storerId) throw new DomainError('WRONG_SKU', `Barcode ${barcode} belongs to another storer`);
      return { sku, packLevel: b.packLevel };
    }
    const [p] = await tx.select().from(skuPacks).where(eq(skuPacks.barcode, barcode)).limit(1);
    if (p) {
      const sku = await this.sku(tx, p.skuId);
      if (storerId && sku.storerId !== storerId) throw new DomainError('WRONG_SKU', `Barcode ${barcode} belongs to another storer`);
      return { sku, packLevel: p.level };
    }
    const rows = await tx.select().from(skus).where(storerId ? and(eq(skus.storerId, storerId), eq(skus.code, barcode)) : eq(skus.code, barcode)).limit(2);
    if (rows.length === 1) return { sku: rows[0], packLevel: 'EACH' };
    if (rows.length > 1) throw new ValidationError(`SKU code ${barcode} exists for several storers; scan a storer-specific barcode or specify the storer`);
    throw new NotFoundError('SKU/barcode', barcode);
  }

  async packs(tx: Tx, skuId: string): Promise<PackRow[]> {
    return tx.select().from(skuPacks).where(eq(skuPacks.skuId, skuId));
  }

  /** Converts a quantity expressed in packLevel units into base units. EACH = base. */
  async toBaseQty(tx: Tx, skuId: string, qtyInPack: Decimal | string | number, packLevel: PackLevel | undefined): Promise<{ baseQty: Decimal; qtyInBase: Decimal }> {
    const q = D(qtyInPack);
    if (!packLevel || packLevel === 'EACH') return { baseQty: q, qtyInBase: new Decimal(1) };
    const [p] = await tx.select().from(skuPacks).where(and(eq(skuPacks.skuId, skuId), eq(skuPacks.level, packLevel))).limit(1);
    if (!p) throw new ValidationError(`SKU has no ${packLevel} pack configuration`);
    return { baseQty: q.mul(p.qtyInBase), qtyInBase: p.qtyInBase };
  }

  async location(tx: Tx, locationId: string): Promise<LocationRow> {
    const [l] = await tx.select().from(locations).where(eq(locations.id, locationId)).limit(1);
    if (!l) throw new NotFoundError('Location', locationId);
    return l;
  }

  /** Accepts code, barcode or check digit within a warehouse. */
  async locationByCode(tx: Tx, warehouseId: string, code: string): Promise<LocationRow> {
    const [l] = await tx
      .select()
      .from(locations)
      .where(and(eq(locations.warehouseId, warehouseId), or(eq(locations.code, code), eq(locations.barcode, code), eq(locations.checkDigit, code))))
      .limit(1);
    if (!l) throw new NotFoundError('Location', code);
    return l;
  }

  async lpnByNumber(tx: Tx, lpnNumber: string) {
    const [l] = await tx.select().from(lpns).where(eq(lpns.lpnNumber, lpnNumber)).limit(1);
    return l ?? null;
  }

  /** Volume (m3) and weight (kg) for a base quantity of a SKU, when dimensions are known. */
  cubeAndWeight(sku: SkuRow, baseQty: Decimal): { cbm: Decimal | null; weightKg: Decimal | null } {
    let cbm: Decimal | null = sku.cbm ? D(sku.cbm).mul(baseQty) : null;
    if (!cbm && sku.lengthCm && sku.widthCm && sku.heightCm) {
      cbm = D(sku.lengthCm).mul(sku.widthCm).mul(sku.heightCm).div(1_000_000).mul(baseQty);
    }
    const weightKg = sku.weightKg ? D(sku.weightKg).mul(baseQty) : null;
    return { cbm, weightKg };
  }
}
