import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  ItemNotFoundError,
  ItemNotActiveError,
  StorageLocationNotFoundError,
  StorageLocationNotActiveError,
  InvalidQuantityError,
  InvalidUnitCostError,
  LotNotFoundError,
  SerialNotFoundError,
} from "../lib/errors.generated";
import { applyValuation } from "../lib/valuation";

export interface ReceiveStockWithInspectionInput {
  itemId: string;
  storageLocationId: string;
  quantity: string;
  unitCost: string;
  sourceId: string;
  lotId?: string | null;
  serialNumberId?: string | null;
}

export interface ItemManagementQueries {
  getItem: (
    db: Transaction,
    input: { id: string },
    ctx: CommandContext,
  ) => Promise<{
    ok: true;
    value: { item: { id: string; status: string } | null };
  }>;
}

/**
 * Function: receiveStockWithInspection
 *
 * Records a quality-inspection (SAP QI Stock) receipt. Creates three
 * InventoryLedger entries in one transaction (GOODS_RECEIPT/DR + BLOCK/CR
 * + BLOCK/DR), placing stock directly in the blocked state. Updates StockLevel
 * (onHand += qty, blocked += qty, incoming -= qty) so available does not
 * change — inspection stock cannot be issued until resolved via a subsequent
 * StockAdjustment (UNBLOCK/SCRAP) or issueStock(fromBlocked=true).
 *
 * For standard trust-on-receipt, use receiveStock.
 */
export async function run(
  db: Transaction,
  input: ReceiveStockWithInspectionInput,
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { itemId, storageLocationId, quantity, unitCost, sourceId, lotId, serialNumberId } = input;

  // 1. Validate item exists and is ACTIVE
  const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value;

  if (!item) {
    return err(new ItemNotFoundError(itemId));
  }

  if (item.status !== "ACTIVE") {
    return err(new ItemNotActiveError(itemId));
  }

  // 2. Validate storage location exists and is ACTIVE
  const location = await db
    .selectFrom("StorageLocation")
    .selectAll()
    .where("id", "=", storageLocationId)
    .executeTakeFirst();

  if (!location) {
    return err(new StorageLocationNotFoundError(storageLocationId));
  }

  if (location.status !== "ACTIVE") {
    return err(new StorageLocationNotActiveError(storageLocationId));
  }

  // 3. Validate quantity > 0 and unitCost > 0
  if (new Decimal(quantity).lte(0)) {
    return err(new InvalidQuantityError(quantity));
  }

  if (new Decimal(unitCost).lte(0)) {
    return err(new InvalidUnitCostError(unitCost));
  }

  // 4. Validate lot if provided
  if (lotId) {
    const lot = await db.selectFrom("Lot").selectAll().where("id", "=", lotId).executeTakeFirst();

    if (!lot) {
      return err(new LotNotFoundError(lotId));
    }
  }

  // 5. Validate serial if provided
  let serial: { id: string; status: string } | undefined;
  if (serialNumberId) {
    serial = (await db
      .selectFrom("SerialNumber")
      .selectAll()
      .where("id", "=", serialNumberId)
      .executeTakeFirst()) as { id: string; status: string } | undefined;

    if (!serial) {
      return err(new SerialNotFoundError(serialNumberId));
    }
  }

  // 6. Create 3 InventoryLedger entries: GOODS_RECEIPT/DR + BLOCK/CR + BLOCK/DR
  const now = new Date();
  const amount = new Decimal(quantity).mul(unitCost).toString();

  const ledgerEntry1 = await db
    .insertInto("InventoryLedger")
    .values({
      sourceType: "GOODS_RECEIPT",
      sourceId,
      dcIndicator: "DR",
      itemId,
      storageLocationId,
      quantity,
      amount,
      executedAt: now,
      lotId: lotId ?? null,
      serialNumberId: serialNumberId ?? null,
      createdAt: now,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  const ledgerEntry2 = await db
    .insertInto("InventoryLedger")
    .values({
      sourceType: "BLOCK",
      sourceId,
      dcIndicator: "CR",
      itemId,
      storageLocationId,
      quantity,
      amount: null,
      executedAt: now,
      lotId: lotId ?? null,
      serialNumberId: serialNumberId ?? null,
      createdAt: now,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  const ledgerEntry3 = await db
    .insertInto("InventoryLedger")
    .values({
      sourceType: "BLOCK",
      sourceId,
      dcIndicator: "DR",
      itemId,
      storageLocationId,
      quantity,
      amount: null,
      executedAt: now,
      lotId: lotId ?? null,
      serialNumberId: serialNumberId ?? null,
      createdAt: now,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  const inventoryLedgerEntries = [ledgerEntry1, ledgerEntry2, ledgerEntry3];

  // 7. Upsert StockLevel (onHand += qty, blocked += qty, incoming -= qty)
  const existingStockLevel = await db
    .selectFrom("StockLevel")
    .selectAll()
    .where("itemId", "=", itemId)
    .where("storageLocationId", "=", storageLocationId)
    .forUpdate()
    .executeTakeFirst();

  if (existingStockLevel) {
    const newIncoming = Decimal.max(
      0,
      new Decimal(existingStockLevel.incoming).minus(quantity),
    ).toString();
    await db
      .updateTable("StockLevel")
      .set({
        onHand: new Decimal(existingStockLevel.onHand).plus(quantity).toString(),
        incoming: newIncoming,
        blocked: new Decimal(existingStockLevel.blocked).plus(quantity).toString(),
        updatedAt: now,
      })
      .where("id", "=", existingStockLevel.id)
      .execute();
  } else {
    await db
      .insertInto("StockLevel")
      .values({
        itemId,
        storageLocationId,
        onHand: quantity,
        reserved: "0",
        incoming: "0",
        blocked: quantity,
        createdAt: now,
        updatedAt: null,
      })
      .execute();
  }

  // 8. Apply valuation (DR) — inspection stock is still valued as asset
  await applyValuation(db, {
    itemId,
    inventoryLedgerId: ledgerEntry1.id,
    dcIndicator: "DR",
    quantity,
    unitCost,
  });

  // 9. Update LotStockLevel if lotId provided
  if (lotId) {
    const existingLotStockLevel = await db
      .selectFrom("LotStockLevel")
      .selectAll()
      .where("lotId", "=", lotId)
      .where("storageLocationId", "=", storageLocationId)
      .forUpdate()
      .executeTakeFirst();

    if (existingLotStockLevel) {
      await db
        .updateTable("LotStockLevel")
        .set({
          onHand: new Decimal(existingLotStockLevel.onHand).plus(quantity).toString(),
          blocked: new Decimal(existingLotStockLevel.blocked).plus(quantity).toString(),
          updatedAt: now,
        })
        .where("id", "=", existingLotStockLevel.id)
        .execute();
    } else {
      await db
        .insertInto("LotStockLevel")
        .values({
          lotId,
          storageLocationId,
          onHand: quantity,
          blocked: quantity,
          createdAt: now,
          updatedAt: null,
        })
        .execute();
    }
  }

  // 10. Update SerialNumber state if serial provided
  // The inspection hold is represented at the StockLevel layer only;
  // serial number state itself is NOT blocked — standard transitions apply.
  if (serial) {
    let newStatus: "AVAILABLE" | "RETURNED" = "AVAILABLE";
    if (serial.status === "ISSUED") {
      newStatus = "RETURNED";
    } else if (serial.status === "RETURNED") {
      newStatus = "AVAILABLE";
    }

    await db
      .updateTable("SerialNumber")
      .set({
        status: newStatus,
        updatedAt: now,
      })
      .where("id", "=", serial.id)
      .execute();
  }

  return ok({ inventoryLedgerEntries });
}
