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

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

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

  // Block closure if an open change order exists
  const openCO = await db
    .selectFrom("PurchaseChangeOrder")
    .selectAll()
    .where("purchaseOrderId", "=", input.id)
    .where("changeOrderStatus", "in", ["DRAFT", "SUBMITTED", "APPROVED"])
    .executeTakeFirst();
  if (openCO) {
    return err(new OpenChangeOrderExistsError(input.id));
  }

  // Fetch ALL lines (ACTIVE + SUPERSEDED) for lineage-aware computation
  const allLines = await db
    .selectFrom("PurchaseOrderLine")
    .selectAll()
    .where("purchaseOrderId", "=", input.id)
    .execute();

  // Only ACTIVE lines are targets for close validation
  const activeLines = allLines.filter((l) => (l.lineStatus ?? "ACTIVE") === "ACTIVE");
  const allLineIds = allLines.map((l) => l.id);

  // Compute received quantities from POSTED GoodsReceipts across all lineage lines
  const postedGRIds = new Set(
    (await db.selectFrom("GoodsReceipt").selectAll().where("status", "=", "POSTED").execute()).map(
      (r) => r.id,
    ),
  );

  const grLines =
    allLineIds.length > 0
      ? await db
          .selectFrom("GoodsReceiptLine")
          .selectAll()
          .where("purchaseOrderLineId", "in", allLineIds)
          .execute()
      : [];

  // Raw received quantity per individual line ID
  const rawReceivedByLineId = new Map<string, Decimal>();
  for (const grl of grLines) {
    if (grl.purchaseOrderLineId && postedGRIds.has(grl.goodsReceiptId)) {
      rawReceivedByLineId.set(
        grl.purchaseOrderLineId,
        (rawReceivedByLineId.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(grl.quantity),
      );
    }
  }

  // Compute billed quantities from MATCHED PurchaseBills across all lineage lines
  const matchedBillIds = new Set(
    (await db.selectFrom("PurchaseBill").selectAll().where("status", "=", "MATCHED").execute()).map(
      (b) => b.id,
    ),
  );

  const billLines =
    allLineIds.length > 0
      ? await db
          .selectFrom("PurchaseBillLine")
          .selectAll()
          .where("purchaseOrderLineId", "in", allLineIds)
          .execute()
      : [];

  // Raw billed quantity per individual line ID
  const rawBilledByLineId = new Map<string, Decimal>();
  for (const bl of billLines) {
    if (bl.purchaseOrderLineId && matchedBillIds.has(bl.purchaseBillId)) {
      rawBilledByLineId.set(
        bl.purchaseOrderLineId,
        (rawBilledByLineId.get(bl.purchaseOrderLineId) ?? new Decimal(0)).plus(bl.quantity),
      );
    }
  }

  // Helper: get all line IDs in the same lineage
  function getLineageIds(line: (typeof allLines)[number]) {
    const rootId = line.rootLineId ?? line.id;
    return allLines.filter((l) => l.id === rootId || l.rootLineId === rootId).map((l) => l.id);
  }

  // Lineage-aware check: for each ACTIVE line, sum received/billed across lineage
  const hasUnresolvedOpenQuantity = activeLines.some((line) => {
    const lineageIds = getLineageIds(line);
    const totalReceived = lineageIds.reduce(
      (sum, id) => sum.plus(rawReceivedByLineId.get(id) ?? 0),
      new Decimal(0),
    );
    const totalBilled = lineageIds.reduce(
      (sum, id) => sum.plus(rawBilledByLineId.get(id) ?? 0),
      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: "CLOSED",
      closeReason: input.closeReason ?? null,
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ purchaseOrder: closed });
}
