import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  AccountInactiveError,
  AccountNotFoundError,
  ApBusinessPartnerNotFoundError,
  ApCurrencyNotFoundError,
  ApDistributionLineNotFoundError,
  ApDocumentLineNotFoundError,
  ApDocumentNotFoundError,
  ApInvalidDocumentStatusError,
  ApInvalidAmountError,
  ApLineSourceInconsistentError,
  ApLineTotalMismatchError,
  ApMinimumLinesNotMetError,
  CompanyNotFoundError,
} from "../lib/errors.generated";
import type {
  BusinessPartnerQueries,
  CoaManagementQueries,
  OrganizationQueries,
  PrimitivesQueries,
} from "../module";
import {
  type AccountPayableDistributionLineInput,
  type AccountPayableDocumentLineInput,
  type AccountPayableDocumentType,
  type AccountPayableDueScheduleLineInput,
  validateDueSchedule,
  validateLineSource,
} from "./createAccountPayableDocument";

export type AccountPayableCorrectionType = "PRICE" | "QUANTITY" | "AMOUNT";

export interface AccountPayableCorrectionDistributionLineInput extends AccountPayableDistributionLineInput {
  correctionOfDistributionLineId?: string | null;
}

export interface CorrectAccountPayableDocumentHeaderInput {
  payableControlAccountId?: string | null;
  documentType: AccountPayableDocumentType;
  externalDocumentNumber?: string | null;
  documentDate: Date;
  postingDate: Date;
  totalAmount: string;
  description?: string | null;
}

export interface AccountPayableCorrectionLineInput extends AccountPayableDocumentLineInput {
  correctionOfLineId: string;
  correctionType: AccountPayableCorrectionType;
  distributions: AccountPayableCorrectionDistributionLineInput[];
}

export interface CorrectAccountPayableDocumentInput {
  correctionOfId: string;
  header: CorrectAccountPayableDocumentHeaderInput;
  lines: AccountPayableCorrectionLineInput[];
  dueSchedule?: AccountPayableDueScheduleLineInput[];
}

function validatePositiveAmount(identifier: string, amount: string) {
  if (new Decimal(amount).lte(0)) return err(new ApInvalidAmountError(identifier));
  return ok({});
}

function validateLineTotal(
  identifier: string,
  totalAmount: string,
  lines: { grossAmount: string }[],
) {
  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<HCF extends object = object, LCF extends object = object>(
  db: Transaction,
  input: Omit<CorrectAccountPayableDocumentInput, "header" | "lines"> & {
    header: CorrectAccountPayableDocumentHeaderInput & HCF;
    lines: (AccountPayableCorrectionLineInput & LCF)[];
  },
  ctx: CommandContext,
  organizationQueries: Pick<OrganizationQueries, "getCompany">,
  businessPartnerQueries: Pick<BusinessPartnerQueries, "getPartner">,
  primitivesQueries: Pick<PrimitivesQueries, "getCurrency">,
  coaManagementQueries: Pick<CoaManagementQueries, "listAccounts">,
) {
  const correctedDocument = await db
    .selectFrom("AccountPayableDocument")
    .selectAll()
    .where("id", "=", input.correctionOfId)
    .executeTakeFirst();
  if (!correctedDocument) return err(new ApDocumentNotFoundError(input.correctionOfId));
  if (correctedDocument.status !== "POSTED") {
    return err(new ApInvalidDocumentStatusError(input.correctionOfId));
  }

  const correctedLines = await db
    .selectFrom("AccountPayableDocumentLine")
    .selectAll()
    .where("accountPayableDocumentId", "=", input.correctionOfId)
    .execute();
  const correctedLineById = new Map(correctedLines.map((line) => [line.id, line]));

  const correctedDistributions = await db
    .selectFrom("AccountPayableDistributionLine as distribution")
    .innerJoin(
      "AccountPayableDocumentLine as line",
      "line.id",
      "distribution.accountPayableDocumentLineId",
    )
    .selectAll("distribution")
    .where("line.accountPayableDocumentId", "=", input.correctionOfId)
    .execute();
  const correctedDistributionById = new Map(
    correctedDistributions.map((distribution) => [distribution.id, distribution]),
  );

  const correctionLines: (AccountPayableCorrectionLineInput & LCF)[] = [];
  for (const line of input.lines) {
    const { correctionOfLineId, correctionType } = line;
    const correctedLine = correctedLineById.get(correctionOfLineId);
    if (!correctedLine) return err(new ApDocumentLineNotFoundError(correctionOfLineId));

    if (correctionType !== "AMOUNT" && correctedLine.sourceType !== "PURCHASE_ORDER") {
      return err(new ApLineSourceInconsistentError(correctionOfLineId));
    }
    if (
      (line.sourceType != null && line.sourceType !== correctedLine.sourceType) ||
      (line.purchaseOrderLineId != null &&
        line.purchaseOrderLineId !== correctedLine.purchaseOrderLineId) ||
      (line.unitId != null && line.unitId !== correctedLine.unitId)
    ) {
      return err(new ApLineSourceInconsistentError(correctionOfLineId));
    }

    for (const distribution of line.distributions) {
      const correctedDistributionId = distribution.correctionOfDistributionLineId;
      if (!correctedDistributionId) continue;
      const correctedDistribution = correctedDistributionById.get(correctedDistributionId);
      if (
        !correctedDistribution ||
        correctedDistribution.accountPayableDocumentLineId !== correctionOfLineId
      ) {
        return err(new ApDistributionLineNotFoundError(correctedDistributionId));
      }
    }

    correctionLines.push({
      ...line,
      sourceType: line.sourceType ?? correctedLine.sourceType,
      purchaseOrderLineId: line.purchaseOrderLineId ?? correctedLine.purchaseOrderLineId,
      unitId: line.unitId ?? correctedLine.unitId,
      distributions: line.distributions.map((distribution) => ({
        ...distribution,
        correctionOfDistributionLineId: distribution.correctionOfDistributionLineId ?? null,
      })),
    });
  }

  const {
    payableControlAccountId: inputPayableControlAccountId,
    documentType,
    externalDocumentNumber,
    documentDate,
    postingDate,
    totalAmount,
    description,
    ...headerCustomFields
  } = input.header;
  const payableControlAccountId =
    inputPayableControlAccountId ?? correctedDocument.payableControlAccountId;

  const { company } = (
    await organizationQueries.getCompany(db, { id: correctedDocument.companyId }, ctx)
  ).value;
  if (!company) return err(new CompanyNotFoundError(correctedDocument.companyId));

  const { partner } = (
    await businessPartnerQueries.getPartner(db, { id: correctedDocument.businessPartnerId }, ctx)
  ).value;
  if (!partner) {
    return err(new ApBusinessPartnerNotFoundError(correctedDocument.businessPartnerId));
  }

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

  if (correctionLines.length === 0) return err(new ApMinimumLinesNotMetError("AP correction"));

  const totalResult = validatePositiveAmount("AP correction", totalAmount);
  if (!totalResult.ok) return totalResult;

  for (const [index, line] of correctionLines.entries()) {
    const identifier = String(index + 1);
    const netAmountResult = validatePositiveAmount(identifier, line.netAmount);
    if (!netAmountResult.ok) return netAmountResult;

    const grossAmountResult = validatePositiveAmount(identifier, line.grossAmount);
    if (!grossAmountResult.ok) return grossAmountResult;

    const lineSourceResult = validateLineSource(identifier, line);
    if (!lineSourceResult.ok) return lineSourceResult;
  }

  const lineTotalResult = validateLineTotal("AP correction", totalAmount, correctionLines);
  if (!lineTotalResult.ok) return lineTotalResult;

  const dueScheduleResult = validateDueSchedule(
    "AP correction",
    totalAmount,
    input.dueSchedule ?? [],
    { requireLines: false },
  );
  if (!dueScheduleResult.ok) return dueScheduleResult;

  const accountIds = [
    ...new Set([
      payableControlAccountId,
      ...correctionLines.flatMap((line) =>
        line.distributions.map((distribution) => distribution.accountId),
      ),
    ]),
  ];
  const accounts = (
    await coaManagementQueries.listAccounts(
      db,
      { companyId: correctedDocument.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));
  }

  for (const [index, line] of correctionLines.entries()) {
    const distributionTotalResult = validateDistributionTotal(
      String(index + 1),
      line.grossAmount,
      line.distributions,
    );
    if (!distributionTotalResult.ok) return distributionTotalResult;

    for (const distribution of line.distributions) {
      const amountResult = validatePositiveAmount(distribution.accountId, distribution.amount);
      if (!amountResult.ok) return amountResult;
    }
  }

  const document = await db
    .insertInto("AccountPayableDocument")
    .values({
      ...(headerCustomFields as Record<string, unknown>),
      companyId: correctedDocument.companyId,
      businessPartnerId: correctedDocument.businessPartnerId,
      currencyId: correctedDocument.currencyId,
      payableControlAccountId,
      documentType,
      correctionOfId: correctedDocument.id,
      externalDocumentNumber: externalDocumentNumber ?? null,
      documentDate,
      postingDate,
      totalAmount,
      description: description ?? null,
      status: "DRAFT",
      registeredAt: null,
      postedAt: null,
      cancelledAt: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  const lines = await db
    .insertInto("AccountPayableDocumentLine")
    .values(
      correctionLines.map((line) => {
        const {
          netAmount,
          taxAmount,
          grossAmount,
          correctionOfLineId,
          correctionType,
          distributions: _distributions,
          description,
          sourceType,
          purchaseOrderLineId,
          quantity,
          unitPrice,
          unitId,
          ...lineCustomFields
        } = line;
        return {
          ...(lineCustomFields as Record<string, unknown>),
          accountPayableDocumentId: document.id,
          netAmount,
          taxAmount: taxAmount ?? null,
          grossAmount,
          correctionOfLineId,
          correctionType,
          description: description ?? null,
          sourceType: sourceType ?? null,
          purchaseOrderLineId: purchaseOrderLineId ?? null,
          quantity: quantity ?? null,
          unitPrice: unitPrice ?? null,
          unitId: unitId ?? null,
        };
      }),
    )
    .returningAll()
    .execute();

  const distributions = [];
  for (const [index, line] of correctionLines.entries()) {
    const insertedLine = lines[index];
    if (!insertedLine) return err(new ApDocumentLineNotFoundError(String(index + 1)));
    distributions.push(
      ...line.distributions.map((distribution) => ({
        accountPayableDocumentLineId: insertedLine.id,
        accountId: distribution.accountId,
        amount: distribution.amount,
        correctionOfDistributionLineId: distribution.correctionOfDistributionLineId ?? null,
        description: distribution.description ?? null,
      })),
    );
  }

  await db.insertInto("AccountPayableDistributionLine").values(distributions).execute();

  if (input.dueSchedule && input.dueSchedule.length > 0) {
    await db
      .insertInto("AccountPayableDueScheduleLine")
      .values(
        input.dueSchedule.map((line) => ({
          accountPayableDocumentId: document.id,
          dueDate: line.dueDate,
          amount: line.amount,
        })),
      )
      .execute();
  }

  return ok({ accountPayableDocument: document });
}
