import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { inventoryExecutionLifecycle } from "../db/inventoryExecution.lifecycle.generated";
import type { Insertable, Transaction, Updateable } from "../generated/kysely-tailordb";
import {
  EmptyIssueLinesError,
  InvalidQuantityError,
  InvalidStatusError,
  InvalidUnitConversionRateError,
  ItemNotFoundError,
  LotNotFoundError,
  PrimaryUnitMismatchError,
  SerialNotFoundError,
  StorageLocationNotFoundError,
} from "../lib/errors.generated";
import type { ItemManagementQueries } from "../module";
import type { CreateGoodsIssueLineInput } from "./createGoodsIssue";

export interface UpdateGoodsIssueInput {
  id: string;
  effectiveDate?: Date;
  lines?: CreateGoodsIssueLineInput[];
}

export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: Omit<UpdateGoodsIssueInput, "lines"> &
    Partial<CF> & {
      lines?: (CreateGoodsIssueLineInput & LCF)[];
    },
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { id, effectiveDate, lines, ...customFields } = input;

  const inventoryExecution = await db
    .selectFrom("InventoryExecution")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();

  if (!inventoryExecution || inventoryExecution.executionType !== "GOODS_ISSUE") {
    return err(new InvalidStatusError(id));
  }
  if (!inventoryExecutionLifecycle.tryTransition(inventoryExecution.status, "update")) {
    return err(new InvalidStatusError(id));
  }

  const updates: Updateable<"InventoryExecution"> = {
    ...(customFields as Updateable<"InventoryExecution">),
  };
  if (effectiveDate !== undefined) updates.effectiveDate = effectiveDate;

  const updatedInventoryExecution =
    Object.keys(updates).length === 0
      ? inventoryExecution
      : await db
          .updateTable("InventoryExecution")
          .set(updates)
          .where("id", "=", id)
          .returningAll()
          .executeTakeFirstOrThrow();

  if (lines === undefined) {
    return ok({ inventoryExecution: updatedInventoryExecution });
  }
  if (lines.length === 0) {
    return err(new EmptyIssueLinesError(id));
  }

  const insertValues: Insertable<"InventoryExecutionLine">[] = [];
  for (const line of lines) {
    const {
      itemId,
      storageLocationId,
      quantity,
      unitId,
      primaryQuantity,
      primaryUnitId,
      unitConversionRate,
      sourceLineId,
      lotId,
      serialNumberId,
      ...lineCustomFields
    } = line;

    if (new Decimal(quantity).lte(0)) {
      return err(new InvalidQuantityError(quantity));
    }
    if (new Decimal(primaryQuantity).lte(0)) {
      return err(new InvalidQuantityError(primaryQuantity));
    }
    if (new Decimal(unitConversionRate).lte(0)) {
      return err(new InvalidUnitConversionRateError(unitConversionRate));
    }

    const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(itemId));
    }
    if (primaryUnitId !== item.unitId) {
      return err(new PrimaryUnitMismatchError(`${primaryUnitId} expected ${item.unitId}`));
    }

    const location = await db
      .selectFrom("StorageLocation")
      .selectAll()
      .where("id", "=", storageLocationId)
      .executeTakeFirst();
    if (!location) {
      return err(new StorageLocationNotFoundError(storageLocationId));
    }

    if (lotId) {
      const lot = await db.selectFrom("Lot").selectAll().where("id", "=", lotId).executeTakeFirst();
      if (!lot) return err(new LotNotFoundError(lotId));
    }

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

    insertValues.push({
      ...lineCustomFields,
      inventoryExecutionId: id,
      direction: "OUT",
      itemId,
      quantity,
      unitId,
      primaryQuantity,
      primaryUnitId,
      unitConversionRate,
      unitCost: null,
      storageLocationId,
      stockType: "AVAILABLE",
      sourceLineId: sourceLineId ?? null,
      lotId: lotId ?? null,
      serialNumberId: serialNumberId ?? null,
    });
  }

  await db.deleteFrom("InventoryExecutionLine").where("inventoryExecutionId", "=", id).execute();
  const inventoryExecutionLines = await db
    .insertInto("InventoryExecutionLine")
    .values(insertValues)
    .returningAll()
    .execute();

  return ok({ inventoryExecution: updatedInventoryExecution, inventoryExecutionLines });
}
