import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CompanyNotFoundError,
  ChartOfAccountsNotFoundError,
  InvalidStatementTypeError,
  InvalidReportingParametersError,
  NoTrialBalanceDataError,
  ComparativeDataUnavailableError,
} from "../lib/errors.generated";

export interface GenerateFinancialStatementInput {
  companyId: string;
  chartOfAccountsId: string;
  trialBalanceId: string;
  statementType: string;
  reportingDate?: Date;
  startDate?: Date;
  endDate?: Date;
  cashFlowMethod?: string;
  comparativePeriodTrialBalanceId?: string;
}

const VALID_STATEMENT_TYPES = ["BALANCE_SHEET", "PROFIT_AND_LOSS", "CASH_FLOW"] as const;

const BALANCE_SHEET_CLASSIFICATIONS = ["ASSET", "LIABILITY", "EQUITY", "RETAINED_EARNINGS"];
const PROFIT_AND_LOSS_CLASSIFICATIONS = ["REVENUE", "EXPENSE"];
const CASH_FLOW_CLASSIFICATIONS = ["ASSET", "LIABILITY", "REVENUE", "EXPENSE"];

function getClassificationsForType(statementType: string): string[] {
  switch (statementType) {
    case "BALANCE_SHEET":
      return BALANCE_SHEET_CLASSIFICATIONS;
    case "PROFIT_AND_LOSS":
      return PROFIT_AND_LOSS_CLASSIFICATIONS;
    case "CASH_FLOW":
      return CASH_FLOW_CLASSIFICATIONS;
    default:
      return [];
  }
}

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";
}

interface AuditEntry {
  type: string;
  statementType?: string;
  actorId?: string;
}

export async function run(
  db: Transaction,
  input: GenerateFinancialStatementInput,
  ctx: CommandContext,
) {
  const auditTrail: AuditEntry[] = [];

  // 1. Validate company exists and is ACTIVE
  const company = await db
    .selectFrom("Company" as never)
    .where("id" as never, "=", input.companyId as never)
    .selectAll()
    .executeTakeFirst();

  if (!company || (company as Record<string, unknown>).status !== "ACTIVE") {
    return err(new CompanyNotFoundError(input.companyId));
  }

  // 2. Validate CoA exists and is ACTIVE
  const coa = await db
    .selectFrom("ChartOfAccounts" as never)
    .where("id" as never, "=", input.chartOfAccountsId as never)
    .selectAll()
    .executeTakeFirst();

  if (!coa || (coa as Record<string, unknown>).status !== "ACTIVE") {
    return err(new ChartOfAccountsNotFoundError(input.chartOfAccountsId));
  }

  // 3. Validate statementType is valid
  if (
    !VALID_STATEMENT_TYPES.includes(input.statementType as (typeof VALID_STATEMENT_TYPES)[number])
  ) {
    return err(new InvalidStatementTypeError(input.statementType));
  }

  // 4. Validate reporting parameters
  if (input.statementType === "BALANCE_SHEET") {
    if (!input.reportingDate) {
      return err(new InvalidReportingParametersError(input.statementType));
    }
  } else {
    // PROFIT_AND_LOSS and CASH_FLOW need startDate + endDate
    if (!input.startDate || !input.endDate) {
      return err(new InvalidReportingParametersError(input.statementType));
    }
    if (input.statementType === "CASH_FLOW" && !input.cashFlowMethod) {
      return err(new InvalidReportingParametersError(input.statementType));
    }
  }

  // 5. Validate trial balance exists
  const trialBalance = await db
    .selectFrom("TrialBalance" as never)
    .where("id" as never, "=", input.trialBalanceId as never)
    .selectAll()
    .executeTakeFirst();

  if (!trialBalance) {
    return err(new NoTrialBalanceDataError(input.trialBalanceId));
  }

  // 6. If comparativePeriodTrialBalanceId, validate it exists
  if (input.comparativePeriodTrialBalanceId) {
    const comparativeTb = await db
      .selectFrom("TrialBalance" as never)
      .where("id" as never, "=", input.comparativePeriodTrialBalanceId as never)
      .selectAll()
      .executeTakeFirst();

    if (!comparativeTb) {
      return err(new ComparativeDataUnavailableError(input.comparativePeriodTrialBalanceId));
    }
  }

  // 7. Fetch trial balance lines
  const trialBalanceLines = (await db
    .selectFrom("TrialBalanceLine" as never)
    .where("trialBalanceId" as never, "=", input.trialBalanceId as never)
    .selectAll()
    .execute()) as Record<string, unknown>[];

  if (trialBalanceLines.length === 0) {
    return err(new NoTrialBalanceDataError(input.trialBalanceId));
  }

  // Fetch comparative lines if applicable
  let comparativeLines: Record<string, unknown>[] = [];
  if (input.comparativePeriodTrialBalanceId) {
    comparativeLines = (await db
      .selectFrom("TrialBalanceLine" as never)
      .where("trialBalanceId" as never, "=", input.comparativePeriodTrialBalanceId as never)
      .selectAll()
      .execute()) as Record<string, unknown>[];
  }

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

  // 8. Fetch accounts to get classification info
  const classifications = getClassificationsForType(input.statementType);

  // Build line items from trial balance lines, filtering by account classification
  const now = new Date();
  const lineItems: Record<string, unknown>[] = [];
  let sortOrder = 0;

  for (const tbLine of trialBalanceLines) {
    // Look up the account to check classification
    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>;
    if (!classifications.includes(acct.classification as string)) continue;

    sortOrder++;
    lineItems.push({
      accountId: tbLine.accountId,
      accountGroupId: tbLine.accountGroupId ?? null,
      label: tbLine.accountName as string,
      amount: tbLine.closingBalance as number,
      comparativeAmount: comparativeMap.get(tbLine.accountId as string) ?? null,
      lineType: "DETAIL",
      sectionType: tbLine.accountGroupId
        ? getSectionType(acct.classification as string, input.statementType)
        : "Unclassified",
      sortOrder,
    });
  }

  // Add SUBTOTAL lines for each section
  const sectionTotals = new Map<string, number>();
  for (const item of lineItems) {
    const current = sectionTotals.get(item.sectionType as string) ?? 0;
    sectionTotals.set(item.sectionType as string, current + (item.amount as number));
  }

  for (const [section, total] of sectionTotals) {
    sortOrder++;
    lineItems.push({
      accountId: null,
      accountGroupId: null,
      label: `Total ${section}`,
      amount: total,
      comparativeAmount: null,
      lineType: "SUBTOTAL",
      sectionType: section,
      sortOrder,
    });
  }

  // 9. Insert FinancialStatement (DRAFT)
  const financialStatement = await db
    .insertInto("FinancialStatement")
    .values({
      companyId: input.companyId,
      chartOfAccountsId: input.chartOfAccountsId,
      trialBalanceId: input.trialBalanceId,
      comparativePeriodTrialBalanceId: input.comparativePeriodTrialBalanceId ?? null,
      statementType: input.statementType as never,
      status: "DRAFT",
      reportingDate: input.reportingDate ?? null,
      startDate: input.startDate ?? null,
      endDate: input.endDate ?? null,
      cashFlowMethod: input.cashFlowMethod ?? null,
      generatedAt: now,
      finalizedAt: null,
      createdAt: now,
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

  // 10. Insert FinancialStatementLineItems
  for (const item of lineItems) {
    await db
      .insertInto("FinancialStatementLineItem")
      .values({
        financialStatementId: financialStatement!.id,
        accountId: item.accountId as string,
        accountGroupId: item.accountGroupId as string | null,
        label: item.label as string,
        amount: item.amount as number,
        comparativeAmount: item.comparativeAmount as number | null,
        lineType: item.lineType as never,
        sectionType: item.sectionType as string,
        sortOrder: item.sortOrder as number,
        createdAt: now,
        updatedAt: null,
      })
      .returningAll()
      .executeTakeFirst();
  }

  // Record audit event
  auditTrail.push({
    type: "FINANCIAL_STATEMENT_GENERATED",
    statementType: input.statementType,
    actorId: ctx.actorId,
  });

  // 11. Return financialStatement
  return ok({ financialStatement: financialStatement!, auditTrail });
}
