/* eslint-disable @typescript-eslint/array-type */
import type { Transaction } from "../generated/kysely-tailordb";
import {
  GoodsReceiptNotDraftError,
  GoodsReceiptNotFoundError,
  InactiveExecutionReferenceError,
  InventoryHandoffFailedError,
  NoOpenReceiptQuantityError,
  OverReceiptToleranceExceededError,
  PurchaseOrderNotReceivableError,
} from "../lib/errors.generated";
import {
  decodeJson,
  derivePurchaseOrderStatus,
  encodeJson,
  isActiveStatus,
  updateOrderLineQuantities,
  type GoodsReceiptLineInput,
  type PurchaseOrderLineInput,
} from "../lib/domain";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";

export interface PostGoodsReceiptInput {
  id: string;
  inventoryHandoffSucceeded?: boolean;
}

export async function run(db: Transaction, input: PostGoodsReceiptInput, ctx: CommandContext) {
  void ctx;
  const goodsReceipt = await db
    .selectFrom("GoodsReceipt")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!goodsReceipt) {
    return err(new GoodsReceiptNotFoundError(input.id));
  }
  if (goodsReceipt.status !== "DRAFT") {
    return err(new GoodsReceiptNotDraftError(input.id));
  }

  const purchaseOrder = await db
    .selectFrom("PurchaseOrder")
    .selectAll()
    .where("id", "=", goodsReceipt.purchaseOrderId)
    .forUpdate()
    .executeTakeFirst();
  if (!purchaseOrder || ["CANCELLED", "CLOSED"].includes(purchaseOrder.status)) {
    return err(new PurchaseOrderNotReceivableError(goodsReceipt.purchaseOrderId));
  }

  const supplier = await db
    .selectFrom("BusinessPartner")
    .selectAll()
    .where("id", "=", goodsReceipt.supplierId)
    .executeTakeFirst();
  if (!supplier || !isActiveStatus(supplier.status)) {
    return err(new InactiveExecutionReferenceError(goodsReceipt.supplierId));
  }

  const orderLines = decodeJson<PurchaseOrderLineInput[]>(purchaseOrder.lines, []);
  const receiptLines = decodeJson<GoodsReceiptLineInput[]>(goodsReceipt.lines, []);
  const receivedQuantities = new Map<string, number>();
  const handoffPayload: Array<Record<string, unknown>> = [];

  for (const receiptLine of receiptLines) {
    const orderLine = orderLines.find(
      (candidate) => candidate.id === receiptLine.purchaseOrderLineId,
    );
    if (!orderLine) {
      return err(new NoOpenReceiptQuantityError(receiptLine.purchaseOrderLineId));
    }
    const item = await db
      .selectFrom("Item")
      .selectAll()
      .where("id", "=", orderLine.itemId)
      .executeTakeFirst();
    if (!item || !isActiveStatus(item.status)) {
      return err(new InactiveExecutionReferenceError(orderLine.itemId));
    }
    const openQuantity = orderLine.quantity - (orderLine.receivedQuantity ?? 0);
    if (openQuantity <= 0) {
      return err(new NoOpenReceiptQuantityError(receiptLine.purchaseOrderLineId));
    }
    if (receiptLine.quantity > openQuantity * 1.05) {
      return err(new OverReceiptToleranceExceededError(receiptLine.purchaseOrderLineId));
    }

    receivedQuantities.set(
      receiptLine.purchaseOrderLineId,
      (receivedQuantities.get(receiptLine.purchaseOrderLineId) ?? 0) + receiptLine.quantity,
    );

    if (receiptLine.requiresInventoryHandoff) {
      handoffPayload.push({
        purchaseOrderId: purchaseOrder.id,
        purchaseOrderLineId: receiptLine.purchaseOrderLineId,
        goodsReceiptId: goodsReceipt.id,
        quantity: receiptLine.quantity,
        unitId: receiptLine.unitId ?? orderLine.unitId ?? null,
        receiptDate: goodsReceipt.receiptDate,
        destinationSite: receiptLine.destinationSite ?? orderLine.receivingSiteId ?? null,
        destinationStorageLocation: receiptLine.destinationStorageLocation ?? null,
      });
    }
  }

  if (input.inventoryHandoffSucceeded === false && handoffPayload.length > 0) {
    return err(new InventoryHandoffFailedError(input.id));
  }

  const nextOrderLines = updateOrderLineQuantities(
    orderLines,
    receivedQuantities,
    "receivedQuantity",
  );
  const nextOrderStatus = derivePurchaseOrderStatus(purchaseOrder.status, nextOrderLines);

  await db
    .updateTable("PurchaseOrder")
    .set({
      lines: encodeJson(nextOrderLines),
      status: nextOrderStatus,
      updatedAt: new Date(),
    })
    .where("id", "=", purchaseOrder.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  const posted = await db
    .updateTable("GoodsReceipt")
    .set({
      status: "POSTED",
      inventoryHandoff: handoffPayload.length > 0 ? encodeJson(handoffPayload) : null,
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ goodsReceipt: posted });
}
