import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { purchaseOrderLifecycle } from "../db/purchaseOrder.lifecycle.generated";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CloseReasonRequiredError,
  OpenQuantityRemainsError,
  PurchaseOrderNotClosableError,
  PurchaseOrderNotFoundError,
} from "../lib/errors.generated";

export interface ClosePurchaseOrderInput {
  id: string;
  closeReason?: string;
  writeOffRemaining?: boolean;
}

export async function run(db: Transaction, input: ClosePurchaseOrderInput, _ctx: CommandContext) {
  const purchaseOrder = await db
    .selectFrom("PurchaseOrder")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!purchaseOrder) {
    return err(new PurchaseOrderNotFoundError(input.id));
  }
  const nextOrderStatus = purchaseOrderLifecycle.tryTransition(purchaseOrder.orderStatus, "close");
  if (!nextOrderStatus) {
    return err(new PurchaseOrderNotClosableError(input.id));
  }

  // Fetch all PO lines for the order
  const poLines = await db
    .selectFrom("PurchaseOrderLine")
    .selectAll()
    .where("purchaseOrderId", "=", input.id)
    .execute();

  const lineIds = poLines.map((l) => l.id);

  // Compute received quantities from POSTED GoodsReceipts
  const grLines =
    lineIds.length > 0
      ? await db
          .selectFrom("GoodsReceiptLine")
          .innerJoin("GoodsReceipt", "GoodsReceipt.id", "GoodsReceiptLine.goodsReceiptId")
          .selectAll("GoodsReceiptLine")
          .where("GoodsReceiptLine.purchaseOrderLineId", "in", lineIds)
          .where("GoodsReceipt.status", "=", "POSTED")
          .execute()
      : [];

  const receivedByLineId = new Map<string, Decimal>();
  for (const grl of grLines) {
    if (grl.purchaseOrderLineId) {
      receivedByLineId.set(
        grl.purchaseOrderLineId,
        (receivedByLineId.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(grl.quantity),
      );
    }
  }

  // Compute billed quantities from MATCHED PurchaseBills
  const billLines =
    lineIds.length > 0
      ? await db
          .selectFrom("PurchaseBillLine")
          .innerJoin("PurchaseBill", "PurchaseBill.id", "PurchaseBillLine.purchaseBillId")
          .selectAll("PurchaseBillLine")
          .where("PurchaseBillLine.purchaseOrderLineId", "in", lineIds)
          .where("PurchaseBill.status", "=", "MATCHED")
          .execute()
      : [];

  const billedByLineId = new Map<string, Decimal>();
  for (const bl of billLines) {
    if (bl.purchaseOrderLineId) {
      billedByLineId.set(
        bl.purchaseOrderLineId,
        (billedByLineId.get(bl.purchaseOrderLineId) ?? new Decimal(0)).plus(bl.quantity),
      );
    }
  }

  // Check: for each line, verify received or billed covers the ordered quantity
  const hasUnresolvedOpenQuantity = poLines.some((line) => {
    const totalReceived = receivedByLineId.get(line.id) ?? new Decimal(0);
    const totalBilled = billedByLineId.get(line.id) ?? new Decimal(0);
    return totalReceived.lt(line.quantity) && totalBilled.lt(line.quantity);
  });
  const writeOffRequested = input.writeOffRemaining === true;

  if (hasUnresolvedOpenQuantity && !writeOffRequested) {
    return err(new OpenQuantityRemainsError(input.id));
  }
  if (hasUnresolvedOpenQuantity && writeOffRequested && !input.closeReason?.trim()) {
    return err(new CloseReasonRequiredError(input.id));
  }

  const closed = await db
    .updateTable("PurchaseOrder")
    .set({
      orderStatus: nextOrderStatus,
      closeReason: input.closeReason ?? null,
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ purchaseOrder: closed });
}
