import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CoNotDraftError,
  CoNotFoundError,
  ModifyItemChangedError,
  ModifyQuantityBelowBilledError,
  ModifyQuantityBelowReceivedError,
  ModifyUomChangedError,
  NoChangeOrderLinesError,
  RemoveLineHasBillsError,
  RemoveLineHasReceiptsError,
} from "../lib/errors.generated";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";

export interface SubmitPurchaseChangeOrderInput {
  id: string;
}

export async function run(
  db: Transaction,
  input: SubmitPurchaseChangeOrderInput,
  ctx: CommandContext,
) {
  void ctx;
  const changeOrder = await db
    .selectFrom("PurchaseChangeOrder")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!changeOrder) {
    return err(new CoNotFoundError(input.id));
  }
  if (changeOrder.changeOrderStatus !== "DRAFT") {
    return err(new CoNotDraftError(input.id));
  }

  const coLines = await db
    .selectFrom("PurchaseChangeOrderLine")
    .selectAll()
    .where("purchaseChangeOrderId", "=", input.id)
    .execute();
  if (coLines.length === 0) {
    return err(new NoChangeOrderLinesError(input.id));
  }

  // Collect original PO line IDs for MODIFY/REMOVE lines
  const originalLineIds = coLines
    .filter(
      (l) => (l.action === "MODIFY" || l.action === "REMOVE") && l.originalPurchaseOrderLineId,
    )
    .map((l) => l.originalPurchaseOrderLineId!);

  // Fetch original PO lines
  const originalPoLines =
    originalLineIds.length > 0
      ? await db
          .selectFrom("PurchaseOrderLine")
          .selectAll()
          .where("id", "in", originalLineIds)
          .execute()
      : [];
  const poLineMap = new Map(originalPoLines.map((l) => [l.id, l]));

  // For MODIFY/REMOVE lines, gather lineage roots to compute received/billed sums
  // Get all PO lines for the same PO to compute lineage
  const allPoLines = await db
    .selectFrom("PurchaseOrderLine")
    .selectAll()
    .where("purchaseOrderId", "=", changeOrder.purchaseOrderId)
    .execute();
  const allPoLineIds = allPoLines.map((l) => l.id);

  // Compute received quantities by lineage root
  const postedGRIds = new Set(
    (await db.selectFrom("GoodsReceipt").selectAll().where("status", "=", "POSTED").execute()).map(
      (r) => r.id,
    ),
  );
  const grLines =
    allPoLineIds.length > 0
      ? await db
          .selectFrom("GoodsReceiptLine")
          .selectAll()
          .where("purchaseOrderLineId", "in", allPoLineIds)
          .execute()
      : [];

  // Map PO line ID -> lineage root ID
  const poLineById = new Map(allPoLines.map((l) => [l.id, l]));
  function getLineageRoot(lineId: string): string {
    const line = poLineById.get(lineId);
    if (!line) return lineId;
    return line.rootLineId ?? lineId;
  }

  // Received quantity by PO line ID
  const receivedByLineId = new Map<string, Decimal>();
  for (const grl of grLines) {
    if (grl.purchaseOrderLineId && postedGRIds.has(grl.goodsReceiptId)) {
      const root = getLineageRoot(grl.purchaseOrderLineId);
      receivedByLineId.set(root, (receivedByLineId.get(root) ?? new Decimal(0)).plus(grl.quantity));
    }
  }

  // Billed quantity by PO line ID
  const matchedBillIds = new Set(
    (await db.selectFrom("PurchaseBill").selectAll().where("status", "=", "MATCHED").execute()).map(
      (b) => b.id,
    ),
  );
  const billLines =
    allPoLineIds.length > 0
      ? await db
          .selectFrom("PurchaseBillLine")
          .selectAll()
          .where("purchaseOrderLineId", "in", allPoLineIds)
          .execute()
      : [];
  const billedByLineId = new Map<string, Decimal>();
  for (const bl of billLines) {
    if (bl.purchaseOrderLineId && matchedBillIds.has(bl.purchaseBillId)) {
      const root = getLineageRoot(bl.purchaseOrderLineId);
      billedByLineId.set(root, (billedByLineId.get(root) ?? new Decimal(0)).plus(bl.quantity));
    }
  }

  // Validate MODIFY lines
  for (const coLine of coLines) {
    if (coLine.action === "MODIFY" && coLine.originalPurchaseOrderLineId) {
      const origLine = poLineMap.get(coLine.originalPurchaseOrderLineId);
      if (!origLine) continue;

      // Cannot change item
      if (coLine.itemId !== null && coLine.itemId !== origLine.itemId) {
        return err(new ModifyItemChangedError(coLine.originalPurchaseOrderLineId));
      }
      // Cannot change UoM
      if (coLine.unitId !== null && coLine.unitId !== origLine.unitId) {
        return err(new ModifyUomChangedError(coLine.originalPurchaseOrderLineId));
      }

      // Quantity cannot be below received
      if (coLine.quantity !== null) {
        const root = getLineageRoot(coLine.originalPurchaseOrderLineId);
        const received = receivedByLineId.get(root) ?? new Decimal(0);
        if (new Decimal(coLine.quantity).lt(received)) {
          return err(new ModifyQuantityBelowReceivedError(coLine.originalPurchaseOrderLineId));
        }
        const billed = billedByLineId.get(root) ?? new Decimal(0);
        if (new Decimal(coLine.quantity).lt(billed)) {
          return err(new ModifyQuantityBelowBilledError(coLine.originalPurchaseOrderLineId));
        }
      }
    }
  }

  // Validate REMOVE lines
  for (const coLine of coLines) {
    if (coLine.action === "REMOVE" && coLine.originalPurchaseOrderLineId) {
      const root = getLineageRoot(coLine.originalPurchaseOrderLineId);
      const received = receivedByLineId.get(root) ?? new Decimal(0);
      if (received.gt(0)) {
        return err(new RemoveLineHasReceiptsError(coLine.originalPurchaseOrderLineId));
      }
      const billed = billedByLineId.get(root) ?? new Decimal(0);
      if (billed.gt(0)) {
        return err(new RemoveLineHasBillsError(coLine.originalPurchaseOrderLineId));
      }
    }
  }

  const submitted = await db
    .updateTable("PurchaseChangeOrder")
    .set({ changeOrderStatus: "SUBMITTED", updatedAt: new Date() })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ changeOrder: submitted });
}
