import { Decimal } from "decimal.js";

type HoldReasonCode =
  | "INVOICE_UNIT_DIFFERS_FROM_PURCHASE_ORDER_UNIT"
  | "INVOICE_UNIT_PRICE_DIFFERS_FROM_PURCHASE_ORDER_UNIT_PRICE"
  | "INVOICE_QUANTITY_EXCEEDS_ORDERED_QUANTITY"
  | "INVOICE_QUANTITY_EXCEEDS_RECEIVED_QUANTITY";

export type AccountPayableLineForMatching = {
  id: string;
  purchaseOrderLineId: string | null;
  quantity: string | null;
  unitPrice: string | null;
  unitId: string | null;
  correctionType?: "PRICE" | "QUANTITY" | "AMOUNT" | null;
};

export type PurchaseOrderLineForMatching = {
  id: string;
  purchaseOrderId: string;
  quantity: string;
  unitPrice: string;
  unitId: string;
  requiresPhysicalReceipt: boolean;
};

export type PurchaseReceiptQuantityForMatching = {
  purchaseOrderLineId: string;
  receivedQuantity: string;
};

export interface AccountPayableToleranceLimit {
  absolute?: string;
  percent?: string;
}

export interface AccountPayableToleranceRule {
  unitPrice?: AccountPayableToleranceLimit;
  quantity?: AccountPayableToleranceLimit;
}

export interface PlannedHold {
  accountPayableDocumentLineId: string;
  reasonCode: HoldReasonCode;
}

export interface AlreadyInvoicedQuantity {
  purchaseOrderLineId: string;
  quantity: string;
}

function sumQuantityByPurchaseOrderLine<T>(
  rows: T[],
  getLineId: (row: T) => string,
  getQuantity: (row: T) => string,
) {
  const quantityByLineId = new Map<string, Decimal>();
  for (const row of rows) {
    const lineId = getLineId(row);
    quantityByLineId.set(
      lineId,
      (quantityByLineId.get(lineId) ?? new Decimal(0)).plus(getQuantity(row)),
    );
  }
  return quantityByLineId;
}

function exceedsTolerance(
  difference: Decimal,
  basis: Decimal,
  tolerance: AccountPayableToleranceLimit | undefined,
) {
  if (!tolerance) return difference.gt(0);
  if (tolerance.absolute === undefined && tolerance.percent === undefined) return difference.gt(0);

  const withinAbsolute = tolerance.absolute !== undefined && difference.lte(tolerance.absolute);
  const withinPercent =
    tolerance.percent !== undefined && difference.lte(basis.abs().mul(tolerance.percent));
  return !(withinAbsolute || withinPercent);
}

export function planAccountPayableSystemHolds(input: {
  lines: AccountPayableLineForMatching[];
  purchaseOrderLines: PurchaseOrderLineForMatching[];
  receiptQuantities: PurchaseReceiptQuantityForMatching[];
  alreadyInvoicedQuantities: AlreadyInvoicedQuantity[];
  tolerance?: AccountPayableToleranceRule;
}): PlannedHold[] {
  const purchaseOrderLineById = new Map(input.purchaseOrderLines.map((line) => [line.id, line]));
  const receivedQuantityByLineId = sumQuantityByPurchaseOrderLine(
    input.receiptQuantities,
    (row) => row.purchaseOrderLineId,
    (row) => row.receivedQuantity,
  );
  const alreadyInvoicedByLineId = sumQuantityByPurchaseOrderLine(
    input.alreadyInvoicedQuantities,
    (row) => row.purchaseOrderLineId,
    (row) => row.quantity,
  );
  const invoiceQuantityByLineId = sumQuantityByPurchaseOrderLine(
    input.lines.filter(
      (
        line,
      ): line is AccountPayableLineForMatching & {
        purchaseOrderLineId: string;
        quantity: string;
      } =>
        line.correctionType == null && line.purchaseOrderLineId !== null && line.quantity !== null,
    ),
    (row) => row.purchaseOrderLineId,
    (row) => row.quantity,
  );

  const plannedHolds: PlannedHold[] = [];
  for (const line of input.lines) {
    if (line.correctionType != null) continue;
    if (
      !line.purchaseOrderLineId ||
      line.quantity === null ||
      line.unitPrice === null ||
      line.unitId === null
    )
      continue;
    const purchaseOrderLine = purchaseOrderLineById.get(line.purchaseOrderLineId);
    if (!purchaseOrderLine) continue;

    const invoiceQuantity = invoiceQuantityByLineId.get(purchaseOrderLine.id) ?? new Decimal(0);
    const alreadyInvoiced = alreadyInvoicedByLineId.get(purchaseOrderLine.id) ?? new Decimal(0);
    const remainingOrdered = new Decimal(purchaseOrderLine.quantity).minus(alreadyInvoiced);
    const remainingReceived = (
      receivedQuantityByLineId.get(purchaseOrderLine.id) ?? new Decimal(0)
    ).minus(alreadyInvoiced);

    if (line.unitId !== purchaseOrderLine.unitId) {
      plannedHolds.push({
        accountPayableDocumentLineId: line.id,
        reasonCode: "INVOICE_UNIT_DIFFERS_FROM_PURCHASE_ORDER_UNIT",
      });
    }
    const unitPriceDifference = new Decimal(line.unitPrice)
      .minus(purchaseOrderLine.unitPrice)
      .abs();
    if (
      exceedsTolerance(
        unitPriceDifference,
        new Decimal(purchaseOrderLine.unitPrice),
        input.tolerance?.unitPrice,
      )
    ) {
      plannedHolds.push({
        accountPayableDocumentLineId: line.id,
        reasonCode: "INVOICE_UNIT_PRICE_DIFFERS_FROM_PURCHASE_ORDER_UNIT_PRICE",
      });
    }
    if (
      exceedsTolerance(
        invoiceQuantity.minus(remainingOrdered),
        remainingOrdered,
        input.tolerance?.quantity,
      )
    ) {
      plannedHolds.push({
        accountPayableDocumentLineId: line.id,
        reasonCode: "INVOICE_QUANTITY_EXCEEDS_ORDERED_QUANTITY",
      });
    }
    if (
      purchaseOrderLine.requiresPhysicalReceipt &&
      exceedsTolerance(
        invoiceQuantity.minus(remainingReceived),
        remainingReceived,
        input.tolerance?.quantity,
      )
    ) {
      plannedHolds.push({
        accountPayableDocumentLineId: line.id,
        reasonCode: "INVOICE_QUANTITY_EXCEEDS_RECEIVED_QUANTITY",
      });
    }
  }

  return plannedHolds;
}
