import type { Transaction } from "../generated/kysely-tailordb";
import {
  MatchModeRequiredError,
  PaymentScheduleCalculationFailedError,
  PurchaseBillNotDraftError,
  PurchaseBillNotFoundError,
  PurchaseOrderCancelledError,
} from "../lib/errors.generated";
import {
  calculateDueSchedule,
  decodeJson,
  derivePurchaseOrderStatus,
  encodeJson,
  findOrderLine,
  priceVarianceExceeded,
  totalReceiptQuantityForOrderLine,
  updateOrderLineQuantities,
  type PaymentTermScheduleLineInput,
  type PurchaseBillLineInput,
  type PurchaseOrderLineInput,
} from "../lib/domain";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";

export interface MatchPurchaseBillInput {
  id: string;
  matchingMode: "TWO_WAY" | "THREE_WAY";
}

export async function run(db: Transaction, input: MatchPurchaseBillInput, 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 !== "DRAFT") {
    return err(new PurchaseBillNotDraftError(input.id));
  }
  if (!input.matchingMode) {
    return err(new MatchModeRequiredError(input.id));
  }

  const purchaseOrders = await db.selectFrom("PurchaseOrder").selectAll().execute();
  const postedReceipts = await db
    .selectFrom("GoodsReceipt")
    .selectAll()
    .where("status", "=", "POSTED")
    .execute();
  const billLines = decodeJson<PurchaseBillLineInput[]>(purchaseBill.lines, []);
  const billedQuantitiesByOrderId = new Map<string, Map<string, number>>();
  let varianceReason: string | null = null;

  for (const billLine of billLines) {
    const match = findOrderLine(purchaseOrders, billLine.purchaseOrderLineId);
    if (!match) continue;
    if (match.order.status === "CANCELLED") {
      return err(new PurchaseOrderCancelledError(String(match.order.id)));
    }

    if (
      input.matchingMode === "THREE_WAY" &&
      billLine.quantity >
        totalReceiptQuantityForOrderLine(postedReceipts, billLine.purchaseOrderLineId) * 1.05
    ) {
      varianceReason = "RECEIPT_QUANTITY_EXCEEDED";
    }
    if (
      billLine.billedUnitPrice !== undefined &&
      priceVarianceExceeded(billLine.billedUnitPrice, match.line.unitPrice)
    ) {
      varianceReason = "PRICE_VARIANCE_EXCEEDED";
    }

    const perOrder =
      billedQuantitiesByOrderId.get(String(match.order.id)) ?? new Map<string, number>();
    perOrder.set(
      billLine.purchaseOrderLineId,
      (perOrder.get(billLine.purchaseOrderLineId) ?? 0) + billLine.quantity,
    );
    billedQuantitiesByOrderId.set(String(match.order.id), perOrder);
  }

  const snapshot = decodeJson<{ lines: PaymentTermScheduleLineInput[] }>(
    purchaseBill.paymentTermSnapshot,
    { lines: [] },
  );
  const billAmount = billLines.reduce(
    (sum, line) => sum + line.quantity * (line.billedUnitPrice ?? 0),
    0,
  );
  const dueSchedule = calculateDueSchedule(
    snapshot.lines,
    new Date(purchaseBill.invoiceDate),
    billAmount,
  );
  if ("error" in dueSchedule) {
    return err(new PaymentScheduleCalculationFailedError(input.id));
  }

  if (!varianceReason) {
    for (const [orderId, quantities] of billedQuantitiesByOrderId) {
      const order = purchaseOrders.find((candidate) => candidate.id === orderId);
      if (!order) continue;
      const lines = decodeJson<PurchaseOrderLineInput[]>(order.lines, []);
      const nextLines = updateOrderLineQuantities(lines, quantities, "billedQuantity");
      await db
        .updateTable("PurchaseOrder")
        .set({
          lines: encodeJson(nextLines),
          status: derivePurchaseOrderStatus(order.status, nextLines),
          updatedAt: new Date(),
        })
        .where("id", "=", orderId)
        .returningAll()
        .executeTakeFirstOrThrow();
    }
  }

  const status = varianceReason ? "BLOCKED" : "MATCHED";
  const matched = await db
    .updateTable("PurchaseBill")
    .set({
      status,
      matchingMode: input.matchingMode,
      varianceReason,
      dueSchedule: varianceReason ? null : encodeJson(dueSchedule.schedule),
      comparisonEvidence: encodeJson({ matchingMode: input.matchingMode, varianceReason }),
      billReadyHandoff: varianceReason
        ? null
        : encodeJson({
            invoiceNumber: purchaseBill.invoiceNumber,
            matchingMode: input.matchingMode,
            dueSchedule: dueSchedule.schedule,
          }),
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ purchaseBill: matched });
}
