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

export interface StockAdjustmentLineInput {
  itemId: string;
  storageLocationId: string;
  quantity: string;
  adjustmentDirection?: StockAdjustmentLineAdjustmentDirection | null;
  fromStockCategory?: StockAdjustmentLineFromStockCategory | null;
  lotId?: string | null;
  serialNumberId?: string | null;
  unitCost?: string | null;
}

export interface StockAdjustmentHeaderInput {
  adjustmentType: StockAdjustmentAdjustmentType;
  reasonCode: string;
  adjustmentDate: Date;
}

export interface CreateStockAdjustmentInput {
  header: StockAdjustmentHeaderInput;
  lines: StockAdjustmentLineInput[];
}

/**
 * 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<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: {
    header: StockAdjustmentHeaderInput & CF;
    lines: (StockAdjustmentLineInput & LCF)[];
  },
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { header, lines } = input;
  const { adjustmentType, reasonCode, adjustmentDate } = header;

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

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

  // 3. 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));
      }
    }
  }

  // 4. Insert StockAdjustment
  const stockAdjustment = await db
    .insertInto("StockAdjustment")
    .values({
      // Header input first (incl. custom fields) so command-set columns always win.
      ...(header as Record<string, unknown>),
      adjustmentType,
      status: "DRAFT",
      reasonCode,
      adjustmentDate,
      rejectionReason: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  // 5. Insert StockAdjustmentLines
  await db
    .insertInto("StockAdjustmentLine")
    .values(
      lines.map((line) => {
        const {
          itemId,
          storageLocationId,
          quantity,
          adjustmentDirection,
          fromStockCategory,
          unitCost,
          lotId,
          serialNumberId,
          ...lineCustomFields
        } = line;
        return {
          ...lineCustomFields,
          stockAdjustmentId: stockAdjustment.id,
          itemId,
          storageLocationId,
          quantity,
          adjustmentDirection: adjustmentDirection ?? null,
          fromStockCategory: fromStockCategory ?? null,
          unitCost: unitCost ?? null,
          lotId: lotId ?? null,
          serialNumberId: serialNumberId ?? null,
        };
      }),
    )
    .execute();

  return ok({ stockAdjustment });
}
