import { err, ok } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { inventorySupplyPlanLifecycle } from "../db/inventorySupplyPlan.lifecycle.generated";
import type { InventorySupplyPlanSourceType } from "../generated/enums";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  InventorySupplyPlanNotFoundError,
  InvalidSupplyPlanQuantitiesError,
  SupplyPlanNotOpenError,
} from "../lib/errors.generated";

export interface InventorySupplyPlanQuantity {
  expectedQuantity: string;
  receivedQuantity: string;
}

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

export interface ConsumeReceiptInventorySupplyPlansInternalInput {
  itemId: string;
  siteId: string;
  quantity: string;
  sourceDocumentType: string | null;
  sourceDocumentId: string | null;
  sourceLineId: string | null;
  lotId: string | null;
  serialNumberId: string | null;
}

export function getOpenInventorySupplyPlanQuantity(supplyPlan: InventorySupplyPlanQuantity) {
  return Decimal.max(
    new Decimal(supplyPlan.expectedQuantity).minus(supplyPlan.receivedQuantity),
    0,
  );
}

function isInventorySupplyPlanSourceType(
  sourceType: string,
): sourceType is InventorySupplyPlanSourceType {
  return (
    sourceType === "PURCHASE_ORDER" ||
    sourceType === "TRANSFER_ORDER" ||
    sourceType === "MANUFACTURING_ORDER"
  );
}

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

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

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

  if (consumedQuantity.gt(getOpenInventorySupplyPlanQuantity(existing))) {
    return err(new InvalidSupplyPlanQuantitiesError(input.consumedQuantity));
  }

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

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

  return ok({ supplyPlan });
}

export async function consumeReceiptInventorySupplyPlansInternal(
  db: Transaction,
  input: ConsumeReceiptInventorySupplyPlansInternalInput,
) {
  if (!input.sourceDocumentType || !input.sourceDocumentId || !input.sourceLineId) {
    return ok({ supplyPlans: [] });
  }
  if (!isInventorySupplyPlanSourceType(input.sourceDocumentType)) {
    return ok({ supplyPlans: [] });
  }

  const matchingSupplyPlans = await db
    .selectFrom("InventorySupplyPlan")
    .selectAll()
    .where("itemId", "=", input.itemId)
    .where("siteId", "=", input.siteId)
    .where("sourceType", "=", input.sourceDocumentType)
    .where("sourceId", "=", input.sourceDocumentId)
    .where("sourceLineId", "=", input.sourceLineId)
    .where("status", "=", "OPEN")
    .orderBy("expectedDate", "asc")
    .orderBy("id", "asc")
    .forUpdate()
    .execute();

  const applicableSupplyPlans = matchingSupplyPlans.filter(
    (supplyPlan) =>
      (supplyPlan.lotId === null || supplyPlan.lotId === input.lotId) &&
      (supplyPlan.serialNumberId === null || supplyPlan.serialNumberId === input.serialNumberId),
  );

  if (applicableSupplyPlans.length === 0) {
    return ok({ supplyPlans: [] });
  }

  const consumedSupplyPlans = [];
  let remainingQuantity = new Decimal(input.quantity);
  for (const supplyPlan of applicableSupplyPlans) {
    if (remainingQuantity.lte(0)) break;

    const consumedQuantity = Decimal.min(
      getOpenInventorySupplyPlanQuantity(supplyPlan),
      remainingQuantity,
    );
    if (consumedQuantity.lte(0)) continue;

    const consumeResult = await consumeInventorySupplyPlanInternal(db, {
      id: supplyPlan.id,
      consumedQuantity: consumedQuantity.toString(),
    });
    if (!consumeResult.ok) {
      return consumeResult;
    }
    consumedSupplyPlans.push(consumeResult.value.supplyPlan);
    remainingQuantity = remainingQuantity.minus(consumedQuantity);
  }

  return ok({ supplyPlans: consumedSupplyPlans });
}
