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

export interface CancelPurchaseBillInput {
  id: string;
}

export async function run(db: Transaction, input: CancelPurchaseBillInput, 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 (!["DRAFT", "BLOCKED", "MATCHED"].includes(purchaseBill.status)) {
    return err(new PurchaseBillNotCancellableError(input.id));
  }
  if (purchaseBill.downstreamAccepted) {
    return err(new DownstreamHandoffAcceptedError(input.id));
  }

  if (purchaseBill.status === "MATCHED") {
    const purchaseOrders = await db.selectFrom("PurchaseOrder").selectAll().execute();
    const billLines = decodeJson<PurchaseBillLineInput[]>(purchaseBill.lines, []);
    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, []).map((line) => ({
        ...line,
        billedQuantity: (line.billedQuantity ?? 0) + (quantities.get(line.id ?? "") ?? 0),
      }));
      await db
        .updateTable("PurchaseOrder")
        .set({
          lines: encodeJson(lines),
          status: derivePurchaseOrderStatus(order.status, lines),
          updatedAt: new Date(),
        })
        .where("id", "=", orderId)
        .returningAll()
        .executeTakeFirstOrThrow();
    }
  }

  const cancelled = await db
    .updateTable("PurchaseBill")
    .set({ status: "CANCELLED", updatedAt: new Date() })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ purchaseBill: cancelled });
}
