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

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

export async function run(
  db: Transaction,
  input: ReleasePurchaseBillInput,
  ctx: CommandContext,
  inventoryQueries: Pick<InventoryQueries, "listPurchaseReceiptEvidenceForMatching">,
) {
  const purchaseBill = await db
    .selectFrom("PurchaseBill")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!purchaseBill) {
    return err(new PurchaseBillNotFoundError(input.id));
  }
  const nextStatus = purchaseBillLifecycle.tryTransition(purchaseBill.status, "release");
  if (!nextStatus) {
    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));
  }

  // Collect affected PO IDs from bill lines (direct lookup, no lineage resolution)
  const affectedOrderIds = new Set<string>();
  const resolvedOrderLines = new Map<
    string,
    NonNullable<Awaited<ReturnType<typeof getOrderLine>>>
  >();

  async function getOrderLine(lineId: string) {
    return db
      .selectFrom("PurchaseOrderLine")
      .selectAll()
      .where("id", "=", lineId)
      .executeTakeFirst();
  }

  for (const billLine of billLines) {
    if (billLine.purchaseOrderLineId === null) continue;
    const orderLine = await getOrderLine(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 receiptEvidence = await loadReceiptEvidence(db, ctx, inventoryQueries, [
      ...resolvedOrderLines.values(),
    ]);
    const plan = await planThreeWayMatch(db, billLines, resolvedOrderLines, receiptEvidence);
    if (plan.allocations.length > 0) {
      await createThreeWayMatchEvents(db, plan.allocations);
    }
  }

  const released = await db
    .updateTable("PurchaseBill")
    .set({
      status: nextStatus,
      releaseReason: input.releaseReason,
      dueSchedule: dueSchedule.schedule,
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  await updatePOBillingStatus(db, affectedOrderIds);

  return ok({ purchaseBill: released });
}

async function loadReceiptEvidence(
  db: Transaction,
  ctx: CommandContext,
  inventoryQueries: Pick<InventoryQueries, "listPurchaseReceiptEvidenceForMatching">,
  orderLines: Selectable<"PurchaseOrderLine">[],
): Promise<PurchaseReceiptEvidenceForMatching[]> {
  const lineIdsByOrderId = new Map<string, string[]>();
  for (const orderLine of orderLines) {
    const current = lineIdsByOrderId.get(orderLine.purchaseOrderId) ?? [];
    current.push(orderLine.id);
    lineIdsByOrderId.set(orderLine.purchaseOrderId, current);
  }

  const receiptEvidence: PurchaseReceiptEvidenceForMatching[] = [];
  for (const [purchaseOrderId, purchaseOrderLineIds] of lineIdsByOrderId) {
    const result = await inventoryQueries.listPurchaseReceiptEvidenceForMatching(
      db,
      { purchaseOrderId, purchaseOrderLineIds },
      ctx,
    );
    if (result.ok) {
      receiptEvidence.push(...result.value.receiptEvidence);
    }
  }
  return receiptEvidence;
}
