import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  JournalEntryNotFoundError,
  InvalidStatusForPostingError,
  MinimumLinesNotMetError,
  UnbalancedEntryError,
  InvalidPeriodStatusError,
  InactiveAccountError,
  SuccessorAccountInvalidError,
} from "../lib/errors.generated";

export interface PostJournalEntryInput {
  id: string;
}

interface AuditEntry {
  type: string;
  originalAccountId?: string;
  successorAccountId?: string;
  journalLineId?: string;
  from?: string;
  to?: string;
  actorId?: string;
}

const VALID_PERIOD_STATUSES = ["OPEN", "FUTURE_ENTERABLE"];

/**
 * Function: postJournalEntry
 *
 * Transitions a journal entry from DRAFT to POSTED status, making it an
 * immutable part of the general ledger. Validates accounting constraints
 * including balance, period status, and account status.
 */
export async function run(db: Transaction, input: PostJournalEntryInput, ctx: CommandContext) {
  const { id } = input;
  const auditTrail: AuditEntry[] = [];

  // 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
  if (journalEntry.status !== "DRAFT") {
    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 functional debits must equal total functional credits
  const totalDebits = journalLines.reduce(
    (sum, line) => sum + (line.functionalDebitAmount ?? 0),
    0,
  );
  const totalCredits = journalLines.reduce(
    (sum, line) => sum + (line.functionalCreditAmount ?? 0),
    0,
  );

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

  // 8. Validate all GL accounts and handle successor redirects
  for (const line of journalLines) {
    const account = await db
      .selectFrom("Account")
      .selectAll()
      .where("id", "=", line.accountId)
      .executeTakeFirst();

    if (!account) {
      return err(new InactiveAccountError(line.accountId));
    }

    if (account.status === "DEPRECATED" && account.successorAccountId) {
      // Resolve successor
      const successor = await db
        .selectFrom("Account")
        .selectAll()
        .where("id", "=", account.successorAccountId)
        .executeTakeFirst();

      if (successor?.status !== "ACTIVE") {
        return err(new SuccessorAccountInvalidError(account.successorAccountId ?? line.accountId));
      }

      // Redirect journal line to successor account
      await db
        .updateTable("JournalLine")
        .set({ accountId: successor.id, updatedAt: new Date() })
        .where("id", "=", line.id)
        .returningAll()
        .executeTakeFirst();

      auditTrail.push({
        type: "SUCCESSOR_REDIRECT",
        originalAccountId: account.id,
        successorAccountId: successor.id,
        journalLineId: line.id,
      });
    } else if (account.status !== "ACTIVE") {
      return err(new InactiveAccountError(line.accountId));
    }
  }

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

  // 10. Record audit event
  auditTrail.push({
    type: "STATUS_TRANSITION",
    from: "DRAFT",
    to: "POSTED",
    actorId: ctx.actorId,
  });

  return ok({ journalEntry: updatedEntry!, auditTrail });
}
