import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  FinancialStatementNotFoundError,
  StatementAlreadyFinalizedError,
  TrialBalanceNotFoundError,
  TrialBalanceImbalancedError,
  ComparativeTrialBalanceNotFoundError,
} from "../lib/errors.generated";

export interface RegenerateFinancialStatementInput {
  id: string;
}

function getSectionType(classification: string, statementType: string): string {
  if (statementType === "BALANCE_SHEET") {
    if (classification === "ASSET") return "Assets";
    if (classification === "LIABILITY") return "Liabilities";
    return "Equity";
  }
  if (statementType === "PROFIT_AND_LOSS") {
    if (classification === "REVENUE") return "Revenue";
    return "Expenses";
  }
  // CASH_FLOW
  if (classification === "REVENUE" || classification === "EXPENSE") return "Operating Activities";
  if (classification === "ASSET") return "Investing Activities";
  return "Financing Activities";
}

/**
 * Function: regenerateFinancialStatement
 *
 * Replaces content of an existing DRAFT financial statement with fresh
 * trial balance data. Only DRAFT statements can be regenerated —
 * FINALIZED statements are immutable. Refetches trial balance data,
 * replaces all line items, updates timestamps, and refreshes
 * comparative data.
 */
export async function run(
  db: Transaction,
  input: RegenerateFinancialStatementInput,
  ctx: CommandContext,
) {
  const { id } = input;
  const auditTrail: { type: string; actorId?: string; timestamp?: Date }[] = [];

  // 1. Find FinancialStatement by id with forUpdate()
  const financialStatement = await db
    .selectFrom("FinancialStatement")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();

  if (!financialStatement) {
    return err(new FinancialStatementNotFoundError(id));
  }

  // 2. Validate status is DRAFT
  if (financialStatement.status !== "DRAFT") {
    return err(new StatementAlreadyFinalizedError(id));
  }

  // 3. Find the trial balance using statement.trialBalanceId
  const trialBalance = await db
    .selectFrom("TrialBalance")
    .selectAll()
    .where("id", "=", financialStatement.trialBalanceId)
    .executeTakeFirst();

  if (!trialBalance) {
    return err(new TrialBalanceNotFoundError(financialStatement.trialBalanceId));
  }

  // 4. Validate trial balance is balanced
  if (!trialBalance.isBalanced || trialBalance.totalDebits !== trialBalance.totalCredits) {
    const discrepancy = Math.abs(trialBalance.totalDebits - trialBalance.totalCredits);
    return err(new TrialBalanceImbalancedError(`${trialBalance.id} (discrepancy: ${discrepancy})`));
  }

  // 5. If comparativePeriodTrialBalanceId is set, validate it exists
  let comparativeLines: Record<string, unknown>[] = [];
  if (financialStatement.comparativePeriodTrialBalanceId) {
    const comparativeTrialBalance = await db
      .selectFrom("TrialBalance")
      .selectAll()
      .where("id", "=", financialStatement.comparativePeriodTrialBalanceId)
      .executeTakeFirst();

    if (!comparativeTrialBalance) {
      return err(
        new ComparativeTrialBalanceNotFoundError(
          financialStatement.comparativePeriodTrialBalanceId,
        ),
      );
    }

    comparativeLines = (await db
      .selectFrom("TrialBalanceLine" as never)
      .where(
        "trialBalanceId" as never,
        "=",
        financialStatement.comparativePeriodTrialBalanceId as never,
      )
      .selectAll()
      .execute()) as Record<string, unknown>[];
  }

  // Build comparative amount map
  const comparativeMap = new Map<string, number>();
  for (const line of comparativeLines) {
    comparativeMap.set(line.accountId as string, line.closingBalance as number);
  }

  // 6. Delete existing line items for this statement
  await db
    .deleteFrom("FinancialStatementLineItem")
    .where("financialStatementId", "=", id)
    .execute();

  // 7. Fetch trial balance lines and rebuild line items
  const trialBalanceLines = (await db
    .selectFrom("TrialBalanceLine")
    .selectAll()
    .where("trialBalanceId", "=", trialBalance.id)
    .orderBy("sortOrder", "asc")
    .execute()) as Record<string, unknown>[];

  const now = new Date();
  let sortOrder = 0;

  // 8. Insert new line items
  for (const tbLine of trialBalanceLines) {
    // Look up the account to get classification for section type
    const account = await db
      .selectFrom("Account" as never)
      .where("id" as never, "=", tbLine.accountId as never)
      .selectAll()
      .executeTakeFirst();

    if (!account) continue;
    const acct = account as Record<string, unknown>;

    sortOrder++;
    await db
      .insertInto("FinancialStatementLineItem")
      .values({
        financialStatementId: id,
        accountId: tbLine.accountId as string,
        accountGroupId: (tbLine.accountGroupId as string | null) ?? null,
        label: tbLine.accountName as string,
        amount: tbLine.closingBalance as number,
        comparativeAmount: comparativeMap.get(tbLine.accountId as string) ?? null,
        lineType: "DETAIL" as never,
        sectionType: getSectionType(
          acct.classification as string,
          financialStatement.statementType,
        ),
        sortOrder,
        createdAt: now,
      })
      .returningAll()
      .executeTakeFirstOrThrow();
  }

  // 9. Update generatedAt timestamp on the statement
  const updatedStatement = await db
    .updateTable("FinancialStatement")
    .set({
      generatedAt: now,
      updatedAt: now,
    })
    .where("id", "=", id)
    .returningAll()
    .executeTakeFirst();

  // 10. Record audit event
  auditTrail.push({
    type: "STATEMENT_REGENERATED",
    actorId: ctx.actorId,
    timestamp: now,
  });

  return ok({ financialStatement: updatedStatement!, auditTrail });
}
