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

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 snapshot = decodeJson<{ lines: PaymentTermScheduleLineInput[] }>(
    purchaseBill.paymentTermSnapshot,
    { lines: [] },
  );
  const billLines = decodeJson<PurchaseBillLineInput[]>(purchaseBill.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));
  }

  const purchaseOrders = await db.selectFrom("PurchaseOrder").selectAll().execute();
  const billedQuantitiesByOrderId = new Map<string, Map<string, number>>();
  for (const billLine of billLines) {
    const match = findOrderLine(purchaseOrders, billLine.purchaseOrderLineId);
    if (!match) continue;
    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);
  }

  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 released = await db
    .updateTable("PurchaseBill")
    .set({
      status: "MATCHED",
      releaseReason: input.releaseReason,
      dueSchedule: encodeJson(dueSchedule.schedule),
      billReadyHandoff: encodeJson({
        invoiceNumber: purchaseBill.invoiceNumber,
        matchingMode: purchaseBill.matchingMode,
        dueSchedule: dueSchedule.schedule,
      }),
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ purchaseBill: released });
}
