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 {
  AccountInactiveError,
  AccountNotFoundError,
  ApAccountingEventPostFailedError,
  ApCurrencyNotFoundError,
  ApDocumentHasActiveHoldError,
  ApDocumentNotFoundError,
  ApInvalidDocumentStatusError,
  ApLineTotalMismatchError,
  ApMinimumLinesNotMetError,
} from "../lib/errors.generated";
import type { AccountingEventCommands, CoaManagementQueries, PrimitivesQueries } from "../module";

export interface PostAccountPayableDocumentInput {
  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({});
}

export async function run(
  db: Transaction,
  input: PostAccountPayableDocumentInput,
  ctx: CommandContext,
  primitivesQueries: Pick<PrimitivesQueries, "getCurrency">,
  accountingEventCommands: Pick<AccountingEventCommands, "postAccountingEvent">,
  coaManagementQueries: Pick<CoaManagementQueries, "listAccounts">,
) {
  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, "post");
  if (!nextStatus) return err(new ApInvalidDocumentStatusError(input.id));

  const documentLines = await db
    .selectFrom("AccountPayableDocumentLine")
    .selectAll()
    .where("accountPayableDocumentId", "=", input.id)
    .orderBy("createdAt", "asc")
    .execute();

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

  const activeHolds = await db
    .selectFrom("AccountPayableDocumentHold")
    .selectAll()
    .where("accountPayableDocumentId", "=", input.id)
    .where("releasedAt", "is", null)
    .execute();
  if (activeHolds.length > 0) return err(new ApDocumentHasActiveHoldError(input.id));

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

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

  const distributionsByLineId = new Map<string, { amount: string }[]>();
  for (const distribution of distributions) {
    const lineDistributions = distributionsByLineId.get(distribution.accountPayableDocumentLineId);
    if (lineDistributions) lineDistributions.push(distribution);
    else distributionsByLineId.set(distribution.accountPayableDocumentLineId, [distribution]);
  }

  for (const line of documentLines) {
    const lineDistributionTotalResult = validateDistributionTotal(
      line.id,
      line.grossAmount,
      distributionsByLineId.get(line.id) ?? [],
    );
    if (!lineDistributionTotalResult.ok) return lineDistributionTotalResult;
  }

  const { currency } = (await primitivesQueries.getCurrency(db, { id: document.currencyId }, ctx))
    .value;
  if (!currency) return err(new ApCurrencyNotFoundError(document.currencyId));

  const accountIds = [
    ...new Set([
      document.payableControlAccountId,
      ...distributions.map((distribution) => distribution.accountId),
    ]),
  ];
  const accounts = (
    await coaManagementQueries.listAccounts(
      db,
      { companyId: document.companyId, accountIds, limit: accountIds.length },
      ctx,
    )
  ).value.items;
  const accountById = new Map(accounts.map((account) => [account.id, account]));
  for (const accountId of accountIds) {
    const account = accountById.get(accountId);
    if (!account) return err(new AccountNotFoundError(accountId));
    if (account.status !== "ACTIVE") return err(new AccountInactiveError(accountId));
  }

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

  const now = new Date();
  const eventResult = await accountingEventCommands.postAccountingEvent(
    db,
    {
      companyId: document.companyId,
      eventKind: "ACCOUNT_PAYABLE_RECOGNIZED",
      schemaVersion: "1",
      payload: {
        source: {
          documentType: "ACCOUNT_PAYABLE_DOCUMENT",
          documentId: document.id,
          documentNumber: document.documentNumber,
        },
        documentType: document.documentType,
        supplierId: document.businessPartnerId,
        documentDate: document.documentDate,
        accountingDate: document.postingDate,
        dueSchedule: dueSchedule.map((line) => ({
          dueDate: line.dueDate,
          amount: line.amount,
        })),
        currencyId: document.currencyId,
        currencyCode: currency.code,
        totalAmount: document.totalAmount,
        payableAccountId: document.payableControlAccountId,
        lines: documentLines.map((line) => ({
          lineId: line.id,
          description: line.description,
          quantity: null,
          unitPrice: null,
          amount: line.grossAmount,
        })),
        distributions: distributions.map((distribution) => ({
          distributionId: distribution.id,
          lineId: distribution.accountPayableDocumentLineId,
          accountId: distribution.accountId,
          amount: distribution.amount,
        })),
      },
    },
    ctx,
  );
  if (!eventResult.ok) return err(new ApAccountingEventPostFailedError(input.id));

  const postedDocument = await db
    .updateTable("AccountPayableDocument")
    .set({
      status: nextStatus,
      postedAt: now,
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({
    accountPayableDocument: postedDocument,
    accountingEvent: eventResult.value.accountingEvent,
  });
}
