import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { accountPayableDocumentLifecycle } from "../db/accountPayableDocument.lifecycle.generated";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  type AccountPayableToleranceRule,
  type AlreadyInvoicedQuantity,
  planAccountPayableSystemHolds,
  type PurchaseOrderLineForMatching,
  type PurchaseReceiptQuantityForMatching,
} from "../internal/holdReconciliation";
import { syncPurchaseOrderBillingStatusFromAccountPayable } from "../internal/purchaseBillingFeedback";
import {
  ApDocumentNotFoundError,
  ApDueScheduleInvalidError,
  ApInvalidDocumentStatusError,
  ApLineTotalMismatchError,
  ApMinimumLinesNotMetError,
} from "../lib/errors.generated";
import type { InventoryQueries, PurchaseCommands, PurchaseQueries } from "../module";
import { validateDueSchedule } from "./createAccountPayableDocument";

export interface RegisterAccountPayableDocumentInput {
  id: string;
}

function validateLineTotal(
  identifier: string,
  totalAmount: string,
  lines: { grossAmount: string }[],
) {
  if (lines.length === 0) return err(new ApMinimumLinesNotMetError(identifier));
  const lineTotal = lines.reduce((sum, line) => sum.plus(line.grossAmount), new Decimal(0));
  if (!lineTotal.eq(new Decimal(totalAmount))) {
    return err(new ApLineTotalMismatchError(identifier));
  }
  return ok({});
}

function validateDistributionTotal(
  identifier: string,
  totalAmount: string,
  distributions: { amount: string }[],
) {
  if (distributions.length === 0) return err(new ApMinimumLinesNotMetError(identifier));
  const distributionTotal = distributions.reduce(
    (sum, distribution) => sum.plus(distribution.amount),
    new Decimal(0),
  );
  if (!distributionTotal.eq(new Decimal(totalAmount))) {
    return err(new ApLineTotalMismatchError(identifier));
  }
  return ok({});
}

async function loadAlreadyInvoicedQuantities(
  db: Transaction,
  accountPayableDocumentId: string,
  purchaseOrderLineIds: string[],
): Promise<AlreadyInvoicedQuantity[]> {
  if (purchaseOrderLineIds.length === 0) return [];

  const rows = await db
    .selectFrom("AccountPayableDocumentLine as line")
    .innerJoin("AccountPayableDocument as document", "document.id", "line.accountPayableDocumentId")
    .select([
      "line.purchaseOrderLineId as purchaseOrderLineId",
      "line.quantity as quantity",
      "line.correctionType as correctionType",
      "document.documentType as documentType",
    ])
    .where("line.purchaseOrderLineId", "in", purchaseOrderLineIds)
    .where("document.id", "!=", accountPayableDocumentId)
    .where("document.status", "in", ["REGISTERED", "POSTED"])
    .execute();

  const quantityByLineId = new Map<string, Decimal>();
  for (const row of rows) {
    if (row.purchaseOrderLineId === null || row.quantity === null) continue;
    if (row.correctionType === "PRICE" || row.correctionType === "AMOUNT") continue;
    const signedQuantity =
      row.documentType === "CREDIT_MEMO"
        ? new Decimal(row.quantity).neg()
        : new Decimal(row.quantity);
    quantityByLineId.set(
      row.purchaseOrderLineId,
      (quantityByLineId.get(row.purchaseOrderLineId) ?? new Decimal(0)).plus(signedQuantity),
    );
  }

  return [...quantityByLineId.entries()].map(([purchaseOrderLineId, quantity]) => ({
    purchaseOrderLineId,
    quantity: quantity.toString(),
  }));
}

async function loadPurchaseReceiptQuantities(
  db: Transaction,
  purchaseOrderLines: PurchaseOrderLineForMatching[],
  ctx: CommandContext,
  inventoryQueries: Pick<InventoryQueries, "listPurchaseReceiptQuantitiesForMatching">,
) {
  const purchaseOrderLineIds = purchaseOrderLines.map((line) => line.id);
  if (purchaseOrderLineIds.length === 0) return [] as PurchaseReceiptQuantityForMatching[];
  const result = await inventoryQueries.listPurchaseReceiptQuantitiesForMatching(
    db,
    { purchaseOrderLineIds },
    ctx,
  );
  return result.value?.receiptQuantities ?? [];
}

async function loadSupplierInvoiceTolerance(
  db: Transaction,
  companyId: string,
  supplierId: string,
): Promise<AccountPayableToleranceRule | undefined> {
  const config = await db
    .selectFrom("InvoiceToleranceConfig")
    .selectAll()
    .where("companyId", "=", companyId)
    .where("supplierId", "=", supplierId)
    .executeTakeFirst();

  if (!config) return undefined;
  return {
    quantity: {
      absolute: config.quantityAbsoluteTolerance ?? undefined,
      percent: config.quantityPercentageTolerance ?? undefined,
    },
    unitPrice: {
      absolute: config.unitPriceAbsoluteTolerance ?? undefined,
      percent: config.unitPricePercentageTolerance ?? undefined,
    },
  };
}

export async function run(
  db: Transaction,
  input: RegisterAccountPayableDocumentInput,
  ctx: CommandContext,
  purchaseQueries: Pick<PurchaseQueries, "listPurchaseOrderLinesForMatching">,
  inventoryQueries: Pick<InventoryQueries, "listPurchaseReceiptQuantitiesForMatching">,
  purchaseCommands: Pick<PurchaseCommands, "recalculatePurchaseOrderBillingStatus">,
) {
  const document = await db
    .selectFrom("AccountPayableDocument")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();

  if (!document) return err(new ApDocumentNotFoundError(input.id));

  const nextStatus = accountPayableDocumentLifecycle.tryTransition(document.status, "register");
  if (!nextStatus) return err(new ApInvalidDocumentStatusError(input.id));

  const lines = await db
    .selectFrom("AccountPayableDocumentLine")
    .selectAll()
    .where("accountPayableDocumentId", "=", input.id)
    .execute();

  const lineTotalResult = validateLineTotal(input.id, document.totalAmount, lines);
  if (!lineTotalResult.ok) return lineTotalResult;

  const distributions = await db
    .selectFrom("AccountPayableDistributionLine as distribution")
    .innerJoin(
      "AccountPayableDocumentLine as line",
      "line.id",
      "distribution.accountPayableDocumentLineId",
    )
    .selectAll("distribution")
    .where("line.accountPayableDocumentId", "=", input.id)
    .execute();

  const distributionTotalResult = validateDistributionTotal(
    input.id,
    document.totalAmount,
    distributions,
  );
  if (!distributionTotalResult.ok) return distributionTotalResult;

  const dueSchedule = await db
    .selectFrom("AccountPayableDueScheduleLine")
    .selectAll()
    .where("accountPayableDocumentId", "=", input.id)
    .execute();

  if (dueSchedule.length === 0) return err(new ApDueScheduleInvalidError(input.id));
  const dueScheduleResult = validateDueSchedule(input.id, document.totalAmount, dueSchedule, {
    requireLines: true,
  });
  if (!dueScheduleResult.ok) return dueScheduleResult;

  const tolerance = await loadSupplierInvoiceTolerance(
    db,
    document.companyId,
    document.businessPartnerId,
  );
  const purchaseOrderLineIds = [
    ...new Set(lines.map((line) => line.purchaseOrderLineId).filter((id): id is string => !!id)),
  ];
  const alreadyInvoicedQuantities = await loadAlreadyInvoicedQuantities(
    db,
    input.id,
    purchaseOrderLineIds,
  );
  const purchaseOrderLines =
    purchaseOrderLineIds.length === 0
      ? []
      : ((
          await purchaseQueries.listPurchaseOrderLinesForMatching(db, { purchaseOrderLineIds }, ctx)
        ).value?.purchaseOrderLines ?? []);
  const receiptQuantities = await loadPurchaseReceiptQuantities(
    db,
    purchaseOrderLines,
    ctx,
    inventoryQueries,
  );
  const holdsToCreate = planAccountPayableSystemHolds({
    lines,
    purchaseOrderLines,
    receiptQuantities,
    alreadyInvoicedQuantities,
    tolerance,
  });
  if (holdsToCreate.length > 0) {
    await db
      .insertInto("AccountPayableDocumentHold")
      .values(
        holdsToCreate.map((hold) => ({
          accountPayableDocumentId: input.id,
          accountPayableDocumentLineId: hold.accountPayableDocumentLineId,
          reasonCode: hold.reasonCode,
          source: "SYSTEM" as const,
          releasedAt: null,
          releaseReason: null,
        })),
      )
      .execute();
  }

  const registeredDocument = await db
    .updateTable("AccountPayableDocument")
    .set({
      status: nextStatus,
      registeredAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  const billingSyncResult = await syncPurchaseOrderBillingStatusFromAccountPayable(
    db,
    { document: registeredDocument, lines },
    ctx,
    purchaseCommands,
  );
  if (!billingSyncResult.ok) return billingSyncResult;

  return ok({ accountPayableDocument: registeredDocument });
}
