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

/**
 * Follow the SUPERSEDED chain to find the current ACTIVE order line.
 */
export async function resolveActiveOrderLine(
  db: Transaction,
  lineId: string,
): Promise<OrderLine | undefined> {
  let orderLine = await db
    .selectFrom("PurchaseOrderLine")
    .selectAll()
    .where("id", "=", lineId)
    .executeTakeFirst();
  if (!orderLine) return undefined;

  while ((orderLine.lineStatus ?? "ACTIVE") === "SUPERSEDED") {
    if (!orderLine.supersededByLineId) return undefined;
    orderLine = await db
      .selectFrom("PurchaseOrderLine")
      .selectAll()
      .where("id", "=", orderLine.supersededByLineId)
      .executeTakeFirst();
    if (!orderLine) return undefined;
  }
  return orderLine;
}

/**
 * Compute FIFO allocation of bill line quantities across posted receipt lines.
 * Returns allocations and whether a quantity variance was detected.
 */
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;

    const rootId = orderLine.rootLineId ?? orderLine.id;
    const allPoLines = await db
      .selectFrom("PurchaseOrderLine")
      .selectAll()
      .where("purchaseOrderId", "=", orderLine.purchaseOrderId)
      .execute();
    const lineageIds = allPoLines
      .filter((l) => l.id === rootId || l.rootLineId === rootId)
      .map((l) => l.id);

    const receiptLinesForLineage =
      lineageIds.length > 0
        ? await db
            .selectFrom("GoodsReceiptLine")
            .selectAll()
            .where("purchaseOrderLineId", "in", lineageIds)
            .execute()
        : [];
    const postedReceiptLines = receiptLinesForLineage.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> {
  const now = new Date();
  for (const evt of allocations) {
    await db
      .insertInto("ThreeWayMatchEvent")
      .values({
        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.
 */
export async function updatePOBillingStatus(
  db: Transaction,
  affectedOrderIds: Set<string>,
): Promise<void> {
  for (const orderId of affectedOrderIds) {
    const allOrderLines = await db
      .selectFrom("PurchaseOrderLine")
      .selectAll()
      .where("purchaseOrderId", "=", orderId)
      .execute();

    const activeOrderLines = allOrderLines.filter((ol) => (ol.lineStatus ?? "ACTIVE") === "ACTIVE");
    const allOrderLineIds = allOrderLines.map((ol) => ol.id);

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

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

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

    const lineProgress: LineProgress[] = activeOrderLines.map((ol) => {
      const rootId = ol.rootLineId ?? ol.id;
      const lineageIds = allOrderLines
        .filter((l) => l.id === rootId || l.rootLineId === rootId)
        .map((l) => l.id);
      const totalBilled = lineageIds.reduce(
        (sum, id) => sum.plus(rawBilledByLineId.get(id) ?? 0),
        new Decimal(0),
      );
      return {
        quantity: ol.quantity,
        billedQuantity: totalBilled.toString(),
      };
    });

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