import { err, ok } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { stockReservationLifecycle } from "../db/stockReservation.lifecycle.generated";
import type { Selectable, Transaction } from "../generated/kysely-tailordb";
import {
  InsufficientStockError,
  InvalidReservationQuantitiesError,
  StockReservationNotFoundError,
  StockReservationNotOpenError,
} from "../lib/errors.generated";

export interface StockReservationQuantity {
  reservedQuantity: string;
  consumedQuantity: string;
}

export interface ConsumeStockReservationInternalInput {
  id: string;
  consumedQuantity: string;
}

export interface ConsumeGoodsIssueReservationsInternalInput {
  itemId: string;
  siteId: string;
  storageLocationId: string;
  stockType: "AVAILABLE" | "BLOCKED" | "IN_TRANSIT";
  quantity: string;
  sourceDocumentType: string | null;
  sourceDocumentId: string | null;
  sourceLineId: string | null;
}

export function getOpenStockReservationQuantity(reservation: StockReservationQuantity) {
  return Decimal.max(
    new Decimal(reservation.reservedQuantity).minus(reservation.consumedQuantity),
    0,
  );
}

export function sumOpenStockReservationQuantity(reservations: readonly StockReservationQuantity[]) {
  return reservations.reduce(
    (total, reservation) => total.plus(getOpenStockReservationQuantity(reservation)),
    new Decimal(0),
  );
}

export async function lockSiteAvailableStockLevels(
  db: Transaction,
  itemId: string,
  siteId: string,
) {
  const locationIds = await db
    .selectFrom("StorageLocation")
    .select("id")
    .where("siteId", "=", siteId)
    .where("status", "=", "ACTIVE")
    .execute();

  if (locationIds.length === 0) {
    return null;
  }

  const stockLevelsResult = await db
    .selectFrom("StockLevel")
    .selectAll()
    .where("itemId", "=", itemId)
    .where(
      "storageLocationId",
      "in",
      locationIds.map((location) => location.id),
    )
    .where("stockType", "=", "AVAILABLE")
    .orderBy("storageLocationId", "asc")
    .forUpdate()
    .execute();
  const stockLevels = Array.isArray(stockLevelsResult) ? stockLevelsResult : [];

  return { locationIds, stockLevels };
}

export async function calculateSiteAvailableExcludingReservation(
  db: Transaction,
  itemId: string,
  siteId: string,
  excludeReservationId?: string,
) {
  const lockedStockLevels = await lockSiteAvailableStockLevels(db, itemId, siteId);

  if (!lockedStockLevels) {
    return null;
  }

  const siteAvailableStock = lockedStockLevels.stockLevels.reduce(
    (total, stockLevel) => total.plus(stockLevel.quantity),
    new Decimal(0),
  );

  const openReservations = await db
    .selectFrom("StockReservation")
    .selectAll()
    .where("itemId", "=", itemId)
    .where("siteId", "=", siteId)
    .where("status", "=", "OPEN")
    .execute();

  const reservedByOtherReservations = openReservations.reduce(
    (total, reservation) =>
      reservation.id === excludeReservationId
        ? total
        : total.plus(getOpenStockReservationQuantity(reservation)),
    new Decimal(0),
  );

  return siteAvailableStock.minus(reservedByOtherReservations);
}

export async function consumeStockReservationInternal(
  db: Transaction,
  input: ConsumeStockReservationInternalInput,
) {
  const consumedQuantity = new Decimal(input.consumedQuantity);
  if (consumedQuantity.lte(0)) {
    return err(new InvalidReservationQuantitiesError(input.consumedQuantity));
  }

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

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

  if (consumedQuantity.gt(getOpenStockReservationQuantity(existing))) {
    return err(new InvalidReservationQuantitiesError(input.consumedQuantity));
  }

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

  const stockReservation = await db
    .updateTable("StockReservation")
    .set({
      consumedQuantity: new Decimal(existing.consumedQuantity).plus(consumedQuantity).toString(),
      status: nextStatus,
    })
    .where("id", "=", existing.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ stockReservation });
}

export async function consumeGoodsIssueReservationsInternal(
  db: Transaction,
  input: ConsumeGoodsIssueReservationsInternalInput,
) {
  const lockedSiteStockLevels = await lockSiteAvailableStockLevels(db, input.itemId, input.siteId);
  const siteStockLevels = lockedSiteStockLevels?.stockLevels ?? [];
  const stockLevel = siteStockLevels.find(
    (siteStockLevel) =>
      siteStockLevel.storageLocationId === input.storageLocationId &&
      siteStockLevel.stockType === input.stockType,
  );
  if (!stockLevel) {
    return err(new InsufficientStockError(`${input.itemId}:${input.storageLocationId}`));
  }

  const locationStockQuantity = new Decimal(stockLevel.quantity);
  if (locationStockQuantity.lt(input.quantity)) {
    return err(new InsufficientStockError(`${input.itemId}:${input.storageLocationId}`));
  }

  const openReservations = await db
    .selectFrom("StockReservation")
    .selectAll()
    .where("itemId", "=", input.itemId)
    .where("siteId", "=", input.siteId)
    .where("status", "=", "OPEN")
    .orderBy("requiredDate", "asc")
    .forUpdate()
    .execute();
  const matchingReservations =
    input.sourceDocumentType && input.sourceDocumentId && input.sourceLineId
      ? openReservations.filter(
          (reservation) =>
            reservation.sourceType === input.sourceDocumentType &&
            reservation.sourceId === input.sourceDocumentId &&
            reservation.sourceLineId === input.sourceLineId &&
            (reservation.storageLocationId === null ||
              reservation.storageLocationId === input.storageLocationId),
        )
      : [];
  const matchingReservationIds = new Set(matchingReservations.map((reservation) => reservation.id));
  const reservedByOtherDemand = sumOpenStockReservationQuantity(
    openReservations.filter((reservation) => !matchingReservationIds.has(reservation.id)),
  );
  const sitePhysicalAvailable = siteStockLevels.reduce(
    (total, siteStockLevel) => total.plus(siteStockLevel.quantity),
    new Decimal(0),
  );
  if (sitePhysicalAvailable.minus(reservedByOtherDemand).lt(input.quantity)) {
    return err(new InsufficientStockError(`${input.itemId}:${input.siteId}`));
  }

  let remainingReservationConsumption = new Decimal(input.quantity);
  for (const reservation of matchingReservations) {
    if (remainingReservationConsumption.lte(0)) break;

    const openQuantity = getOpenStockReservationQuantity(reservation);
    const consumedQuantity = Decimal.min(openQuantity, remainingReservationConsumption);
    if (consumedQuantity.lte(0)) continue;

    const consumeResult = await consumeStockReservationInternal(db, {
      id: reservation.id,
      consumedQuantity: consumedQuantity.toString(),
    });
    if (!consumeResult.ok) {
      return consumeResult;
    }
    remainingReservationConsumption = remainingReservationConsumption.minus(consumedQuantity);
  }

  return ok({
    stockLevel: stockLevel as Selectable<"StockLevel">,
    locationStockQuantity,
  });
}
