import { Injectable, Logger } from '@nestjs/common';
import { and, eq, inArray, sql } from 'drizzle-orm';
import { ConfirmTaskDto } from '@wms/shared';
import { DbService, Tx } from '@/common/db/db.service';
import { skuSlottings, inventoryBalances, locations, tasks, skuPacks } from '@/db/schema';
import { requireRequestContext } from '@/common/context/request-context';
import { DomainError, ValidationError, ErrorCodes } from '@/common/errors/domain-error';
import { D, assertWarehouseAccess, serialize } from '@/common/util';
import { LookupService } from '@/modules/master-data/lookup.service';
import { LedgerService } from '@/modules/inventory/ledger.service';
import { PutawayService } from '@/modules/inventory/putaway.service';
import { TasksService, TaskRow, TaskConfirmResult } from '@/modules/tasks/tasks.service';

/**
 * Min/max replenishment of forward pick faces from reserve stock.
 * A pick face below its minimum gets REPLENISH tasks (FIFO/FEFO from reserve,
 * whole replenishment packs where possible) up to its maximum. Runs on a
 * schedule and on demand; de-duplicates against open tasks.
 */
@Injectable()
export class ReplenishmentService {
  private readonly logger = new Logger(ReplenishmentService.name);

  constructor(
    private readonly db: DbService,
    private readonly lookup: LookupService,
    private readonly ledger: LedgerService,
    private readonly putaway: PutawayService,
    private readonly tasksService: TasksService,
  ) {
    this.tasksService.registerHandler('REPLENISH', (tx, task, dto) => this.confirm(tx, task, dto));
  }

  run(warehouseId: string) {
    assertWarehouseAccess(warehouseId);
    return this.db.run((tx) => this.runInTx(tx, warehouseId));
  }

  async runInTx(tx: Tx, warehouseId: string): Promise<{ created: number; skipped: number; details: unknown[] }> {
    const slots = await tx.select().from(skuSlottings).where(and(eq(skuSlottings.warehouseId, warehouseId), eq(skuSlottings.active, true)));
    let created = 0;
    let skipped = 0;
    const details: unknown[] = [];
    for (const slot of slots) {
      const { current, gap } = await this.putaway.pickFaceGap(tx, slot);
      if (current.gte(slot.minQty)) continue;
      const [{ pending }] = await tx.select({ pending: sql<string>`coalesce(sum(${tasks.qty} - ${tasks.qtyDone}),0)` }).from(tasks).where(and(eq(tasks.type, 'REPLENISH'), eq(tasks.toLocationId, slot.pickLocationId), eq(tasks.skuId, slot.skuId), inArray(tasks.status, ['PENDING', 'ASSIGNED', 'IN_PROGRESS'])));
      let need = gap.minus(pending);
      if (need.lte(0)) {
        skipped++;
        continue;
      }
      const sku = await this.lookup.sku(tx, slot.skuId);
      const [pack] = await tx.select().from(skuPacks).where(and(eq(skuPacks.skuId, slot.skuId), eq(skuPacks.level, slot.replenishPackLevel))).limit(1);
      const unit = pack ? D(pack.qtyInBase) : D(1);
      // reserve stock, oldest first, not already allocated
      const sources = await tx
        .select({ b: inventoryBalances })
        .from(inventoryBalances)
        .innerJoin(locations, eq(locations.id, inventoryBalances.locationId))
        .where(and(eq(inventoryBalances.warehouseId, warehouseId), eq(inventoryBalances.skuId, slot.skuId), eq(inventoryBalances.status, 'AVAILABLE'), eq(locations.usage, 'RESERVE'), sql`${inventoryBalances.qtyOnHand} > ${inventoryBalances.qtyAllocated}`))
        .orderBy(sku.rotationRule === 'FEFO' ? sql`${inventoryBalances.expiryDate} nulls last` : inventoryBalances.firstReceivedAt);
      for (const src of sources) {
        if (need.lte(0)) break;
        const free = D(src.b.qtyOnHand).minus(src.b.qtyAllocated);
        let take = free.lt(need) ? free : need;
        // round down to whole replenishment packs when the source allows, but never below one pack if free covers it
        if (unit.gt(1)) {
          const packs = take.div(unit).floor();
          take = packs.gt(0) ? packs.mul(unit) : free.gte(unit) ? unit : take;
        }
        if (take.lte(0)) continue;
        await this.ledger.reserve(tx, src.b.id, take);
        const t = await this.tasksService.create(tx, {
          warehouseId, type: 'REPLENISH', priority: current.isZero() ? 2 : 4, storerId: src.b.storerId, skuId: slot.skuId, qty: take, packLevel: slot.replenishPackLevel,
          fromLocationId: src.b.locationId, toLocationId: slot.pickLocationId, lpnId: src.b.lpnId, lotNumber: src.b.lotNumber, expiryDate: src.b.expiryDate,
          refType: 'SLOTTING', refId: slot.id, data: { balanceId: src.b.id, min: slot.minQty.toString(), max: slot.maxQty.toString(), current: current.toString() },
        });
        details.push({ taskNumber: t.taskNumber, skuId: slot.skuId, qty: take.toString(), from: src.b.locationId, to: slot.pickLocationId });
        need = need.minus(take);
        created++;
      }
      if (need.gt(0)) details.push({ skuId: slot.skuId, unfilled: need.toString(), reason: 'No reserve stock' });
    }
    return serialize({ created, skipped, details });
  }

  /** REPLENISH task handler: move reserved quantity from reserve to the pick face; reservation released on arrival. */
  private async confirm(tx: Tx, task: TaskRow, dto: ConfirmTaskDto): Promise<TaskConfirmResult> {
    const ctx = requireRequestContext();
    const sku = await this.lookup.sku(tx, task.skuId!);
    const fromLoc = await this.lookup.location(tx, task.fromLocationId!);
    const toLoc = await this.lookup.location(tx, task.toLocationId!);
    if (dto.fromLocationCode && ![fromLoc.code, fromLoc.barcode, fromLoc.checkDigit].includes(dto.fromLocationCode)) throw new DomainError(ErrorCodes.WRONG_LOCATION, `Expected ${fromLoc.code}`);
    if (dto.toLocationCode && ![toLoc.code, toLoc.barcode, toLoc.checkDigit].includes(dto.toLocationCode)) throw new DomainError(ErrorCodes.WRONG_LOCATION, `Expected destination ${toLoc.code}`);
    const remaining = D(task.qty ?? 0).minus(task.qtyDone);
    const qty = dto.qty ? (await this.lookup.toBaseQty(tx, sku.id, dto.qty, dto.packLevel ?? task.packLevel ?? 'EACH')).baseQty : remaining;
    if (qty.gt(remaining) || qty.lte(0)) throw new ValidationError(`Quantity must be between 0 and ${remaining}`);
    const data = task.data as { balanceId?: string };
    if (data.balanceId) await this.ledger.release(tx, data.balanceId, qty); // release the reservation, then move
    await this.ledger.post(tx, {
      type: 'REPLENISH', qty, warehouseId: task.warehouseId, storerId: task.storerId!, skuId: sku.id,
      from: { locationId: fromLoc.id, lpnId: task.lpnId, status: 'AVAILABLE' }, to: { locationId: toLoc.id, lpnId: null, status: 'AVAILABLE' },
      lotNumber: task.lotNumber ?? '', expiryDate: task.expiryDate, taskId: task.id, refType: 'TASK', refId: task.id, idempotencyKey: dto.idempotencyKey,
    });
    const done = D(task.qtyDone).plus(qty);
    const completed = done.gte(D(task.qty ?? 0));
    const [row] = await tx.update(tasks).set({ qtyDone: done, status: completed ? 'COMPLETED' : 'IN_PROGRESS', completedAt: completed ? new Date() : null, completedById: completed ? ctx.userId : null }).where(eq(tasks.id, task.id)).returning();
    return { task: row, completed, message: completed ? `Replenished ${done} to ${toLoc.code}` : `Partial ${done}/${task.qty}` };
  }


}
