import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  AccountingPeriodNotFoundError,
  NotFinalPeriodError,
  StandardCloseNotCompleteError,
  DuplicateYearEndCloseError,
  RetainedEarningsAccountNotFoundError,
  NewFiscalYearNotDefinedError,
} from "../lib/errors.generated";

export interface ExecuteYearEndCloseInput {
  accountingPeriodId: string;
}

interface AuditEntry {
  type: string;
  fiscalYearId?: string;
  closingEntryId?: string;
  openingEntryId?: string;
  netIncome?: number;
  actorId?: string;
}

const TEMPORARY_CLASSIFICATIONS = ["REVENUE", "EXPENSE"];
const PERMANENT_CLASSIFICATIONS = ["ASSET", "LIABILITY", "EQUITY"];
const VALID_CLOSE_STATUSES = ["SOFT_LOCKED", "PERMANENTLY_CLOSED"];

/**
 * Function: executeYearEndClose
 *
 * Executes fiscal year-end close on the final period of a fiscal year.
 * Generates a closing journal entry that zeros temporary accounts (REVENUE, EXPENSE)
 * and transfers net income/loss to retained earnings. Then generates opening balance
 * entries for the new fiscal year's permanent accounts.
 */
export async function run(db: Transaction, input: ExecuteYearEndCloseInput, ctx: CommandContext) {
  const { accountingPeriodId } = input;
  const auditTrail: AuditEntry[] = [];

  // 1. Find AccountingPeriod
  const period = await db
    .selectFrom("AccountingPeriod")
    .selectAll()
    .where("id", "=", accountingPeriodId)
    .executeTakeFirst();

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

  // 2. Find FiscalYear for period
  const fiscalYear = await db
    .selectFrom("FiscalYear")
    .selectAll()
    .where("id", "=", period.fiscalYearId)
    .forUpdate()
    .executeTakeFirst();

  // 3. Validate this is the final period of the fiscal year
  const periodEnd = new Date(period.endDate).getTime();
  const fyEnd = new Date(fiscalYear!.endDate).getTime();
  if (periodEnd !== fyEnd) {
    return err(new NotFinalPeriodError(accountingPeriodId));
  }

  // 4. Check PeriodClose exists and is in SOFT_LOCKED or PERMANENTLY_CLOSED status
  const periodClose = await db
    .selectFrom("PeriodClose")
    .selectAll()
    .where("accountingPeriodId", "=", accountingPeriodId)
    .executeTakeFirst();

  if (!periodClose || !VALID_CLOSE_STATUSES.includes(periodClose.status as string)) {
    return err(new StandardCloseNotCompleteError(accountingPeriodId));
  }

  // 5. Check fiscal year yearEndCloseExecuted flag
  if (fiscalYear!.yearEndCloseExecuted) {
    return err(new DuplicateYearEndCloseError(fiscalYear!.id));
  }

  // 6. Find retained earnings account in active CoA
  const _chartOfAccounts = await db
    .selectFrom("ChartOfAccounts")
    .selectAll()
    .where("companyId", "=", period.companyId)
    .where("status", "=", "ACTIVE")
    .executeTakeFirst();

  const retainedEarningsAccount = await db
    .selectFrom("Account")
    .selectAll()
    .where("classification", "=", "RETAINED_EARNINGS")
    .where("status", "=", "ACTIVE")
    .executeTakeFirst();

  if (!retainedEarningsAccount) {
    return err(new RetainedEarningsAccountNotFoundError(period.companyId));
  }

  // 7. Find all posted journal lines for temporary accounts in this fiscal year
  // Get all accounting period IDs for this fiscal year
  const allPeriods = await db
    .selectFrom("AccountingPeriod")
    .select("id")
    .where("fiscalYearId", "=", fiscalYear!.id)
    .execute();

  const allPeriodIds = allPeriods.map((p: { id: string }) => p.id);

  // Find all posted journal lines for temporary accounts across ALL periods in the fiscal year
  const journalLines = await db
    .selectFrom("JournalLine")
    .innerJoin("JournalEntry", "JournalEntry.id", "JournalLine.journalEntryId")
    .innerJoin("Account", "Account.id", "JournalLine.accountId")
    .selectAll()
    .where("JournalEntry.accountingPeriodId", "in", allPeriodIds)
    .where("JournalEntry.status", "=", "POSTED")
    .execute();

  // Filter for temporary account lines
  const temporaryLines = journalLines.filter((line: Record<string, unknown>) =>
    TEMPORARY_CLASSIFICATIONS.includes(line.classification as string),
  );

  // 8. Calculate net income
  const revenueLines = temporaryLines.filter(
    (line: Record<string, unknown>) => line.classification === "REVENUE",
  );
  const expenseLines = temporaryLines.filter(
    (line: Record<string, unknown>) => line.classification === "EXPENSE",
  );

  const totalRevenueCredits = revenueLines.reduce(
    (sum: number, line: Record<string, unknown>) =>
      sum + ((line.functionalCreditAmount as number) ?? 0),
    0,
  );
  const totalRevenueDebits = revenueLines.reduce(
    (sum: number, line: Record<string, unknown>) =>
      sum + ((line.functionalDebitAmount as number) ?? 0),
    0,
  );
  const totalExpenseDebits = expenseLines.reduce(
    (sum: number, line: Record<string, unknown>) =>
      sum + ((line.functionalDebitAmount as number) ?? 0),
    0,
  );
  const totalExpenseCredits = expenseLines.reduce(
    (sum: number, line: Record<string, unknown>) =>
      sum + ((line.functionalCreditAmount as number) ?? 0),
    0,
  );

  const netIncome =
    totalRevenueCredits - totalRevenueDebits - (totalExpenseDebits - totalExpenseCredits);

  // 9. Create CLOSING type journal entry
  const now = new Date();
  const closingEntry = await db
    .insertInto("JournalEntry")
    .values({
      companyId: period.companyId,
      accountingPeriodId,
      entryDate: now,
      journalType: "CLOSING",
      referenceNumber: `CLOSE-${fiscalYear!.id}`,
      status: "POSTED",
      description: `Year-end closing entry for ${fiscalYear!.name}`,
      sourceDocumentReference: null,
      adjustingEntryType: null,
      reversalOfId: null,
      postedAt: now,
      createdAt: now,
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

  // Create closing journal lines: debit revenue accounts, credit expense accounts, net to retained earnings
  const closingLines: {
    journalEntryId: string;
    accountId: string;
    debitAmount: number | null;
    creditAmount: number | null;
    description: string;
    currencyCode: null;
    exchangeRate: null;
    functionalDebitAmount: number | null;
    functionalCreditAmount: number | null;
    createdAt: Date;
    updatedAt: null;
  }[] = [];

  // Debit revenue accounts (to zero them out)
  for (const line of revenueLines) {
    closingLines.push({
      journalEntryId: closingEntry!.id,
      accountId: line.accountId,
      debitAmount: line.functionalCreditAmount! ?? 0,
      creditAmount: null,
      description: "Close revenue account",
      currencyCode: null,
      exchangeRate: null,
      functionalDebitAmount: line.functionalCreditAmount! ?? 0,
      functionalCreditAmount: null,
      createdAt: now,
      updatedAt: null,
    });
  }

  // Credit expense accounts (to zero them out)
  for (const line of expenseLines) {
    closingLines.push({
      journalEntryId: closingEntry!.id,
      accountId: line.accountId,
      debitAmount: null,
      creditAmount: line.functionalDebitAmount! ?? 0,
      description: "Close expense account",
      currencyCode: null,
      exchangeRate: null,
      functionalDebitAmount: null,
      functionalCreditAmount: line.functionalDebitAmount! ?? 0,
      createdAt: now,
      updatedAt: null,
    });
  }

  // Net income/loss to retained earnings
  if (netIncome >= 0) {
    closingLines.push({
      journalEntryId: closingEntry!.id,
      accountId: retainedEarningsAccount.id,
      debitAmount: null,
      creditAmount: netIncome,
      description: "Transfer net income to retained earnings",
      currencyCode: null,
      exchangeRate: null,
      functionalDebitAmount: null,
      functionalCreditAmount: netIncome,
      createdAt: now,
      updatedAt: null,
    });
  } else {
    closingLines.push({
      journalEntryId: closingEntry!.id,
      accountId: retainedEarningsAccount.id,
      debitAmount: Math.abs(netIncome),
      creditAmount: null,
      description: "Transfer net loss to retained earnings",
      currencyCode: null,
      exchangeRate: null,
      functionalDebitAmount: Math.abs(netIncome),
      functionalCreditAmount: null,
      createdAt: now,
      updatedAt: null,
    });
  }

  if (closingLines.length > 0) {
    await db.insertInto("JournalLine").values(closingLines).returningAll().execute();
  }

  // 10. Find next fiscal year
  const nextFiscalYear = await db
    .selectFrom("FiscalYear")
    .selectAll()
    .where("companyId", "=", period.companyId)
    .where("startDate", ">", fiscalYear!.endDate)
    .orderBy("startDate", "asc")
    .limit(1)
    .executeTakeFirst();

  if (!nextFiscalYear) {
    return err(new NewFiscalYearNotDefinedError(fiscalYear!.id));
  }

  // 11. Generate OPENING type journal entry for permanent accounts in new fiscal year
  const permanentLines = journalLines.filter((line: Record<string, unknown>) =>
    PERMANENT_CLASSIFICATIONS.includes(line.classification as string),
  );

  // Find first period of new fiscal year
  const newFirstPeriod = await db
    .selectFrom("AccountingPeriod")
    .selectAll()
    .where("fiscalYearId", "=", nextFiscalYear.id)
    .orderBy("startDate", "asc")
    .limit(1)
    .executeTakeFirst();

  const openingEntry = await db
    .insertInto("JournalEntry")
    .values({
      companyId: period.companyId,
      accountingPeriodId: newFirstPeriod ? newFirstPeriod.id : nextFiscalYear.id,
      entryDate: now,
      journalType: "OPENING",
      referenceNumber: `OPEN-${nextFiscalYear.id}`,
      status: "POSTED",
      description: `Opening balance entry for ${nextFiscalYear.name}`,
      sourceDocumentReference: null,
      adjustingEntryType: null,
      reversalOfId: null,
      postedAt: now,
      createdAt: now,
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

  // Create opening journal lines for permanent accounts
  const openingLines: {
    journalEntryId: string;
    accountId: string;
    debitAmount: number | null;
    creditAmount: number | null;
    description: string;
    currencyCode: null;
    exchangeRate: null;
    functionalDebitAmount: number | null;
    functionalCreditAmount: number | null;
    createdAt: Date;
    updatedAt: null;
  }[] = [];
  for (const line of permanentLines) {
    openingLines.push({
      journalEntryId: openingEntry!.id,
      accountId: line.accountId,
      debitAmount: line.functionalDebitAmount! ?? null,
      creditAmount: line.functionalCreditAmount! ?? null,
      description: "Opening balance",
      currencyCode: null,
      exchangeRate: null,
      functionalDebitAmount: line.functionalDebitAmount! ?? null,
      functionalCreditAmount: line.functionalCreditAmount! ?? null,
      createdAt: now,
      updatedAt: null,
    });
  }

  // Include retained earnings in opening entry
  if (netIncome >= 0) {
    openingLines.push({
      journalEntryId: openingEntry!.id,
      accountId: retainedEarningsAccount.id,
      debitAmount: null,
      creditAmount: netIncome,
      description: "Opening retained earnings balance",
      currencyCode: null,
      exchangeRate: null,
      functionalDebitAmount: null,
      functionalCreditAmount: netIncome,
      createdAt: now,
      updatedAt: null,
    });
  } else {
    openingLines.push({
      journalEntryId: openingEntry!.id,
      accountId: retainedEarningsAccount.id,
      debitAmount: Math.abs(netIncome),
      creditAmount: null,
      description: "Opening retained earnings balance",
      currencyCode: null,
      exchangeRate: null,
      functionalDebitAmount: Math.abs(netIncome),
      functionalCreditAmount: null,
      createdAt: now,
      updatedAt: null,
    });
  }

  if (openingLines.length > 0) {
    await db.insertInto("JournalLine").values(openingLines).returningAll().execute();
  }

  // 12. Update fiscal year yearEndCloseExecuted = true
  await db
    .updateTable("FiscalYear")
    .set({
      yearEndCloseExecuted: true,
      updatedAt: now,
    })
    .where("id", "=", fiscalYear!.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  // 13. Update PeriodClose yearEndCloseExecuted = true
  await db
    .updateTable("PeriodClose")
    .set({
      yearEndCloseExecuted: true,
      updatedAt: now,
    })
    .where("id", "=", periodClose.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  // 14. Record audit trail
  auditTrail.push({
    type: "YEAR_END_CLOSE",
    fiscalYearId: fiscalYear!.id,
    closingEntryId: closingEntry!.id,
    openingEntryId: openingEntry!.id,
    netIncome,
    actorId: ctx.actorId,
  });

  return ok({
    closingEntry: closingEntry!,
    openingEntry: openingEntry!,
    auditTrail,
  });
}
