import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  PaymentScheduleCalculationFailedError,
  PurchaseBillNotBlockedError,
  PurchaseBillNotFoundError,
  ReleaseReasonRequiredError,
} from "../lib/errors.generated";
import { calculateDueSchedule } from "../lib/domain";
import {
  resolveActiveOrderLine,
  planThreeWayMatch,
  createThreeWayMatchEvents,
  updatePOBillingStatus,
} from "../lib/matching";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";

export interface ReleasePurchaseBillInput {
  id: string;
  releaseReason: string;
}

export async function run(db: Transaction, input: ReleasePurchaseBillInput, ctx: CommandContext) {
  void ctx;
  const purchaseBill = await db
    .selectFrom("PurchaseBill")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!purchaseBill) {
    return err(new PurchaseBillNotFoundError(input.id));
  }
  if (purchaseBill.status !== "BLOCKED") {
    return err(new PurchaseBillNotBlockedError(input.id));
  }
  if (!input.releaseReason.trim()) {
    return err(new ReleaseReasonRequiredError(input.id));
  }

  const billLines = await db
    .selectFrom("PurchaseBillLine")
    .selectAll()
    .where("purchaseBillId", "=", input.id)
    .execute();

  const billAmount = billLines.reduce(
    (sum, line) => sum.plus(new Decimal(line.quantity).mul(line.billedUnitPrice ?? 0)),
    new Decimal(0),
  );
  const dueSchedule = calculateDueSchedule(
    purchaseBill.paymentTermSnapshotLines ?? [],
    new Date(purchaseBill.invoiceDate),
    billAmount.toString(),
  );
  if ("error" in dueSchedule) {
    return err(new PaymentScheduleCalculationFailedError(input.id));
  }

  // Resolve order lines and collect affected PO IDs
  const affectedOrderIds = new Set<string>();
  const resolvedOrderLines = new Map<
    string,
    Awaited<ReturnType<typeof resolveActiveOrderLine>> & {}
  >();

  for (const billLine of billLines) {
    if (billLine.purchaseOrderLineId === null) continue;
    const orderLine = await resolveActiveOrderLine(db, billLine.purchaseOrderLineId);
    if (!orderLine) continue;
    resolvedOrderLines.set(billLine.id, orderLine);
    affectedOrderIds.add(orderLine.purchaseOrderId);
  }

  // Create ThreeWayMatchEvent records for THREE_WAY bills
  if (purchaseBill.matchingMode === "THREE_WAY") {
    const postedReceipts = await db
      .selectFrom("GoodsReceipt")
      .selectAll()
      .where("status", "=", "POSTED")
      .execute();
    const postedReceiptIdSet = new Set(postedReceipts.map((r) => r.id));

    const plan = await planThreeWayMatch(db, billLines, resolvedOrderLines, postedReceiptIdSet);
    if (plan.allocations.length > 0) {
      await createThreeWayMatchEvents(db, plan.allocations);
    }
  }

  const released = await db
    .updateTable("PurchaseBill")
    .set({
      status: "MATCHED",
      releaseReason: input.releaseReason,
      // @ts-expect-error TODO: SDK bug — db.object() generates Timestamp for nested db.date(), but Kysely only unwraps ColumnType at top-level columns
      dueSchedule: dueSchedule.schedule,
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  await updatePOBillingStatus(db, affectedOrderIds);

  return ok({ purchaseBill: released });
}
