import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { journalEntryLifecycle } from "../db/journalEntry.lifecycle.generated";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  JournalEntryNotFoundError,
  InvalidStatusForPostingError,
  MinimumLinesNotMetError,
  UnbalancedEntryError,
  InvalidPeriodStatusError,
  AccountNotFoundError,
  AccountInactiveError,
} from "../lib/errors.generated";
import type { CoaManagementQueries } from "../module";

export interface PostJournalEntryInput {
  id: string;
}

const VALID_PERIOD_STATUSES = ["OPEN"];

/**
 * Function: postJournalEntry
 *
 * Transitions a journal entry from DRAFT to POSTED status, making its
 * journal lines an immutable part of the general ledger. Validates accounting constraints
 * including balance, period status, and account existence.
 */
export async function run(
  db: Transaction,
  input: PostJournalEntryInput,
  ctx: CommandContext,
  coaManagementQueries: Pick<CoaManagementQueries, "listAccounts">,
) {
  const { id } = input;

  // 1. Find journal entry
  const journalEntry = await db
    .selectFrom("JournalEntry")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();

  if (!journalEntry) {
    return err(new JournalEntryNotFoundError(id));
  }

  // 2. Validate status is DRAFT
  const nextStatus = journalEntryLifecycle.tryTransition(journalEntry.status, "post");
  if (!nextStatus) {
    return err(new InvalidStatusForPostingError(id));
  }

  // 3. Fetch journal lines
  const journalLines = await db
    .selectFrom("JournalLine")
    .selectAll()
    .where("journalEntryId", "=", id)
    .execute();

  // 4. Validate minimum two lines
  if (journalLines.length < 2) {
    return err(new MinimumLinesNotMetError(id));
  }

  // 5. Fetch the accounting period
  const period = await db
    .selectFrom("AccountingPeriod")
    .selectAll()
    .where("id", "=", journalEntry.accountingPeriodId)
    .executeTakeFirst();

  // 6. Validate period status
  if (!period || !VALID_PERIOD_STATUSES.includes(period.status)) {
    return err(new InvalidPeriodStatusError(journalEntry.accountingPeriodId));
  }

  // 7. Validate balance: total debits must equal total credits
  const totalDebits = journalLines.reduce(
    (sum, line) => sum.plus(line.debitAmount ?? 0),
    new Decimal(0),
  );
  const totalCredits = journalLines.reduce(
    (sum, line) => sum.plus(line.creditAmount ?? 0),
    new Decimal(0),
  );

  if (!totalDebits.eq(totalCredits)) {
    return err(new UnbalancedEntryError(id));
  }

  // 8. Validate all GL account references exist and are active.
  const accountIds = [...new Set(journalLines.map((line) => line.accountId))];
  const accounts = (
    await coaManagementQueries.listAccounts(
      db,
      { companyId: journalEntry.companyId, accountIds, limit: accountIds.length },
      ctx,
    )
  ).value.items;
  const accountById = new Map(accounts.map((account) => [account.id, account]));

  for (const line of journalLines) {
    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 now = new Date();

  // 9. Transition entry to POSTED
  const updatedEntry = await db
    .updateTable("JournalEntry")
    .set({
      status: nextStatus,
      postedAt: now,
    })
    .where("id", "=", id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ journalEntry: updatedEntry });
}
