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";
import type { InventoryCommands, InventoryQueries } from "../module";

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

export async function run(
  db: Transaction,
  input: ClosePurchaseOrderInput,
  ctx: CommandContext,
  inventoryCommands: Pick<InventoryCommands, "closeInventorySupplyPlan">,
  inventoryQueries: Pick<InventoryQueries, "listPurchaseReceiptEvidenceForMatching">,
) {
  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);

  const receiptEvidenceResult =
    lineIds.length > 0
      ? await inventoryQueries.listPurchaseReceiptEvidenceForMatching(
          db,
          { purchaseOrderId: input.id, purchaseOrderLineIds: lineIds },
          ctx,
        )
      : ok({ receiptEvidence: [] });

  const receivedByLineId = new Map<string, Decimal>();
  for (const evidence of receiptEvidenceResult.value.receiptEvidence) {
    receivedByLineId.set(
      evidence.purchaseOrderLineId,
      (receivedByLineId.get(evidence.purchaseOrderLineId) ?? new Decimal(0)).plus(
        evidence.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 = new Decimal(line.billedQuantity ?? "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,
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  const closeResult = await inventoryCommands.closeInventorySupplyPlan(
    db,
    { target: "SOURCE_DOCUMENT", sourceType: "PURCHASE_ORDER", sourceId: input.id },
    ctx,
  );
  if (!closeResult.ok) return closeResult;

  return ok({ purchaseOrder: closed });
}
