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

export interface CreateStockAdjustmentLineInput {
  itemId: string;
  storageLocationId: string;
  quantity: string;
  adjustmentDirection?: "INCREASE" | "DECREASE" | null;
  fromStockCategory?: "AVAILABLE" | "BLOCKED" | null;
  lotId?: string | null;
  serialNumberId?: string | null;
  unitCost?: string | null;
}

export interface CreateStockAdjustmentInput {
  adjustmentType: "CORRECTION" | "SCRAP" | "BLOCK" | "UNBLOCK";
  reasonCode: string;
  lines: CreateStockAdjustmentLineInput[];
}

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

const VALID_ADJUSTMENT_TYPES = ["CORRECTION", "SCRAP", "BLOCK", "UNBLOCK"];

/**
 * Function: createStockAdjustment
 *
 * Creates a new stock adjustment document in DRAFT status.
 * Stock adjustments are used to correct inventory discrepancies,
 * scrap damaged goods, or block/unblock stock for quality holds.
 */
export async function run(
  db: Transaction,
  input: CreateStockAdjustmentInput,
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { adjustmentType, reasonCode, lines } = input;

  // 1. Validate adjustmentType
  if (!VALID_ADJUSTMENT_TYPES.includes(adjustmentType)) {
    return err(new InvalidAdjustmentTypeError(adjustmentType));
  }

  // 2. Validate reasonCode
  if (!reasonCode || reasonCode.trim() === "") {
    return err(new ReasonCodeRequiredError("reasonCode"));
  }

  // 3. Validate at least one line
  if (!lines || lines.length === 0) {
    return err(new EmptyAdjustmentLinesError("lines"));
  }

  // 4. Validate each line
  for (const line of lines) {
    // quantity > 0
    if (new Decimal(line.quantity).lte(0)) {
      return err(new InvalidQuantityError(line.quantity));
    }

    // item must exist and be ACTIVE
    const { item } = (await itemManagementQueries.getItem(db, { id: line.itemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(line.itemId));
    }
    if (item.status !== "ACTIVE") {
      return err(new ItemNotActiveError(line.itemId));
    }

    // storage location must exist
    const location = await db
      .selectFrom("StorageLocation")
      .selectAll()
      .where("id", "=", line.storageLocationId)
      .executeTakeFirst();

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

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

    // fromStockCategory only valid for SCRAP type
    if (line.fromStockCategory && adjustmentType !== "SCRAP") {
      return err(new InvalidFromStockCategoryError(line.itemId));
    }

    // lotId and serialNumberId are optional. The caller (application layer) is
    // responsible for providing them when the item is lot-tracked or serial-tracked.
    // When provided, we validate the referenced record exists.
    if (line.lotId) {
      const lot = await db
        .selectFrom("Lot")
        .selectAll()
        .where("id", "=", line.lotId)
        .executeTakeFirst();
      if (!lot) {
        return err(new LotNotFoundError(line.lotId));
      }
    }

    if (line.serialNumberId) {
      const serial = await db
        .selectFrom("SerialNumber")
        .selectAll()
        .where("id", "=", line.serialNumberId)
        .executeTakeFirst();
      if (!serial) {
        return err(new SerialNotFoundError(line.serialNumberId));
      }
    }
  }

  // 5. Insert StockAdjustment
  const now = new Date();
  const stockAdjustment = await db
    .insertInto("StockAdjustment")
    .values({
      adjustmentType,
      status: "DRAFT",
      reasonCode,
      rejectionReason: null,
      createdAt: now,
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  // 6. Insert StockAdjustmentLines
  const stockAdjustmentLines = [];
  for (const line of lines) {
    const adjustmentLine = await db
      .insertInto("StockAdjustmentLine")
      .values({
        stockAdjustmentId: stockAdjustment.id,
        itemId: line.itemId,
        storageLocationId: line.storageLocationId,
        quantity: line.quantity,
        adjustmentDirection: line.adjustmentDirection ?? null,
        fromStockCategory: line.fromStockCategory ?? null,
        unitCost: line.unitCost ?? null,
        lotId: line.lotId ?? null,
        serialNumberId: line.serialNumberId ?? null,
        createdAt: now,
        updatedAt: null,
      })
      .returningAll()
      .executeTakeFirstOrThrow();

    stockAdjustmentLines.push(adjustmentLine);
  }

  return ok({ stockAdjustment, stockAdjustmentLines });
}
