import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  NoActiveChartOfAccountsError,
  AccountingPeriodNotFoundError,
  InvalidVariantError,
  TrialBalanceImbalancedError,
} from "../lib/errors.generated";

export interface GenerateTrialBalanceInput {
  companyId: string;
  accountingPeriodId: string;
  variant: string;
}

const VALID_VARIANTS = ["PRE_CLOSE", "POST_CLOSE"] as const;

/**
 * Function: generateTrialBalance
 *
 * Creates a point-in-time snapshot report of GL account balances for a specific
 * accounting period. Includes opening balance, period debits, credits, closing
 * balance for each account organized by account group hierarchy. Supports
 * PRE_CLOSE and POST_CLOSE variants.
 */
export async function run(db: Transaction, input: GenerateTrialBalanceInput, ctx: CommandContext) {
  const { companyId, accountingPeriodId, variant } = input;

  // 1. Find the active ChartOfAccounts for the company
  const chartOfAccounts = await db
    .selectFrom("ChartOfAccounts")
    .selectAll()
    .where("companyId", "=", companyId)
    .where("status", "=", "ACTIVE")
    .executeTakeFirst();

  if (!chartOfAccounts) {
    return err(new NoActiveChartOfAccountsError(companyId));
  }

  // 2. Find the AccountingPeriod
  const accountingPeriod = await db
    .selectFrom("AccountingPeriod")
    .selectAll()
    .where("id", "=", accountingPeriodId)
    .executeTakeFirst();

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

  // 3. Validate variant is PRE_CLOSE or POST_CLOSE
  if (!VALID_VARIANTS.includes(variant as (typeof VALID_VARIANTS)[number])) {
    return err(new InvalidVariantError(variant));
  }

  // 4. Fetch all accounts from the CoA, sorted by account code ascending
  const accountsRaw = await db
    .selectFrom("Account")
    .selectAll()
    .where("status", "=", "ACTIVE")
    .execute();

  const accounts = [
    ...(accountsRaw as {
      id: string;
      code: string | null;
      name: string | null;
      classification: string | null;
      accountGroupId?: string | null;
    }[]),
  ].sort((a, b) => (a.code ?? "").localeCompare(b.code ?? ""));

  // 5. Fetch posted journal lines for the period
  const journalEntries = await db
    .selectFrom("JournalEntry")
    .selectAll()
    .where("accountingPeriodId", "=", accountingPeriodId)
    .where("status", "=", "POSTED")
    .execute();

  // 6. For PRE_CLOSE, exclude CLOSING journal type entries; for POST_CLOSE include all
  const filteredEntries =
    variant === "PRE_CLOSE"
      ? journalEntries.filter((e: { journalType: string }) => e.journalType !== "CLOSING")
      : journalEntries;

  const entryIds = new Set(filteredEntries.map((e: { id: string }) => e.id));

  const allJournalLines = await db
    .selectFrom("JournalLine")
    .selectAll()
    .where("journalEntryId", "in", [...entryIds])
    .execute();

  const journalLines = allJournalLines.filter((line: { journalEntryId: string }) =>
    entryIds.has(line.journalEntryId),
  );

  // 7. Fetch opening balances from the immediately preceding period's trial balance
  const precedingPeriod = await db
    .selectFrom("AccountingPeriod")
    .selectAll()
    .where("companyId", "=", companyId)
    .where("endDate", "<", accountingPeriod.startDate)
    .orderBy("endDate", "desc")
    .executeTakeFirst();

  const priorBalanceByAccount = new Map<string, number>();

  if (precedingPeriod) {
    const precedingTrialBalance = await db
      .selectFrom("TrialBalance")
      .selectAll()
      .where("companyId", "=", companyId)
      .where("chartOfAccountsId", "=", chartOfAccounts.id)
      .where("accountingPeriodId", "=", precedingPeriod.id)
      .where("variant", "=", variant as "PRE_CLOSE" | "POST_CLOSE")
      .executeTakeFirst();

    if (precedingTrialBalance) {
      const priorLines = await db
        .selectFrom("TrialBalanceLine")
        .selectAll()
        .where("trialBalanceId", "=", precedingTrialBalance.id)
        .execute();

      for (const line of priorLines) {
        priorBalanceByAccount.set(line.accountId, line.closingBalance ?? 0);
      }
    }
  }

  // Calculate per-account balances
  const linesByAccount = new Map<string, { debits: number; credits: number }>();
  for (const line of journalLines) {
    const accountId = line.accountId;
    const current = linesByAccount.get(accountId) ?? { debits: 0, credits: 0 };
    current.debits += line.functionalDebitAmount ?? 0;
    current.credits += line.functionalCreditAmount ?? 0;
    linesByAccount.set(accountId, current);
  }

  // Build trial balance lines for all accounts (including zero-balance),
  // accounts are already sorted by code ascending.
  let totalDebits = 0;
  let totalCredits = 0;

  const trialBalanceLines = accounts.map(
    (
      account: {
        id: string;
        code: string | null;
        name: string | null;
        classification: string | null;
        accountGroupId?: string | null;
      },
      index: number,
    ) => {
      const openingBalance = priorBalanceByAccount.get(account.id) ?? 0;
      const periodData = linesByAccount.get(account.id) ?? { debits: 0, credits: 0 };
      const periodDebits = periodData.debits;
      const periodCredits = periodData.credits;
      let closingBalance = openingBalance + periodDebits - periodCredits;

      // POST_CLOSE variant: zero out balances for temporary accounts (revenue and expense)
      if (
        variant === "POST_CLOSE" &&
        (account.classification === "REVENUE" || account.classification === "EXPENSE")
      ) {
        closingBalance = 0;
      }

      totalDebits += periodDebits;
      totalCredits += periodCredits;

      return {
        accountId: account.id,
        accountGroupId: account.accountGroupId ?? null,
        accountCode: account.code ?? "",
        accountName: account.name ?? "",
        openingBalance,
        periodDebits,
        periodCredits,
        closingBalance,
        sortOrder: index + 1,
      };
    },
  );

  // 8. Check equilibrium: totalDebits == totalCredits
  if (totalDebits !== totalCredits) {
    const discrepancy = Math.abs(totalDebits - totalCredits);
    return err(new TrialBalanceImbalancedError(`${companyId} (discrepancy: ${discrepancy})`));
  }

  // 9. Upsert: delete existing TrialBalance (and lines) for same period/variant if any
  const existingTrialBalance = await db
    .selectFrom("TrialBalance")
    .selectAll()
    .where("companyId", "=", companyId)
    .where("accountingPeriodId", "=", accountingPeriodId)
    .where("chartOfAccountsId", "=", chartOfAccounts.id)
    .where("variant", "=", variant as "PRE_CLOSE" | "POST_CLOSE")
    .executeTakeFirst();

  if (existingTrialBalance) {
    await db
      .deleteFrom("TrialBalanceLine")
      .where("trialBalanceId", "=", existingTrialBalance.id)
      .execute();

    await db.deleteFrom("TrialBalance").where("id", "=", existingTrialBalance.id).execute();
  }

  // 10. Insert TrialBalance record
  const now = new Date();
  const trialBalance = await db
    .insertInto("TrialBalance")
    .values({
      companyId,
      accountingPeriodId,
      chartOfAccountsId: chartOfAccounts.id,
      variant: variant as "PRE_CLOSE" | "POST_CLOSE",
      generatedAt: now,
      totalDebits,
      totalCredits,
      isBalanced: true,
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  // 11. Insert TrialBalanceLine records for each account
  for (const line of trialBalanceLines) {
    await db
      .insertInto("TrialBalanceLine")
      .values({
        trialBalanceId: trialBalance.id,
        accountId: line.accountId,
        accountGroupId: line.accountGroupId,
        accountCode: line.accountCode,
        accountName: line.accountName,
        openingBalance: line.openingBalance,
        periodDebits: line.periodDebits,
        periodCredits: line.periodCredits,
        closingBalance: line.closingBalance,
        sortOrder: line.sortOrder,
        updatedAt: null,
      })
      .returningAll()
      .executeTakeFirstOrThrow();
  }

  // 12. Emit audit event and return
  const auditEvent = {
    type: "TRIAL_BALANCE_GENERATED",
    variant,
    actorId: ctx.actorId,
    timestamp: now,
    trialBalanceId: trialBalance.id,
    totalDebits,
    totalCredits,
    isBalanced: true,
  };

  return ok({ trialBalance, auditEvent });
}
