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 { PurchaseBillMatchingMode } from "../generated/enums";
import type { Selectable, Transaction } from "../generated/kysely-tailordb";
import { calculateDueSchedule } from "../lib/domain";
import {
  MatchModeRequiredError,
  PaymentScheduleCalculationFailedError,
  PurchaseBillNotDraftError,
  PurchaseBillNotFoundError,
  PurchaseOrderCancelledError,
} from "../lib/errors.generated";
import {
  planThreeWayMatch,
  createThreeWayMatchEvents,
  updatePOBillingStatus,
  type PurchaseReceiptEvidenceForMatching,
} from "../lib/matching";
import type { InventoryQueries } from "../module";

function priceVarianceExceeded(
  billedUnitPrice: string | number,
  orderedUnitPrice: string | number,
) {
  const billed = new Decimal(billedUnitPrice);
  const ordered = new Decimal(orderedUnitPrice);
  if (ordered.lte(0)) return billed.gt(0);
  return billed.gt(ordered);
}

export interface MatchPurchaseBillInput {
  id: string;
  matchingMode: PurchaseBillMatchingMode;
}

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

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

  const affectedOrderIds = new Set<string>();
  const currentBilledByOrderLineId = new Map<string, Decimal>();
  const orderLineById = new Map<string, NonNullable<Awaited<ReturnType<typeof getOrderLine>>>>();
  // Map from bill line ID to the resolved PO line
  const resolvedOrderLines = new Map<
    string,
    NonNullable<Awaited<ReturnType<typeof getOrderLine>>>
  >();
  let varianceReason: string | null = null;
  let billAmount = new Decimal(0);

  // Helper: fetch PO line directly by stable ID (no lineage resolution needed)
  async function getOrderLine(lineId: string) {
    return db
      .selectFrom("PurchaseOrderLine")
      .selectAll()
      .where("id", "=", lineId)
      .executeTakeFirst();
  }

  for (const billLine of billLines) {
    if (billLine.purchaseOrderLineId === null) {
      billAmount = billAmount.plus(
        new Decimal(billLine.quantity).mul(billLine.billedUnitPrice ?? 0),
      );
      continue;
    }

    const orderLine = await getOrderLine(billLine.purchaseOrderLineId);
    if (!orderLine) continue;

    const purchaseOrder = await db
      .selectFrom("PurchaseOrder")
      .selectAll()
      .where("id", "=", orderLine.purchaseOrderId)
      .executeTakeFirst();
    if (!purchaseOrder) continue;

    if (purchaseOrder.orderStatus === "CANCELLED") {
      return err(new PurchaseOrderCancelledError(purchaseOrder.id));
    }

    affectedOrderIds.add(purchaseOrder.id);
    resolvedOrderLines.set(billLine.id, orderLine);
    orderLineById.set(orderLine.id, orderLine);
    currentBilledByOrderLineId.set(
      orderLine.id,
      (currentBilledByOrderLineId.get(orderLine.id) ?? new Decimal(0)).plus(billLine.quantity),
    );

    if (
      billLine.billedUnitPrice !== null &&
      billLine.billedUnitPrice !== undefined &&
      priceVarianceExceeded(billLine.billedUnitPrice, orderLine.unitPrice)
    ) {
      varianceReason ??= "PRICE_VARIANCE_EXCEEDED";
    }

    billAmount = billAmount.plus(new Decimal(billLine.quantity).mul(billLine.billedUnitPrice ?? 0));
  }

  const orderLineIds = [...currentBilledByOrderLineId.keys()];
  if (orderLineIds.length > 0) {
    const existingBillLines = await db
      .selectFrom("PurchaseBillLine")
      .selectAll()
      .where("purchaseOrderLineId", "in", orderLineIds)
      .execute();
    const existingBillIds = [...new Set(existingBillLines.map((line) => line.purchaseBillId))];
    const matchedBillIds = new Set(
      existingBillIds.length > 0
        ? (
            await db
              .selectFrom("PurchaseBill")
              .select("id")
              .where("id", "in", existingBillIds)
              .where("status", "=", "MATCHED")
              .execute()
          ).map((b) => b.id)
        : [],
    );

    const previouslyBilledByOrderLineId = new Map<string, Decimal>();
    for (const line of existingBillLines) {
      if (line.purchaseOrderLineId && matchedBillIds.has(line.purchaseBillId)) {
        previouslyBilledByOrderLineId.set(
          line.purchaseOrderLineId,
          (previouslyBilledByOrderLineId.get(line.purchaseOrderLineId) ?? new Decimal(0)).plus(
            line.quantity,
          ),
        );
      }
    }

    for (const [orderLineId, currentBilled] of currentBilledByOrderLineId) {
      const orderLine = orderLineById.get(orderLineId);
      if (!orderLine) continue;
      const totalBilled = (previouslyBilledByOrderLineId.get(orderLineId) ?? new Decimal(0)).plus(
        currentBilled,
      );
      if (totalBilled.gt(orderLine.quantity)) {
        varianceReason ??= "ORDER_QUANTITY_EXCEEDED";
      }
    }
  }

  if (input.matchingMode === "THREE_WAY") {
    const receiptEvidence = await loadReceiptEvidence(db, ctx, inventoryQueries, [
      ...resolvedOrderLines.values(),
    ]);
    const plan = await planThreeWayMatch(db, billLines, resolvedOrderLines, receiptEvidence);
    if (plan.quantityVarianceDetected) {
      varianceReason ??= "RECEIPT_QUANTITY_EXCEEDED";
    }

    if (!varianceReason && plan.allocations.length > 0) {
      await createThreeWayMatchEvents(db, plan.allocations);
    }
  }

  const dueSchedule = calculateDueSchedule(
    purchaseBill.paymentTermSnapshotLines ?? [],
    new Date(purchaseBill.invoiceDate),
    billAmount.toString(),
  );
  if ("error" in dueSchedule) {
    return err(new PaymentScheduleCalculationFailedError(input.id));
  }

  const nextStatus = purchaseBillLifecycle.tryTransition(
    purchaseBill.status,
    varianceReason ? "block" : "match",
  );
  if (!nextStatus) {
    return err(new PurchaseBillNotDraftError(input.id));
  }

  const matched = await db
    .updateTable("PurchaseBill")
    .set({
      status: nextStatus,
      matchingMode: input.matchingMode,
      varianceReason,
      dueSchedule: varianceReason ? null : dueSchedule.schedule,
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  if (!varianceReason) {
    await updatePOBillingStatus(db, affectedOrderIds);
  }

  return ok({ purchaseBill: matched });
}

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;
}
