import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { stockReservationLifecycle } from "../db/stockReservation.lifecycle.generated";
import type { StockReservationSourceType } from "../generated/enums";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  calculateSiteAvailableExcludingReservation,
  getOpenStockReservationQuantity,
} from "../internal/stockReservation";
import {
  InsufficientAvailableStockError,
  InvalidReservationQuantitiesError,
  ItemNotFoundError,
  SiteNotFoundError,
  StockReservationNotFoundError,
  StockReservationNotOpenError,
  StorageLocationNotFoundError,
} from "../lib/errors.generated";
import type { ItemManagementQueries, OrganizationQueries } from "../module";

export interface UpdateStockReservationInput {
  id: string;
  sourceType?: StockReservationSourceType;
  sourceLineId?: string;
  sourceId?: string;
  itemId?: string;
  siteId?: string;
  storageLocationId?: string | null;
  reservedQuantity?: string;
  requiredDate?: Date;
}

export async function run(
  db: Transaction,
  input: UpdateStockReservationInput,
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
  organizationQueries: Pick<OrganizationQueries, "getSite">,
) {
  const {
    sourceType,
    sourceLineId,
    sourceId,
    itemId,
    siteId,
    storageLocationId,
    reservedQuantity: inputReservedQuantity,
    requiredDate,
  } = input;

  const existing = await db
    .selectFrom("StockReservation")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();

  if (!existing) {
    return err(new StockReservationNotFoundError(input.id));
  }

  const nextStatus = stockReservationLifecycle.tryTransition(existing.status, "update");
  if (!nextStatus) {
    return err(new StockReservationNotOpenError(existing.id));
  }

  const updateSet: Record<string, unknown> = { status: nextStatus };

  if (sourceId !== undefined) {
    updateSet.sourceId = sourceId;
  }
  if (sourceType !== undefined) {
    updateSet.sourceType = sourceType;
  }
  if (sourceLineId !== undefined) {
    updateSet.sourceLineId = sourceLineId;
  }

  const effectiveItemId = itemId ?? existing.itemId;
  if (itemId !== undefined) {
    const { item } = (await itemManagementQueries.getItem(db, { id: effectiveItemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(effectiveItemId));
    }
    updateSet.itemId = itemId;
  }

  const effectiveSiteId = siteId ?? existing.siteId;
  if (siteId !== undefined) {
    const { site } = (await organizationQueries.getSite(db, { id: siteId }, ctx)).value;
    if (!site) {
      return err(new SiteNotFoundError(siteId));
    }
    updateSet.siteId = siteId;
  }

  const effectiveStorageLocationId =
    storageLocationId !== undefined ? storageLocationId : existing.storageLocationId;
  if ((siteId !== undefined || storageLocationId !== undefined) && effectiveStorageLocationId) {
    const location = await db
      .selectFrom("StorageLocation")
      .selectAll()
      .where("id", "=", effectiveStorageLocationId)
      .where("siteId", "=", effectiveSiteId)
      .executeTakeFirst();

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

  if (storageLocationId !== undefined) {
    updateSet.storageLocationId = storageLocationId;
  }

  const effectiveReservedQuantity = inputReservedQuantity ?? existing.reservedQuantity;
  const reservedQuantity = new Decimal(effectiveReservedQuantity);
  const consumedQuantity = new Decimal(existing.consumedQuantity);
  if (reservedQuantity.lt(0) || reservedQuantity.lt(consumedQuantity)) {
    return err(new InvalidReservationQuantitiesError(effectiveReservedQuantity));
  }

  const openReservedQuantity = getOpenStockReservationQuantity({
    reservedQuantity: effectiveReservedQuantity,
    consumedQuantity: existing.consumedQuantity,
  });
  const available = await calculateSiteAvailableExcludingReservation(
    db,
    effectiveItemId,
    effectiveSiteId,
    existing.id,
  );
  if (!available || available.lt(openReservedQuantity)) {
    return err(new InsufficientAvailableStockError(`${effectiveItemId}:${effectiveSiteId}`));
  }

  if (inputReservedQuantity !== undefined) {
    updateSet.reservedQuantity = inputReservedQuantity;
  }
  if (requiredDate !== undefined) {
    updateSet.requiredDate = requiredDate;
  }

  const stockReservation = await db
    .updateTable("StockReservation")
    .set(updateSet)
    .where("id", "=", existing.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ stockReservation });
}
