import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CompanyNotFoundError,
  AccountingPeriodNotFoundError,
  AccountingPeriodCompanyMismatchError,
  InvalidEntryDateError,
  AccountNotFoundError,
  AccountInactiveError,
  InvalidDebitCreditError,
  MinimumLinesNotMetError,
} from "../lib/errors.generated";
import type { CoaManagementQueries, OrganizationQueries } from "../module";

export interface JournalEntryHeaderInput {
  companyId: string;
  accountingPeriodId: string;
  entryDate: Date;
  description?: string;
  sourceDocumentType?:
    | "ACCOUNT_PAYABLE_DOCUMENT"
    | "PURCHASE_BILL"
    | "SALES_INVOICE"
    | "INVENTORY_EXECUTION"
    | "PRODUCTION_ORDER"
    | "YEAR_END_CLOSE";
  sourceDocumentId?: string;
}

export interface CreateJournalEntryInput {
  header: JournalEntryHeaderInput;
  lines: JournalLineInput[];
}

export interface JournalLineInput {
  accountId: string;
  debitAmount: string | null;
  creditAmount: string | null;
  description: string | null;
}

function validateDebitCredit(
  identifier: string,
  debitAmount: string | null,
  creditAmount: string | null,
) {
  if (debitAmount != null && creditAmount != null) {
    return err(new InvalidDebitCreditError(identifier));
  }

  if (debitAmount == null && creditAmount == null) {
    return err(new InvalidDebitCreditError(identifier));
  }

  if (debitAmount != null && new Decimal(debitAmount).lte(0)) {
    return err(new InvalidDebitCreditError(identifier));
  }

  if (creditAmount != null && new Decimal(creditAmount).lte(0)) {
    return err(new InvalidDebitCreditError(identifier));
  }

  return ok({});
}

/**
 * Function: createJournalEntry
 *
 * Establishes a new journal entry in DRAFT status within the general ledger.
 * Validates company existence, accounting period, and entry date before creating.
 */
export async function run<HCF extends object = object, LCF extends object = object>(
  db: Transaction,
  input: Omit<CreateJournalEntryInput, "header" | "lines"> & {
    header: JournalEntryHeaderInput & HCF;
    lines: (JournalLineInput & LCF)[];
  },
  ctx: CommandContext,
  organizationQueries: Pick<OrganizationQueries, "getCompany">,
  coaManagementQueries: Pick<CoaManagementQueries, "listAccounts">,
) {
  const {
    companyId,
    accountingPeriodId,
    entryDate,
    description,
    sourceDocumentType,
    sourceDocumentId,
    ...headerCustomFields
  } = input.header;
  const { lines } = input;

  // 1. Validate company exists.
  const { company } = (await organizationQueries.getCompany(db, { id: companyId }, ctx)).value;

  if (!company) {
    return err(new CompanyNotFoundError(companyId));
  }

  // 2. Validate accounting period exists and belongs to the same company.
  const period = await db
    .selectFrom("AccountingPeriod")
    .selectAll()
    .where("id", "=", accountingPeriodId)
    .executeTakeFirst();

  if (!period) {
    return err(new AccountingPeriodNotFoundError(accountingPeriodId));
  }

  if (period.companyId !== companyId) {
    return err(new AccountingPeriodCompanyMismatchError(accountingPeriodId));
  }

  // 3. Validate entry date
  if (!(entryDate instanceof Date) || isNaN(entryDate.getTime())) {
    return err(new InvalidEntryDateError(String(entryDate)));
  }

  if (lines.length < 2) {
    return err(new MinimumLinesNotMetError(accountingPeriodId));
  }

  const accountIds = [...new Set(lines.map((line) => line.accountId))];
  const accounts = (
    await coaManagementQueries.listAccounts(
      db,
      { companyId, accountIds, limit: accountIds.length },
      ctx,
    )
  ).value.items;
  const accountById = new Map(accounts.map((account) => [account.id, account]));

  for (const line of lines) {
    const account = accountById.get(line.accountId);
    if (!account) {
      return err(new AccountNotFoundError(line.accountId));
    }
    if (account.status !== "ACTIVE") {
      return err(new AccountInactiveError(line.accountId));
    }

    const debitCreditResult = validateDebitCredit(
      accountingPeriodId,
      line.debitAmount,
      line.creditAmount,
    );
    if (!debitCreditResult.ok) return debitCreditResult;
  }

  // 4. Create journal entry in DRAFT status
  const journalEntry = await db
    .insertInto("JournalEntry")
    .values({
      ...headerCustomFields,
      companyId,
      accountingPeriodId,
      entryDate,
      status: "DRAFT",
      description: description ?? null,
      sourceDocumentType: sourceDocumentType ?? null,
      sourceDocumentId: sourceDocumentId ?? null,
      reversalOfId: null,
      postedAt: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  await db
    .insertInto("JournalLine")
    .values(
      lines.map((line) => {
        const { accountId, debitAmount, creditAmount, description, ...lineCustomFields } = line;
        return {
          ...lineCustomFields,
          journalEntryId: journalEntry.id,
          accountId,
          debitAmount,
          creditAmount,
          description,
        };
      }),
    )
    .returningAll()
    .execute();

  return ok({ journalEntry });
}
