import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CoNotApprovedError,
  CoNotFoundError,
  ModifyQuantityBelowBilledError,
  PoNotFoundError,
  ModifyQuantityBelowReceivedError,
  RemoveLineHasBillsError,
  RemoveLineHasReceiptsError,
  RevisionMismatchError,
} from "../lib/errors.generated";
import { deriveReceiptStatus, deriveBillingStatus, type LineProgress } from "../lib/domain";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";

export interface ApplyPurchaseChangeOrderInput {
  id: string;
}

export async function run(
  db: Transaction,
  input: ApplyPurchaseChangeOrderInput,
  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 !== "APPROVED") {
    return err(new CoNotApprovedError(input.id));
  }

  // Fetch the PO
  const purchaseOrder = await db
    .selectFrom("PurchaseOrder")
    .selectAll()
    .where("id", "=", changeOrder.purchaseOrderId)
    .forUpdate()
    .executeTakeFirst();
  if (!purchaseOrder) {
    return err(new PoNotFoundError(changeOrder.purchaseOrderId));
  }

  // Stale revision check
  const currentRevision = purchaseOrder.currentRevisionNumber ?? 1;
  if (changeOrder.fromRevisionNumber !== currentRevision) {
    return err(new RevisionMismatchError(input.id));
  }

  // Fetch CO lines
  const coLines = await db
    .selectFrom("PurchaseChangeOrderLine")
    .selectAll()
    .where("purchaseChangeOrderId", "=", input.id)
    .execute();

  // Fetch all PO lines
  const allPoLines = await db
    .selectFrom("PurchaseOrderLine")
    .selectAll()
    .where("purchaseOrderId", "=", changeOrder.purchaseOrderId)
    .execute();
  const poLineById = new Map(allPoLines.map((l) => [l.id, l]));
  const allPoLineIds = allPoLines.map((l) => l.id);

  function getLineageRoot(lineId: string): string {
    const line = poLineById.get(lineId);
    if (!line) return lineId;
    return line.rootLineId ?? lineId;
  }

  // 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()
      : [];

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

  // Compute billed quantities by lineage root
  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 billedByRoot = new Map<string, Decimal>();
  for (const bl of billLines) {
    if (bl.purchaseOrderLineId && matchedBillIds.has(bl.purchaseBillId)) {
      const root = getLineageRoot(bl.purchaseOrderLineId);
      billedByRoot.set(root, (billedByRoot.get(root) ?? new Decimal(0)).plus(bl.quantity));
    }
  }

  // Re-validate MODIFY and REMOVE constraints at apply time
  for (const coLine of coLines) {
    if (coLine.action === "MODIFY" && coLine.originalPurchaseOrderLineId) {
      if (coLine.quantity !== null) {
        const root = getLineageRoot(coLine.originalPurchaseOrderLineId);
        const received = receivedByRoot.get(root) ?? new Decimal(0);
        if (new Decimal(coLine.quantity).lt(received)) {
          return err(new ModifyQuantityBelowReceivedError(coLine.originalPurchaseOrderLineId));
        }
        const billed = billedByRoot.get(root) ?? new Decimal(0);
        if (new Decimal(coLine.quantity).lt(billed)) {
          return err(new ModifyQuantityBelowBilledError(coLine.originalPurchaseOrderLineId));
        }
      }
    }
    if (coLine.action === "REMOVE" && coLine.originalPurchaseOrderLineId) {
      const root = getLineageRoot(coLine.originalPurchaseOrderLineId);
      const received = receivedByRoot.get(root) ?? new Decimal(0);
      if (received.gt(0)) {
        return err(new RemoveLineHasReceiptsError(coLine.originalPurchaseOrderLineId));
      }
      const billed = billedByRoot.get(root) ?? new Decimal(0);
      if (billed.gt(0)) {
        return err(new RemoveLineHasBillsError(coLine.originalPurchaseOrderLineId));
      }
    }
  }

  const newRevision = currentRevision + 1;

  // Track new lines created (for status recalculation)
  // We need to track which line IDs are now ACTIVE after applying changes
  const supersededLineIds = new Set<string>();
  const newActiveLines: {
    rootLineId: string | null;
    quantity: string;
  }[] = [];

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

      const rootLineId = origLine.rootLineId ?? origLine.id;

      // Create new ACTIVE line inheriting fields from original
      const newLine = await db
        .insertInto("PurchaseOrderLine")
        .values({
          purchaseOrderId: changeOrder.purchaseOrderId,
          itemId: origLine.itemId,
          itemSnapshotName: origLine.itemSnapshotName,
          itemSnapshotSku: origLine.itemSnapshotSku,
          description: origLine.description,
          quantity: coLine.quantity ?? origLine.quantity,
          unitPrice: coLine.unitPrice ?? origLine.unitPrice,
          unitId: origLine.unitId,
          receivingSiteId: origLine.receivingSiteId,
          requiresPhysicalReceipt: origLine.requiresPhysicalReceipt,
          matchedPriceRuleId: origLine.matchedPriceRuleId,
          revisionNumber: newRevision,
          lineStatus: "ACTIVE",
          supersededByLineId: null,
          rootLineId,
          createdAt: new Date(),
          updatedAt: null,
        })
        .returningAll()
        .executeTakeFirstOrThrow();

      // Supersede original line
      await db
        .updateTable("PurchaseOrderLine")
        .set({
          lineStatus: "SUPERSEDED",
          supersededByLineId: newLine.id,
          updatedAt: new Date(),
        })
        .where("id", "=", origLine.id)
        .execute();

      supersededLineIds.add(origLine.id);
      newActiveLines.push({ rootLineId, quantity: newLine.quantity });
    }
  }

  // Process REMOVE lines
  for (const coLine of coLines) {
    if (coLine.action === "REMOVE" && coLine.originalPurchaseOrderLineId) {
      await db
        .updateTable("PurchaseOrderLine")
        .set({
          lineStatus: "SUPERSEDED",
          supersededByLineId: null,
          updatedAt: new Date(),
        })
        .where("id", "=", coLine.originalPurchaseOrderLineId)
        .execute();

      supersededLineIds.add(coLine.originalPurchaseOrderLineId);
    }
  }

  // Process ADD lines
  for (const coLine of coLines) {
    if (coLine.action === "ADD") {
      const newLine = await db
        .insertInto("PurchaseOrderLine")
        .values({
          purchaseOrderId: changeOrder.purchaseOrderId,
          itemId: coLine.itemId!,
          itemSnapshotName: null,
          itemSnapshotSku: null,
          description: null,
          quantity: coLine.quantity!,
          unitPrice: coLine.unitPrice!,
          unitId: coLine.unitId ?? null,
          receivingSiteId: coLine.receivingSiteId ?? null,
          requiresPhysicalReceipt: true,
          matchedPriceRuleId: null,
          revisionNumber: newRevision,
          lineStatus: "ACTIVE",
          supersededByLineId: null,
          rootLineId: null,
          createdAt: new Date(),
          updatedAt: null,
        })
        .returningAll()
        .executeTakeFirstOrThrow();

      newActiveLines.push({ rootLineId: null, quantity: newLine.quantity });
    }
  }

  // Build the full set of ACTIVE lines for status recalculation
  // Existing ACTIVE lines that were not superseded + new ACTIVE lines
  const remainingActiveLines = allPoLines.filter(
    (l) => (l.lineStatus === "ACTIVE" || l.lineStatus === null) && !supersededLineIds.has(l.id),
  );

  // Compute lineage-based progress for each ACTIVE line
  const lineProgressList: LineProgress[] = [];

  for (const activeLine of remainingActiveLines) {
    const root = activeLine.rootLineId ?? activeLine.id;
    const received = receivedByRoot.get(root) ?? new Decimal(0);
    const billed = billedByRoot.get(root) ?? new Decimal(0);
    lineProgressList.push({
      quantity: activeLine.quantity,
      receivedQuantity: received.toString(),
      billedQuantity: billed.toString(),
    });
  }

  for (const newLine of newActiveLines) {
    const root = newLine.rootLineId;
    if (root) {
      const received = receivedByRoot.get(root) ?? new Decimal(0);
      const billed = billedByRoot.get(root) ?? new Decimal(0);
      lineProgressList.push({
        quantity: newLine.quantity,
        receivedQuantity: received.toString(),
        billedQuantity: billed.toString(),
      });
    } else {
      // ADD line — no lineage history
      lineProgressList.push({
        quantity: newLine.quantity,
        receivedQuantity: "0",
        billedQuantity: "0",
      });
    }
  }

  const receiptStatus = deriveReceiptStatus(lineProgressList);
  const billingStatus = deriveBillingStatus(lineProgressList);

  // Update PO
  await db
    .updateTable("PurchaseOrder")
    .set({
      currentRevisionNumber: newRevision,
      receiptStatus,
      billingStatus,
      updatedAt: new Date(),
    })
    .where("id", "=", changeOrder.purchaseOrderId)
    .execute();

  // Set CO status to APPLIED
  const applied = await db
    .updateTable("PurchaseChangeOrder")
    .set({ changeOrderStatus: "APPLIED", updatedAt: new Date() })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ changeOrder: applied });
}
