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 PurchaseReceiptEvidenceForMatching {
  purchaseOrderLineId: string;
  inventoryExecutionLineId: string;
  inventoryExecutionId: string;
  quantity: string;
  createdAt: Date;
}

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

/**
 * Compute FIFO allocation of bill line quantities across posted inventory receipt lines.
 * Returns allocations and whether a quantity variance was detected.
 *
 * InventoryExecutionLine carries the PO-line source reference, so availability
 * and existing consumption must be tracked per execution line.
 */
export async function planThreeWayMatch(
  db: Transaction,
  billLines: BillLine[],
  resolvedOrderLines: Map<string, OrderLine>,
  receiptEvidence: PurchaseReceiptEvidenceForMatching[],
): Promise<{ allocations: MatchAllocation[]; quantityVarianceDetected: boolean }> {
  const allocations: MatchAllocation[] = [];
  let quantityVarianceDetected = false;

  const receiptEvidenceByOrderLineId = new Map<string, PurchaseReceiptEvidenceForMatching[]>();
  for (const evidence of receiptEvidence) {
    const current = receiptEvidenceByOrderLineId.get(evidence.purchaseOrderLineId) ?? [];
    current.push(evidence);
    receiptEvidenceByOrderLineId.set(evidence.purchaseOrderLineId, current);
  }

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

    const receiptLines = receiptEvidenceByOrderLineId.get(orderLine.id) ?? [];

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

    let remaining = new Decimal(billLine.quantity);
    for (const rl of receiptLines) {
      if (remaining.lte(0)) break;
      const available = new Decimal(rl.quantity).minus(
        usedByLineId.get(rl.inventoryExecutionLineId) ?? 0,
      );
      if (available.lte(0)) continue;
      const allocated = Decimal.min(remaining, available);
      allocations.push({
        purchaseBillLineId: billLine.id,
        inventoryExecutionLineId: rl.inventoryExecutionLineId,
        matchedQuantity: allocated,
      });
      remaining = remaining.minus(allocated);
    }
    if (remaining.gt(0)) {
      quantityVarianceDetected = true;
    }
  }

  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,
        inventoryExecutionLineId: evt.inventoryExecutionLineId,
        matchedQuantity: evt.matchedQuantity.toString(),
        matchedAt: now,
        matchedBy: 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 billIdsForOrder = [...new Set(billLinesForOrder.map((line) => line.purchaseBillId))];
    const matchedBillIds = new Set(
      billIdsForOrder.length > 0
        ? (
            await db
              .selectFrom("PurchaseBill")
              .select("id")
              .where("id", "in", billIdsForOrder)
              .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),
      })
      .where("id", "=", orderId)
      .returningAll()
      .executeTakeFirstOrThrow();
  }
}
