import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Transaction, type Updateable } from "../generated/kysely-tailordb";
import {
  DuplicateBarcodeError,
  UnitNotFoundError,
  ItemNotFoundError,
  NoFieldsToUpdateError,
  UomLockedError,
} from "../lib/errors.generated";
import { type PrimitivesQueries } from "../module";

export type UpdateItemInput = (
  | {
      id: string;
    }
  | {
      sku: string;
    }
) & {
  name?: string;
  barcode?: string | null;
  unitId?: string;
};

/**
 * Function: updateItem
 *
 * Updates mutable fields of an existing item.
 * The item can be looked up by id or sku. UoM can only be changed in DRAFT status.
 */
export async function run<CF extends Record<string, unknown>>(
  db: Transaction,
  input: UpdateItemInput & Omit<Partial<CF>, "status">,
  ctx: CommandContext,
  primitivesQueries: Pick<PrimitivesQueries, "getUnit">,
) {
  const { name, barcode, unitId } = input;

  const ITEM_OWN_KEYS = new Set(["id", "sku", "name", "barcode", "unitId"]);
  const customFields: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
    if (!ITEM_OWN_KEYS.has(key)) {
      customFields[key] = value;
    }
  }

  // 1. Check item exists (lookup by id or sku)
  const item =
    "id" in input
      ? await db
          .selectFrom("Item")
          .selectAll()
          .where("id", "=", (input as { id: string }).id)
          .forUpdate()
          .executeTakeFirst()
      : await db
          .selectFrom("Item")
          .selectAll()
          .where("sku", "=", (input as { sku: string }).sku)
          .forUpdate()
          .executeTakeFirst();

  if (!item) {
    const key = "id" in input ? (input as { id: string }).id : (input as { sku: string }).sku;
    return err(new ItemNotFoundError(key));
  }

  // 2. Check at least one field provided
  const hasName = name !== undefined;
  const hasBarcode = barcode !== undefined;
  const hasUnitId = unitId !== undefined;
  const hasCustomFields = Object.keys(customFields).length > 0;

  if (!hasName && !hasBarcode && !hasUnitId && !hasCustomFields) {
    return err(new NoFieldsToUpdateError(item.id));
  }

  // 3. Check barcode uniqueness when provided (non-null)
  if (hasBarcode && barcode !== null) {
    const existingBarcode = await db
      .selectFrom("Item")
      .selectAll()
      .where("barcode", "=", barcode)
      .where("id", "!=", item.id)
      .forUpdate()
      .executeTakeFirst();

    if (existingBarcode) {
      return err(new DuplicateBarcodeError(barcode));
    }
  }

  // 4. UoM can only be changed in DRAFT status
  if (hasUnitId) {
    if (item.status !== "DRAFT") {
      return err(new UomLockedError(item.id));
    }

    // Validate UoM exists and is active
    const { unit } = (await primitivesQueries.getUnit(db, { id: unitId }, ctx)).value;

    if (unit?.status !== "ACTIVE") {
      return err(new UnitNotFoundError(unitId));
    }
  }

  // 5. Apply updates
  const updates: Updateable<"Item"> = {
    ...(customFields as Updateable<"Item">),
  };
  if (hasName) updates.name = name;
  if (hasBarcode) updates.barcode = barcode;
  if (hasUnitId) updates.unitId = unitId;

  const updatedItem = await db
    .updateTable("Item")
    .set(updates)
    .where("id", "=", item.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ item: updatedItem });
}
