import { Decimal } from "decimal.js";
import type { Transaction, Selectable } from "../generated/kysely-tailordb";
import { deriveBillingStatus, type LineProgress } from "./domain";

type OrderLine = Selectable<"PurchaseOrderLine">;
type BillLine = Selectable<"PurchaseBillLine">;

export interface MatchAllocation {
  purchaseBillLineId: string;
  goodsReceiptLineId: string;
  matchedQuantity: Decimal;
}

/**
 * Compute FIFO allocation of bill line quantities across posted receipt lines.
 * Returns allocations and whether a quantity variance was detected.
 *
 * With stable PO line IDs, no lineage resolution is needed — receipt lines
 * reference the same PO line ID throughout its lifecycle.
 */
export async function planThreeWayMatch(
  db: Transaction,
  billLines: BillLine[],
  resolvedOrderLines: Map<string, OrderLine>,
  postedReceiptIdSet: Set<string>,
): Promise<{ allocations: MatchAllocation[]; quantityVarianceDetected: boolean }> {
  const allocations: MatchAllocation[] = [];
  let quantityVarianceDetected = false;

  for (const billLine of billLines) {
    if (billLine.purchaseOrderLineId === null) continue;
    const orderLine = resolvedOrderLines.get(billLine.id);
    if (!orderLine) continue;

    // Direct lookup — no lineage resolution needed
    const receiptLines = await db
      .selectFrom("GoodsReceiptLine")
      .selectAll()
      .where("purchaseOrderLineId", "=", orderLine.id)
      .execute();

    const postedReceiptLines = receiptLines.filter((rl) =>
      postedReceiptIdSet.has(rl.goodsReceiptId),
    );
    const totalReceived = postedReceiptLines.reduce(
      (sum, rl) => sum.plus(rl.quantity),
      new Decimal(0),
    );

    if (new Decimal(billLine.quantity).gt(totalReceived)) {
      quantityVarianceDetected = true;
    }

    const receiptLineIds = postedReceiptLines.map((rl) => rl.id);
    const existingMatches =
      receiptLineIds.length > 0
        ? await db
            .selectFrom("ThreeWayMatchEvent")
            .selectAll()
            .where("goodsReceiptLineId", "in", receiptLineIds)
            .execute()
        : [];
    const usedByReceiptLine = new Map<string, Decimal>();
    for (const m of existingMatches) {
      usedByReceiptLine.set(
        m.goodsReceiptLineId,
        (usedByReceiptLine.get(m.goodsReceiptLineId) ?? new Decimal(0)).plus(m.matchedQuantity),
      );
    }

    let remaining = new Decimal(billLine.quantity);
    for (const rl of postedReceiptLines) {
      if (remaining.lte(0)) break;
      const available = new Decimal(rl.quantity).minus(usedByReceiptLine.get(rl.id) ?? 0);
      if (available.lte(0)) continue;
      const allocated = Decimal.min(remaining, available);
      allocations.push({
        purchaseBillLineId: billLine.id,
        goodsReceiptLineId: rl.id,
        matchedQuantity: allocated,
      });
      remaining = remaining.minus(allocated);
    }
  }

  return { allocations, quantityVarianceDetected };
}

/**
 * Insert ThreeWayMatchEvent records from planned allocations.
 */
export async function createThreeWayMatchEvents(
  db: Transaction,
  allocations: MatchAllocation[],
): Promise<void> {
  if (allocations.length === 0) {
    return;
  }
  const now = new Date();
  await db
    .insertInto("ThreeWayMatchEvent")
    .values(
      allocations.map((evt) => ({
        purchaseBillLineId: evt.purchaseBillLineId,
        goodsReceiptLineId: evt.goodsReceiptLineId,
        matchedQuantity: evt.matchedQuantity.toString(),
        matchedAt: now,
        matchedBy: null,
        updatedAt: null,
      })),
    )
    .execute();
}

/**
 * Derive and update billingStatus for affected purchase orders.
 * With stable PO line IDs, billing quantities are summed directly per line.
 */
export async function updatePOBillingStatus(
  db: Transaction,
  affectedOrderIds: Set<string>,
): Promise<void> {
  for (const orderId of affectedOrderIds) {
    const orderLines = await db
      .selectFrom("PurchaseOrderLine")
      .selectAll()
      .where("purchaseOrderId", "=", orderId)
      .execute();

    const orderLineIds = orderLines.map((ol) => ol.id);

    const billLinesForOrder =
      orderLineIds.length > 0
        ? await db
            .selectFrom("PurchaseBillLine")
            .selectAll()
            .where("purchaseOrderLineId", "in", orderLineIds)
            .execute()
        : [];

    const matchedBillIds = new Set(
      (
        await db.selectFrom("PurchaseBill").selectAll().where("status", "=", "MATCHED").execute()
      ).map((b) => b.id),
    );

    // Direct per-line billed quantity (no lineage)
    const billedByLineId = new Map<string, Decimal>();
    for (const bl of billLinesForOrder) {
      if (bl.purchaseOrderLineId && matchedBillIds.has(bl.purchaseBillId)) {
        billedByLineId.set(
          bl.purchaseOrderLineId,
          (billedByLineId.get(bl.purchaseOrderLineId) ?? new Decimal(0)).plus(bl.quantity),
        );
      }
    }

    const lineProgress: LineProgress[] = orderLines.map((ol) => ({
      quantity: ol.quantity,
      billedQuantity: (billedByLineId.get(ol.id) ?? new Decimal(0)).toString(),
    }));

    await db
      .updateTable("PurchaseOrder")
      .set({
        billingStatus: deriveBillingStatus(lineProgress),
        updatedAt: new Date(),
      })
      .where("id", "=", orderId)
      .returningAll()
      .executeTakeFirstOrThrow();
  }
}
