import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type { defineModule as defineItemManagementModule } from "../../item-management";
import type { Transaction } from "../generated/kysely-tailordb";
import { deriveReceiptStatus, deriveBillingStatus, type LineProgress } from "../lib/domain";
import {
  EmptyAmendmentChangesError,
  PoNotFoundError,
  PoNotOrderedError,
  LineNotFoundError,
  ModifyQuantityBelowReceivedError,
  ModifyQuantityBelowBilledError,
  RemoveLineHasReceiptsError,
  RemoveLineHasBillsError,
  RemoveLineHasDownstreamReferencesError,
  AddInvalidQuantityError,
  ItemNotFoundError,
  InvalidQuantityError,
  InvalidUnitPriceError,
  ReceivingSiteNotFoundError,
  ReceivingSiteRequiredError,
} from "../lib/errors.generated";
type ItemManagementModule = ReturnType<typeof defineItemManagementModule>;
type ItemManagementQueries = ItemManagementModule["queries"];

export type AmendOrderedPurchaseOrderChange =
  | {
      action: "MODIFY";
      lineId: string;
      quantity?: string;
      unitPrice?: string;
      receivingSiteId?: string | null;
      requiresPhysicalReceipt?: boolean;
    }
  | {
      action: "REMOVE";
      lineId: string;
    }
  | {
      action: "ADD";
      itemId: string;
      quantity: string;
      unitPrice: string;
      unitId: string;
      receivingSiteId?: string;
      requiresPhysicalReceipt?: boolean;
    };

export interface AmendOrderedPurchaseOrderInput {
  id: string;
  reason?: string;
  changes: AmendOrderedPurchaseOrderChange[];
}

export async function run(
  db: Transaction,
  input: AmendOrderedPurchaseOrderInput,
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  // 1. Fetch and validate PurchaseOrder
  const purchaseOrder = await db
    .selectFrom("PurchaseOrder")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!purchaseOrder) {
    return err(new PoNotFoundError(input.id));
  }
  if (purchaseOrder.orderStatus !== "ORDERED") {
    return err(new PoNotOrderedError(input.id));
  }
  if (input.changes.length === 0) {
    return err(new EmptyAmendmentChangesError(input.id));
  }

  // 2. Fetch current PO lines
  const currentLines = await db
    .selectFrom("PurchaseOrderLine")
    .selectAll()
    .where("purchaseOrderId", "=", input.id)
    .execute();
  const lineMap = new Map(currentLines.map((l) => [l.id, l]));

  // 3. Collect all line IDs referenced by MODIFY/REMOVE to compute received/billed
  const referencedLineIds = input.changes
    .filter(
      (c): c is Extract<AmendOrderedPurchaseOrderChange, { action: "MODIFY" | "REMOVE" }> =>
        c.action === "MODIFY" || c.action === "REMOVE",
    )
    .map((c) => c.lineId);

  // Fetch received quantities from non-cancelled GoodsReceiptLines (POSTED + DRAFT)
  const receivedByLineId = new Map<string, Decimal>();
  if (referencedLineIds.length > 0) {
    const receiptLines = await db
      .selectFrom("GoodsReceiptLine")
      .innerJoin("GoodsReceipt", "GoodsReceipt.id", "GoodsReceiptLine.goodsReceiptId")
      .select(["GoodsReceiptLine.purchaseOrderLineId", "GoodsReceiptLine.quantity"])
      .where("GoodsReceiptLine.purchaseOrderLineId", "in", referencedLineIds)
      .where("GoodsReceipt.status", "!=", "CANCELLED")
      .execute();
    for (const grl of receiptLines) {
      if (grl.purchaseOrderLineId) {
        receivedByLineId.set(
          grl.purchaseOrderLineId,
          (receivedByLineId.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(grl.quantity),
        );
      }
    }
  }

  // Fetch billed quantities from non-cancelled PurchaseBillLines (MATCHED + DRAFT + BLOCKED)
  const billedByLineId = new Map<string, Decimal>();
  if (referencedLineIds.length > 0) {
    const billLines = await db
      .selectFrom("PurchaseBillLine")
      .innerJoin("PurchaseBill", "PurchaseBill.id", "PurchaseBillLine.purchaseBillId")
      .select(["PurchaseBillLine.purchaseOrderLineId", "PurchaseBillLine.quantity"])
      .where("PurchaseBillLine.purchaseOrderLineId", "in", referencedLineIds)
      .where("PurchaseBill.status", "!=", "CANCELLED")
      .execute();
    for (const bl of billLines) {
      if (bl.purchaseOrderLineId) {
        billedByLineId.set(
          bl.purchaseOrderLineId,
          (billedByLineId.get(bl.purchaseOrderLineId) ?? new Decimal(0)).plus(bl.quantity),
        );
      }
    }
  }

  const newRevisionNumber = purchaseOrder.revisionNumber + 1;
  const now = new Date();

  // 4. Validate all changes and collect pending operations
  type Line = (typeof currentLines)[number];
  const modifyOps: {
    line: Line;
    quantity: string;
    unitPrice: string;
    receivingSiteId: string | null;
    requiresPhysicalReceipt: boolean;
  }[] = [];
  const removeLines: Line[] = [];
  const addOps: {
    change: Extract<AmendOrderedPurchaseOrderChange, { action: "ADD" }>;
    item: { name: string | null; sku: string | null };
  }[] = [];
  const siteIdsToValidate = new Set<string>();

  for (const change of input.changes) {
    if (change.action === "MODIFY") {
      const line = lineMap.get(change.lineId);
      if (!line) return err(new LineNotFoundError(change.lineId));

      const newQuantity = new Decimal(change.quantity ?? line.quantity);
      const newUnitPrice = change.unitPrice ?? line.unitPrice;

      if (change.quantity !== undefined) {
        if (newQuantity.lte(0)) return err(new InvalidQuantityError(change.lineId));
        const received = receivedByLineId.get(change.lineId) ?? new Decimal(0);
        const billed = billedByLineId.get(change.lineId) ?? new Decimal(0);
        if (newQuantity.lt(received))
          return err(new ModifyQuantityBelowReceivedError(change.lineId));
        if (newQuantity.lt(billed)) return err(new ModifyQuantityBelowBilledError(change.lineId));
      }

      if (change.unitPrice !== undefined && new Decimal(change.unitPrice).lt(0)) {
        return err(new InvalidUnitPriceError(change.lineId));
      }

      const newReceivingSiteId =
        change.receivingSiteId !== undefined ? change.receivingSiteId : line.receivingSiteId;
      const newRequiresPhysicalReceipt =
        change.requiresPhysicalReceipt ?? line.requiresPhysicalReceipt;
      if (newRequiresPhysicalReceipt && !newReceivingSiteId && !purchaseOrder.receivingSiteId) {
        return err(new ReceivingSiteRequiredError(change.lineId));
      }
      if (change.receivingSiteId) siteIdsToValidate.add(change.receivingSiteId);

      modifyOps.push({
        line,
        quantity: newQuantity.toString(),
        unitPrice: newUnitPrice,
        receivingSiteId: newReceivingSiteId,
        requiresPhysicalReceipt: newRequiresPhysicalReceipt,
      });

      // Update lineMap so subsequent changes for the same line see the amended state
      lineMap.set(line.id, {
        ...line,
        quantity: newQuantity.toString(),
        unitPrice: newUnitPrice,
        receivingSiteId: newReceivingSiteId,
        requiresPhysicalReceipt: newRequiresPhysicalReceipt,
      });
    } else if (change.action === "REMOVE") {
      const line = lineMap.get(change.lineId);
      if (!line) return err(new LineNotFoundError(change.lineId));

      const received = receivedByLineId.get(change.lineId) ?? new Decimal(0);
      const billed = billedByLineId.get(change.lineId) ?? new Decimal(0);
      if (received.gt(0)) return err(new RemoveLineHasReceiptsError(change.lineId));
      if (billed.gt(0)) return err(new RemoveLineHasBillsError(change.lineId));

      removeLines.push(line);
      lineMap.delete(line.id);
    } else if (change.action === "ADD") {
      const newQuantity = new Decimal(change.quantity);
      if (newQuantity.lte(0)) return err(new AddInvalidQuantityError(input.id));
      if (new Decimal(change.unitPrice).lt(0)) return err(new InvalidUnitPriceError(input.id));

      const receivingSiteId = change.receivingSiteId ?? null;
      const requiresPhysicalReceipt = change.requiresPhysicalReceipt ?? true;
      if (requiresPhysicalReceipt && !receivingSiteId && !purchaseOrder.receivingSiteId) {
        return err(new ReceivingSiteRequiredError(input.id));
      }
      if (change.receivingSiteId) siteIdsToValidate.add(change.receivingSiteId);

      const { item } = (await itemManagementQueries.getItem(db, { id: change.itemId }, ctx)).value;
      if (!item) return err(new ItemNotFoundError(change.itemId));

      addOps.push({ change, item });
    }
  }

  // Batch validate receiving sites
  if (siteIdsToValidate.size > 0) {
    const siteIds = [...siteIdsToValidate];
    const sites = await db.selectFrom("Site").select("id").where("id", "in", siteIds).execute();
    const foundSiteIds = new Set(sites.map((s) => s.id));
    for (const siteId of siteIds) {
      if (!foundSiteIds.has(siteId)) return err(new ReceivingSiteNotFoundError(siteId));
    }
  }

  // Batch validate REMOVE downstream references
  if (removeLines.length > 0) {
    const removeLineIds = removeLines.map((l) => l.id);

    const draftReceiptRef = await db
      .selectFrom("GoodsReceiptLine")
      .innerJoin("GoodsReceipt", "GoodsReceipt.id", "GoodsReceiptLine.goodsReceiptId")
      .select(["GoodsReceiptLine.purchaseOrderLineId"])
      .where("GoodsReceiptLine.purchaseOrderLineId", "in", removeLineIds)
      .where("GoodsReceipt.status", "=", "DRAFT")
      .executeTakeFirst();
    if (draftReceiptRef) {
      return err(
        new RemoveLineHasDownstreamReferencesError(String(draftReceiptRef.purchaseOrderLineId)),
      );
    }

    const draftOrBlockedBillRef = await db
      .selectFrom("PurchaseBillLine")
      .innerJoin("PurchaseBill", "PurchaseBill.id", "PurchaseBillLine.purchaseBillId")
      .select(["PurchaseBillLine.purchaseOrderLineId"])
      .where("PurchaseBillLine.purchaseOrderLineId", "in", removeLineIds)
      .where("PurchaseBill.status", "in", ["DRAFT", "BLOCKED"])
      .executeTakeFirst();
    if (draftOrBlockedBillRef) {
      return err(
        new RemoveLineHasDownstreamReferencesError(
          String(draftOrBlockedBillRef.purchaseOrderLineId),
        ),
      );
    }
  }

  // 5. Execute mutations

  // MODIFY: update each line (different values per row)
  for (const op of modifyOps) {
    await db
      .updateTable("PurchaseOrderLine")
      .set({
        quantity: op.quantity,
        unitPrice: op.unitPrice,
        receivingSiteId: op.receivingSiteId,
        requiresPhysicalReceipt: op.requiresPhysicalReceipt,
        updatedAt: now,
      })
      .where("id", "=", op.line.id)
      .execute();
  }

  // REMOVE: batch delete
  if (removeLines.length > 0) {
    const removeLineIds = removeLines.map((l) => l.id);
    await db.deleteFrom("PurchaseOrderLine").where("id", "in", removeLineIds).execute();
  }

  // ADD: batch insert
  let addedLines: Line[] = [];
  if (addOps.length > 0) {
    addedLines = await db
      .insertInto("PurchaseOrderLine")
      .values(
        addOps.map((op) => ({
          purchaseOrderId: input.id,
          itemId: op.change.itemId,
          itemSnapshotName: op.item.name ?? null,
          itemSnapshotSku: op.item.sku ?? null,
          quantity: op.change.quantity,
          unitPrice: op.change.unitPrice,
          unitId: op.change.unitId,
          receivingSiteId: op.change.receivingSiteId ?? null,
          requiresPhysicalReceipt: op.change.requiresPhysicalReceipt ?? true,
          createdAt: now,
          updatedAt: null,
        })),
      )
      .returningAll()
      .execute();
    for (const line of addedLines) lineMap.set(line.id, line);
  }

  // Batch insert all revisions
  const revisions = [
    ...modifyOps.map((op) => ({
      purchaseOrderLineId: op.line.id,
      purchaseOrderId: input.id,
      action: "MODIFIED" as const,
      revisionNumber: newRevisionNumber,
      quantity: op.line.quantity,
      unitPrice: op.line.unitPrice,
      itemId: op.line.itemId,
      unitId: op.line.unitId,
      receivingSiteId: op.line.receivingSiteId ?? null,
      reason: input.reason ?? null,
      createdAt: now,
      updatedAt: null,
    })),
    ...removeLines.map((line) => ({
      purchaseOrderLineId: line.id,
      purchaseOrderId: input.id,
      action: "REMOVED" as const,
      revisionNumber: newRevisionNumber,
      quantity: line.quantity,
      unitPrice: line.unitPrice,
      itemId: line.itemId,
      unitId: line.unitId,
      receivingSiteId: line.receivingSiteId ?? null,
      reason: input.reason ?? null,
      createdAt: now,
      updatedAt: null,
    })),
    ...addedLines.map((line) => ({
      purchaseOrderLineId: line.id,
      purchaseOrderId: input.id,
      action: "ADDED" as const,
      revisionNumber: newRevisionNumber,
      quantity: line.quantity,
      unitPrice: line.unitPrice,
      itemId: line.itemId,
      unitId: line.unitId,
      receivingSiteId: line.receivingSiteId ?? null,
      reason: input.reason ?? null,
      createdAt: now,
      updatedAt: null,
    })),
  ];
  if (revisions.length > 0) {
    await db.insertInto("PurchaseOrderLineRevision").values(revisions).execute();
  }

  // 5. Recalculate receiptStatus and billingStatus from remaining lines
  const remainingLineIds = [...lineMap.keys()];

  const allReceivedByLine = new Map<string, Decimal>();
  const allBilledByLine = new Map<string, Decimal>();

  if (remainingLineIds.length > 0) {
    const postedReceiptLinesAll = await db
      .selectFrom("GoodsReceiptLine")
      .innerJoin("GoodsReceipt", "GoodsReceipt.id", "GoodsReceiptLine.goodsReceiptId")
      .select(["GoodsReceiptLine.purchaseOrderLineId", "GoodsReceiptLine.quantity"])
      .where("GoodsReceiptLine.purchaseOrderLineId", "in", remainingLineIds)
      .where("GoodsReceipt.status", "=", "POSTED")
      .execute();
    for (const grl of postedReceiptLinesAll) {
      if (grl.purchaseOrderLineId) {
        allReceivedByLine.set(
          grl.purchaseOrderLineId,
          (allReceivedByLine.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(grl.quantity),
        );
      }
    }

    const matchedBillLinesAll = await db
      .selectFrom("PurchaseBillLine")
      .innerJoin("PurchaseBill", "PurchaseBill.id", "PurchaseBillLine.purchaseBillId")
      .select(["PurchaseBillLine.purchaseOrderLineId", "PurchaseBillLine.quantity"])
      .where("PurchaseBillLine.purchaseOrderLineId", "in", remainingLineIds)
      .where("PurchaseBill.status", "=", "MATCHED")
      .execute();
    for (const bl of matchedBillLinesAll) {
      if (bl.purchaseOrderLineId) {
        allBilledByLine.set(
          bl.purchaseOrderLineId,
          (allBilledByLine.get(bl.purchaseOrderLineId) ?? new Decimal(0)).plus(bl.quantity),
        );
      }
    }
  }

  const lineProgress: LineProgress[] = [...lineMap.values()].map((ol) => ({
    quantity: ol.quantity,
    receivedQuantity: (allReceivedByLine.get(ol.id) ?? new Decimal(0)).toString(),
    billedQuantity: (allBilledByLine.get(ol.id) ?? new Decimal(0)).toString(),
  }));

  // 6. Update PurchaseOrder with new revision number and derived statuses
  const updated = await db
    .updateTable("PurchaseOrder")
    .set({
      revisionNumber: newRevisionNumber,
      receiptStatus: deriveReceiptStatus(lineProgress),
      billingStatus: deriveBillingStatus(lineProgress),
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ purchaseOrder: updated });
}
