class ReportsController < ApplicationController
  include Periodable

  # Allow API key authentication for exports (for Google Sheets integration)
  # Note: We run authentication_for_export which handles both session and API key auth
  skip_authentication only: :export_transactions
  before_action :authenticate_for_export, only: :export_transactions

  def index
    setup_report_data(show_flash: true)

    # Build reports sections for collapsible/reorderable UI
    @reports_sections = build_reports_sections

    @breadcrumbs = [ [ "Home", root_path ], [ "Reports", nil ] ]
  end

  def print
    setup_report_data(show_flash: false)

    render layout: "print"
  end

  def update_preferences
    if Current.user.update_reports_preferences(preferences_params)
      head :ok
    else
      head :unprocessable_entity
    end
  end

  def export_transactions
    @period_type = params[:period_type]&.to_sym || :monthly
    @start_date = parse_date_param(:start_date) || default_start_date
    @end_date = parse_date_param(:end_date) || default_end_date

    # Validate and fix date range if end_date is before start_date
    # Don't show flash message since we're returning CSV data
    validate_and_fix_date_range(show_flash: false)

    @period = Period.custom(start_date: @start_date, end_date: @end_date)

    # Build monthly breakdown data for export
    @export_data = build_monthly_breakdown_for_export

    respond_to do |format|
      format.csv do
        csv_data = generate_transactions_csv
        send_data csv_data,
                  filename: "transactions_breakdown_#{@start_date.strftime('%Y%m%d')}_to_#{@end_date.strftime('%Y%m%d')}.csv",
                  type: "text/csv"
      end

      # Excel and PDF exports require additional gems (caxlsx and prawn)
      # Uncomment and install gems if needed:
      #
      # format.xlsx do
      #   xlsx_data = generate_transactions_xlsx
      #   send_data xlsx_data,
      #             filename: "transactions_breakdown_#{@start_date.strftime('%Y%m%d')}_to_#{@end_date.strftime('%Y%m%d')}.xlsx",
      #             type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
      # end
      #
      # format.pdf do
      #   pdf_data = generate_transactions_pdf
      #   send_data pdf_data,
      #             filename: "transactions_breakdown_#{@start_date.strftime('%Y%m%d')}_to_#{@end_date.strftime('%Y%m%d')}.pdf",
      #             type: "application/pdf"
      # end
    end
  end

  def google_sheets_instructions
    # Re-build the params needed for the export URL
    base_params = {
      period_type: params[:period_type],
      start_date: params[:start_date],
      end_date: params[:end_date],
      sort_by: params[:sort_by],
      sort_direction: params[:sort_direction]
    }.compact

    # Build the full URL with the API key, if present
    @csv_url = export_transactions_reports_url(base_params.merge(format: :csv))
    @api_key_present = @csv_url.include?("api_key=")

    # This action will render `app/views/reports/google_sheets_instructions.html.erb`
    # It will render *inside* the modal frame.
  end

  private
    def setup_report_data(show_flash: false)
      @period_type = params[:period_type]&.to_sym || :monthly
      @start_date = parse_date_param(:start_date) || default_start_date
      @end_date = parse_date_param(:end_date) || default_end_date

      # Validate and fix date range if end_date is before start_date
      validate_and_fix_date_range(show_flash: show_flash)

      # Build the period
      @period = Period.custom(start_date: @start_date, end_date: @end_date)
      @previous_period = build_previous_period

      # Get aggregated data
      @current_income_totals = Current.family.income_statement.income_totals(period: @period)
      @current_expense_totals = Current.family.income_statement.expense_totals(period: @period)

      @previous_income_totals = Current.family.income_statement.income_totals(period: @previous_period)
      @previous_expense_totals = Current.family.income_statement.expense_totals(period: @previous_period)

      # Calculate summary metrics
      @summary_metrics = build_summary_metrics

      # Build trend data (last 6 months)
      @trends_data = build_trends_data

      # Net worth metrics
      @net_worth_metrics = build_net_worth_metrics

      # Transactions breakdown
      @transactions = build_transactions_breakdown

      # Investment metrics
      @investment_metrics = build_investment_metrics

      # Investment flows (contributions/withdrawals)
      @investment_flows = InvestmentFlowStatement.new(Current.family, user: Current.user).period_totals(period: @period)

      # Flags for view rendering
      @has_accounts = accessible_accounts.any?
    end

    def preferences_params
      prefs = params.require(:preferences)
      {}.tap do |permitted|
        permitted["reports_collapsed_sections"] = prefs[:reports_collapsed_sections].to_unsafe_h if prefs[:reports_collapsed_sections]
        permitted["reports_section_order"] = prefs[:reports_section_order] if prefs[:reports_section_order]
      end
    end

    def build_reports_sections
      all_sections = [
        {
          key: "net_worth",
          title: "reports.net_worth.title",
          partial: "reports/net_worth",
          locals: { net_worth_metrics: @net_worth_metrics },
          visible: accessible_accounts.any?,
          collapsible: true
        },
        {
          key: "trends_insights",
          title: "reports.trends.title",
          partial: "reports/trends_insights",
          locals: { trends_data: @trends_data },
          visible: @has_accounts,
          collapsible: true
        },
        {
          key: "investment_performance",
          title: "reports.investment_performance.title",
          partial: "reports/investment_performance",
          locals: { investment_metrics: @investment_metrics },
          visible: @investment_metrics[:has_investments],
          collapsible: true
        },
        {
          key: "investment_flows",
          title: "reports.investment_flows.title",
          partial: "reports/investment_flows",
          locals: { investment_flows: @investment_flows },
          visible: @investment_metrics[:has_investments] && (@investment_flows.contributions.amount > 0 || @investment_flows.withdrawals.amount > 0),
          collapsible: true
        },
        {
          key: "transactions_breakdown",
          title: "reports.transactions_breakdown.title",
          partial: "reports/transactions_breakdown",
          locals: {
            transactions: @transactions,
            period_type: @period_type,
            start_date: @start_date,
            end_date: @end_date
          },
          visible: @has_accounts,
          collapsible: true
        }
      ]

      # Order sections according to user preference
      section_order = Current.user.reports_section_order
      ordered_sections = section_order.map do |key|
        all_sections.find { |s| s[:key] == key }
      end.compact

      # Add any new sections that aren't in the saved order (future-proofing)
      all_sections.each do |section|
        ordered_sections << section unless ordered_sections.include?(section)
      end

      ordered_sections
    end

    def validate_and_fix_date_range(show_flash: false)
      return unless @start_date > @end_date

      # Swap the dates to maintain user's intended date range
      @start_date, @end_date = @end_date, @start_date
      flash.now[:alert] = t("reports.invalid_date_range") if show_flash
    end

    def ensure_money(value)
      return value if value.is_a?(Money)
      # Value is numeric (BigDecimal or Integer) in dollars - pass directly to Money.new
      Money.new(value, Current.family.currency)
    end

    def parse_date_param(param_name)
      date_string = params[param_name]
      return nil if date_string.blank?

      Date.parse(date_string)
    rescue Date::Error
      nil
    end

    def default_start_date
      case @period_type
      when :monthly
        Date.current.beginning_of_month.to_date
      when :quarterly
        Date.current.beginning_of_quarter.to_date
      when :ytd
        Date.current.beginning_of_year.to_date
      when :last_6_months
        6.months.ago.beginning_of_month.to_date
      when :custom
        1.month.ago.to_date
      else
        Date.current.beginning_of_month.to_date
      end
    end

    def default_end_date
      case @period_type
      when :monthly, :last_6_months
        Date.current.end_of_month.to_date
      when :quarterly
        Date.current.end_of_quarter.to_date
      when :ytd
        Date.current
      when :custom
        Date.current
      else
        Date.current.end_of_month.to_date
      end
    end

    def build_previous_period
      duration = (@end_date - @start_date).to_i
      previous_end = @start_date - 1.day
      previous_start = previous_end - duration.days

      Period.custom(start_date: previous_start, end_date: previous_end)
    end

    def build_summary_metrics
      # Ensure we always have Money objects
      current_income = ensure_money(@current_income_totals.total)
      current_expenses = ensure_money(@current_expense_totals.total)
      net_savings = current_income - current_expenses

      previous_income = ensure_money(@previous_income_totals.total)
      previous_expenses = ensure_money(@previous_expense_totals.total)

      # Calculate percentage changes
      income_change = calculate_percentage_change(previous_income, current_income)
      expense_change = calculate_percentage_change(previous_expenses, current_expenses)

      # Get budget performance for current period
      budget_percent = calculate_budget_performance

      {
        current_income: current_income,
        income_change: income_change,
        current_expenses: current_expenses,
        expense_change: expense_change,
        net_savings: net_savings,
        budget_percent: budget_percent
      }
    end

    def calculate_percentage_change(previous_value, current_value)
      return 0 if previous_value.zero?

      ((current_value - previous_value) / previous_value * 100).round(1)
    end

    def calculate_budget_performance
      # Only calculate if we're looking at current month
      return nil unless @period_type == :monthly && @start_date.beginning_of_month.to_date == Date.current.beginning_of_month.to_date

      budget = Budget.find_or_bootstrap(Current.family, start_date: @start_date.beginning_of_month.to_date, user: Current.user)
      return 0 if budget.nil? || budget.allocated_spending.zero?

      (budget.actual_spending / budget.allocated_spending * 100).round(1)
    rescue StandardError
      nil
    end

    def build_trends_data
      # Generate month-by-month data based on the current period filter
      trends = []

      # Generate list of months within the period
      current_month = @start_date.beginning_of_month
      end_of_period = @end_date.end_of_month

      while current_month <= end_of_period
        month_start = current_month
        month_end = current_month.end_of_month

        # Ensure we don't go beyond the end date
        month_end = @end_date if month_end > @end_date

        period = Period.custom(start_date: month_start, end_date: month_end)

        income = Current.family.income_statement.income_totals(period: period).total
        expenses = Current.family.income_statement.expense_totals(period: period).total

        trends << {
          month: month_start.strftime("%b %Y"),
          is_current_month: (month_start.month == Date.current.month && month_start.year == Date.current.year),
          income: income,
          expenses: expenses,
          net: income - expenses
        }

        current_month = current_month.next_month
      end

      trends
    end

    def build_transactions_breakdown
      # Base query: all transactions in the period
      # Exclude transfers, one-time, and CC payments (matching income_statement logic)
      transactions = Transaction
        .joins(:entry)
        .joins(entry: :account)
        .where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
        .where(entries: { entryable_type: "Transaction", excluded: false, date: @period.date_range })
        .where.not(kind: Transaction::BUDGET_EXCLUDED_KINDS)
        .includes(entry: :account, category: :parent)

      # Apply filters (includes finance account scoping)
      transactions = apply_transaction_filters(transactions)

      # Get trades in the period (matching income_statement logic)
      trades = Trade
        .joins(:entry)
        .joins(entry: :account)
        .where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
        .where(entries: { entryable_type: "Trade", excluded: false, date: @period.date_range })
        .includes(entry: :account, category: :parent)

      trades = apply_entry_filters(trades)

      # Get sort parameters
      sort_by = params[:sort_by] || "amount"
      sort_direction = params[:sort_direction] || "desc"

      # Group by category (tracking parent relationship) and type
      # Structure: { [parent_category_id, type] => { parent_data, subcategories: { subcategory_id => data } } }
      grouped_data = {}
      family_currency = Current.family.currency

      # Helper to initialize a category group hash
      init_category_group = ->(id, name, color, icon, type) do
        { category_id: id, category_name: name, category_color: color, category_icon: icon, type: type, total: 0, count: 0, subcategories: {} }
      end

      # Helper to initialize a subcategory hash
      init_subcategory = ->(category) do
        { category_id: category.id, category_name: category.name, category_color: category.color, category_icon: category.lucide_icon, total: 0, count: 0 }
      end

      # Helper to process an entry (transaction or trade)
      process_entry = ->(category, entry, is_trade) do
        type = entry.amount > 0 ? "expense" : "income"
        begin
          converted_amount = Money.new(entry.amount.abs, entry.currency).exchange_to(family_currency).amount
        rescue Money::ConversionError
          converted_amount = entry.amount.abs
        end

        if category.nil?
          # Uncategorized or Other Investments (for trades)
          if is_trade
            parent_key = [ :other_investments, type ]
            grouped_data[parent_key] ||= init_category_group.call(:other_investments, Category.other_investments.name, Category.other_investments.color, Category.other_investments.lucide_icon, type)
          else
            parent_key = [ :uncategorized, type ]
            grouped_data[parent_key] ||= init_category_group.call(:uncategorized, Category.uncategorized.name, Category.uncategorized.color, Category.uncategorized.lucide_icon, type)
          end
        elsif category.parent_id.present?
          # This is a subcategory - group under parent
          parent = category.parent
          parent_key = [ parent.id, type ]
          grouped_data[parent_key] ||= init_category_group.call(parent.id, parent.name, parent.color || Category::UNCATEGORIZED_COLOR, parent.lucide_icon, type)

          # Add to subcategory
          grouped_data[parent_key][:subcategories][category.id] ||= init_subcategory.call(category)
          grouped_data[parent_key][:subcategories][category.id][:count] += 1
          grouped_data[parent_key][:subcategories][category.id][:total] += converted_amount
        else
          # This is a root category (no parent)
          parent_key = [ category.id, type ]
          grouped_data[parent_key] ||= init_category_group.call(category.id, category.name, category.color || Category::UNCATEGORIZED_COLOR, category.lucide_icon, type)
        end

        grouped_data[parent_key][:count] += 1
        grouped_data[parent_key][:total] += converted_amount
      end

      # Process transactions
      transactions.each do |transaction|
        process_entry.call(transaction.category, transaction.entry, false)
      end

      # Process trades
      trades.each do |trade|
        process_entry.call(trade.category, trade.entry, true)
      end

      # Convert to array and sort subcategories
      result = grouped_data.values.map do |parent_data|
        subcategories = parent_data[:subcategories].values.sort_by { |s| sort_direction == "asc" ? s[:total] : -s[:total] }
        parent_data.merge(subcategories: subcategories)
      end

      # Sort by amount (total) with the specified direction
      if sort_direction == "asc"
        result.sort_by { |g| g[:total] }
      else
        result.sort_by { |g| -g[:total] }
      end
    end

    def build_investment_metrics
      investment_statement = Current.family.investment_statement
      investment_accounts = investment_statement.investment_accounts

      return { has_investments: false } unless investment_accounts.any?

      period_totals = investment_statement.totals(period: @period)

      {
        has_investments: true,
        portfolio_value: investment_statement.portfolio_value_money,
        unrealized_trend: investment_statement.unrealized_gains_trend,
        period_contributions: period_totals.contributions,
        period_withdrawals: period_totals.withdrawals,
        top_holdings: investment_statement.top_holdings(limit: 5),
        accounts: investment_accounts.to_a,
        gains_by_tax_treatment: build_gains_by_tax_treatment(investment_statement)
      }
    end

    def build_gains_by_tax_treatment(investment_statement)
      currency = Current.family.currency
      # Eager-load account and accountable to avoid N+1 when accessing tax_treatment
      current_holdings = investment_statement.current_holdings
        .includes(account: :accountable)
        .to_a

      # Group holdings by tax treatment (from account)
      holdings_by_treatment = current_holdings.group_by { |h| h.account.tax_treatment || :taxable }

      # Get sell trades in period with realized gains
      # Eager-load security, account, and accountable to avoid N+1
      sell_trades = Current.family.trades
        .joins(:entry)
        .where(entries: { date: @period.date_range })
        .where("trades.qty < 0")
        .includes(:security, entry: { account: :accountable })
        .to_a

      # Preload holdings for all accounts that have sell trades to avoid N+1 in realized_gain_loss
      account_ids = sell_trades.map { |t| t.entry.account_id }.uniq
      holdings_by_account = Holding
        .where(account_id: account_ids)
        .where("date <= ?", @period.date_range.end)
        .order(date: :desc)
        .group_by(&:account_id)

      # Inject preloaded holdings into trades for realized_gain_loss calculation
      sell_trades.each do |trade|
        trade.instance_variable_set(:@preloaded_holdings, holdings_by_account[trade.entry.account_id] || [])
      end

      trades_by_treatment = sell_trades.group_by { |t| t.entry.account.tax_treatment || :taxable }

      # Unwrap helper: Trend#value / realized_gain_loss#value are Money objects,
      # and this codebase's Money keeps the source currency through `*` and
      # through `Money.new(money, _)`. Unwrapping to BigDecimal first keeps sums
      # and the final Money.new(..., currency) correctly labeled in family currency.
      to_numeric = ->(value) { value.is_a?(Money) ? value.amount : value }

      # Unrealized gains mark holdings to market, so convert at today's FX.
      foreign_holding_currencies = current_holdings.map(&:currency).compact.uniq.reject { |c| c == currency }
      holding_rates = ExchangeRate.rates_for(foreign_holding_currencies, to: currency, date: Date.current)
      convert_current = ->(amount, from) {
        numeric = to_numeric.call(amount)
        from == currency ? numeric : numeric * (holding_rates[from] || 1)
      }

      # Realized gains are locked at trade time, so convert each at its own
      # entry-date FX. Mirrors InvestmentStatement::Totals, which also uses
      # entry-date rates for contributions/withdrawals on this same card.
      foreign_trade_currencies = sell_trades.map(&:currency).compact.uniq.reject { |c| c == currency }
      rates_by_trade_date = sell_trades.map { |t| t.entry.date }.uniq.each_with_object({}) do |date, memo|
        memo[date] = ExchangeRate.rates_for(foreign_trade_currencies, to: currency, date: date)
      end
      convert_trade = ->(amount, from, date) {
        numeric = to_numeric.call(amount)
        from == currency ? numeric : numeric * (rates_by_trade_date.dig(date, from) || 1)
      }

      # Build metrics per treatment
      %i[taxable tax_deferred tax_exempt tax_advantaged].each_with_object({}) do |treatment, hash|
        holdings = holdings_by_treatment[treatment] || []
        trades = trades_by_treatment[treatment] || []

        # Sum unrealized gains from holdings (only those with known cost basis)
        unrealized = holdings.sum do |h|
          trend = h.trend
          trend ? convert_current.call(trend.value, h.currency) : 0
        end

        # Sum realized gains from sell trades
        realized = trades.sum do |t|
          gain = t.realized_gain_loss
          gain ? convert_trade.call(gain.value, t.currency, t.entry.date) : 0
        end

        # Only include treatment groups that have some activity
        next if holdings.empty? && trades.empty?

        hash[treatment] = {
          holdings: holdings,
          sell_trades: trades,
          unrealized_gain: Money.new(unrealized, currency),
          realized_gain: Money.new(realized, currency),
          total_gain: Money.new(unrealized + realized, currency)
        }
      end
    end

    def build_net_worth_metrics
      balance_sheet = Current.family.balance_sheet
      currency = Current.family.currency

      # Current net worth
      current_net_worth = balance_sheet.net_worth
      total_assets = balance_sheet.assets.total
      total_liabilities = balance_sheet.liabilities.total

      # Get net worth series for the period to calculate change
      # The series.trend gives us the change from first to last value in the period
      net_worth_series = balance_sheet.net_worth_series(period: @period)
      trend = net_worth_series&.trend

      # Get asset and liability groups for breakdown
      asset_groups = balance_sheet.assets.account_groups.map do |group|
        { name: group.name, total: Money.new(group.total, currency) }
      end.reject { |g| g[:total].zero? }

      liability_groups = balance_sheet.liabilities.account_groups.map do |group|
        { name: group.name, total: Money.new(group.total, currency) }
      end.reject { |g| g[:total].zero? }

      {
        current_net_worth: Money.new(current_net_worth, currency),
        total_assets: Money.new(total_assets, currency),
        total_liabilities: Money.new(total_liabilities, currency),
        trend: trend,
        asset_groups: asset_groups,
        liability_groups: liability_groups
      }
    end

    def apply_transaction_filters(scope)
      scope = apply_entry_filters(scope)

      # Filter by tag (Transaction-specific — trades don't have taggings)
      if params[:filter_tag_id].present?
        scope = scope.joins(:taggings).where(taggings: { tag_id: params[:filter_tag_id] })
      end

      scope
    end

    # Filters applicable to both transactions and trades (entry-level + category)
    def apply_entry_filters(scope)
      # Scope to user's finance accounts
      finance_account_ids = Current.user&.finance_accounts&.pluck(:id) || []
      scope = scope.where(entries: { account_id: finance_account_ids })

      # Filter by category (including subcategories)
      if params[:filter_category_id].present?
        category_id = params[:filter_category_id]
        # Scope to family's categories to prevent cross-family data access
        subcategory_ids = Current.family.categories.where(parent_id: category_id).pluck(:id)
        all_category_ids = [ category_id ] + subcategory_ids
        scope = scope.where(category_id: all_category_ids)
      end

      # Filter by account
      if params[:filter_account_id].present?
        scope = scope.where(entries: { account_id: params[:filter_account_id] })
      end

      # Filter by amount range
      if params[:filter_amount_min].present?
        scope = scope.where("ABS(entries.amount) >= ?", params[:filter_amount_min].to_f)
      end

      if params[:filter_amount_max].present?
        scope = scope.where("ABS(entries.amount) <= ?", params[:filter_amount_max].to_f)
      end

      # Filter by date range (within the period)
      if params[:filter_date_start].present?
        filter_start = Date.parse(params[:filter_date_start])
        scope = scope.where("entries.date >= ?", filter_start) if filter_start >= @start_date
      end

      if params[:filter_date_end].present?
        filter_end = Date.parse(params[:filter_date_end])
        scope = scope.where("entries.date <= ?", filter_end) if filter_end <= @end_date
      end

      scope
    rescue Date::Error
      scope
    end

    def build_transactions_breakdown_for_export
      # Get flat transactions list (not grouped) for export
      # Exclude transfers, one-time, and CC payments (matching income_statement logic)
      transactions = Transaction
        .joins(:entry)
        .joins(entry: :account)
        .where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
        .where(entries: { entryable_type: "Transaction", excluded: false, date: @period.date_range })
        .where.not(kind: Transaction::BUDGET_EXCLUDED_KINDS)
        .includes(entry: :account, category: [])

      transactions = apply_transaction_filters(transactions)

      sort_by = params[:sort_by] || "date"
      # Whitelist sort_direction to prevent SQL injection
      sort_direction = %w[asc desc].include?(params[:sort_direction]&.downcase) ? params[:sort_direction].upcase : "DESC"

      case sort_by
      when "date"
        transactions.order("entries.date #{sort_direction}")
      when "amount"
        transactions.order("entries.amount #{sort_direction}")
      else
        transactions.order("entries.date DESC")
      end
    end

    def build_monthly_breakdown_for_export
      # Generate list of months in the period
      months = []
      current_month = @start_date.beginning_of_month
      end_of_period = @end_date.end_of_month

      while current_month <= end_of_period
        months << current_month
        current_month = current_month.next_month
      end

      # Get all transactions in the period
      # Exclude transfers, one-time, and CC payments (matching income_statement logic)
      transactions = Transaction
        .joins(:entry)
        .joins(entry: :account)
        .where(accounts: { family_id: Current.family.id, status: [ "draft", "active" ] })
        .where(entries: { entryable_type: "Transaction", excluded: false, date: @period.date_range })
        .where.not(kind: Transaction::BUDGET_EXCLUDED_KINDS)
        .includes(entry: :account, category: [])

      transactions = apply_transaction_filters(transactions)

      # Group by category, type, and month
      breakdown = {}
      family_currency = Current.family.currency

      # Process transactions
      transactions.each do |transaction|
        entry = transaction.entry
        is_expense = entry.amount > 0
        type = is_expense ? "expense" : "income"
        category_name = transaction.category&.name || "Uncategorized"
        month_key = entry.date.beginning_of_month

        # Convert to family currency
        begin
          converted_amount = Money.new(entry.amount.abs, entry.currency).exchange_to(family_currency).amount
        rescue Money::ConversionError
          converted_amount = entry.amount.abs
        end

        key = [ category_name, type ]
        breakdown[key] ||= { category: category_name, type: type, months: {}, total: 0 }
        breakdown[key][:months][month_key] ||= 0
        breakdown[key][:months][month_key] += converted_amount
        breakdown[key][:total] += converted_amount
      end

      # Convert to array and sort by type and total (descending)
      result = breakdown.map do |key, data|
        {
          category: data[:category],
          type: data[:type],
          months: data[:months],
          total: data[:total]
        }
      end

      # Separate and sort income and expenses
      income_data = result.select { |r| r[:type] == "income" }.sort_by { |r| -r[:total] }
      expense_data = result.select { |r| r[:type] == "expense" }.sort_by { |r| -r[:total] }

      {
        months: months,
        income: income_data,
        expenses: expense_data
      }
    end

    def generate_transactions_csv
      require "csv"

      CSV.generate do |csv|
        # Build header row: Category + Month columns + Total
        month_headers = @export_data[:months].map { |m| m.strftime("%b %Y") }
        header_row = [ "Category" ] + month_headers + [ "Total" ]
        csv << header_row

        # Income section
        if @export_data[:income].any?
          csv << [ "INCOME" ] + Array.new(month_headers.length + 1, "")

          @export_data[:income].each do |category_data|
            row = [ category_data[:category] ]

            # Add amounts for each month
            @export_data[:months].each do |month|
              amount = category_data[:months][month] || 0
              row << Money.new(amount, Current.family.currency).format
            end

            # Add row total
            row << Money.new(category_data[:total], Current.family.currency).format
            csv << row
          end

          # Income totals row
          totals_row = [ "TOTAL INCOME" ]
          @export_data[:months].each do |month|
            month_total = @export_data[:income].sum { |c| c[:months][month] || 0 }
            totals_row << Money.new(month_total, Current.family.currency).format
          end
          grand_income_total = @export_data[:income].sum { |c| c[:total] }
          totals_row << Money.new(grand_income_total, Current.family.currency).format
          csv << totals_row

          # Blank row
          csv << []
        end

        # Expenses section
        if @export_data[:expenses].any?
          csv << [ "EXPENSES" ] + Array.new(month_headers.length + 1, "")

          @export_data[:expenses].each do |category_data|
            row = [ category_data[:category] ]

            # Add amounts for each month
            @export_data[:months].each do |month|
              amount = category_data[:months][month] || 0
              row << Money.new(amount, Current.family.currency).format
            end

            # Add row total
            row << Money.new(category_data[:total], Current.family.currency).format
            csv << row
          end

          # Expenses totals row
          totals_row = [ "TOTAL EXPENSES" ]
          @export_data[:months].each do |month|
            month_total = @export_data[:expenses].sum { |c| c[:months][month] || 0 }
            totals_row << Money.new(month_total, Current.family.currency).format
          end
          grand_expenses_total = @export_data[:expenses].sum { |c| c[:total] }
          totals_row << Money.new(grand_expenses_total, Current.family.currency).format
          csv << totals_row
        end
      end
    end

    def generate_transactions_xlsx
      require "caxlsx"

      package = Axlsx::Package.new
      workbook = package.workbook
      bold_style = workbook.styles.add_style(b: true)

      workbook.add_worksheet(name: "Breakdown") do |sheet|
        # Build header row: Category + Month columns + Total
        month_headers = @export_data[:months].map { |m| m.strftime("%b %Y") }
        header_row = [ "Category" ] + month_headers + [ "Total" ]
        sheet.add_row header_row, style: bold_style

        # Income section
        if @export_data[:income].any?
          sheet.add_row [ "INCOME" ] + Array.new(month_headers.length + 1, ""), style: bold_style

          @export_data[:income].each do |category_data|
            row = [ category_data[:category] ]

            # Add amounts for each month
            @export_data[:months].each do |month|
              amount = category_data[:months][month] || 0
              row << Money.new(amount, Current.family.currency).format
            end

            # Add row total
            row << Money.new(category_data[:total], Current.family.currency).format
            sheet.add_row row
          end

          # Income totals row
          totals_row = [ "TOTAL INCOME" ]
          @export_data[:months].each do |month|
            month_total = @export_data[:income].sum { |c| c[:months][month] || 0 }
            totals_row << Money.new(month_total, Current.family.currency).format
          end
          grand_income_total = @export_data[:income].sum { |c| c[:total] }
          totals_row << Money.new(grand_income_total, Current.family.currency).format
          sheet.add_row totals_row, style: bold_style

          # Blank row
          sheet.add_row []
        end

        # Expenses section
        if @export_data[:expenses].any?
          sheet.add_row [ "EXPENSES" ] + Array.new(month_headers.length + 1, ""), style: bold_style

          @export_data[:expenses].each do |category_data|
            row = [ category_data[:category] ]

            # Add amounts for each month
            @export_data[:months].each do |month|
              amount = category_data[:months][month] || 0
              row << Money.new(amount, Current.family.currency).format
            end

            # Add row total
            row << Money.new(category_data[:total], Current.family.currency).format
            sheet.add_row row
          end

          # Expenses totals row
          totals_row = [ "TOTAL EXPENSES" ]
          @export_data[:months].each do |month|
            month_total = @export_data[:expenses].sum { |c| c[:months][month] || 0 }
            totals_row << Money.new(month_total, Current.family.currency).format
          end
          grand_expenses_total = @export_data[:expenses].sum { |c| c[:total] }
          totals_row << Money.new(grand_expenses_total, Current.family.currency).format
          sheet.add_row totals_row, style: bold_style
        end
      end

      package.to_stream.read
    end

    def generate_transactions_pdf
      require "prawn"

      Prawn::Document.new(page_layout: :landscape) do |pdf|
        pdf.text "Transaction Breakdown Report", size: 20, style: :bold
        pdf.text "Period: #{@start_date.strftime('%b %-d, %Y')} to #{@end_date.strftime('%b %-d, %Y')}", size: 12
        pdf.move_down 20

        if @export_data[:income].any? || @export_data[:expenses].any?
          # Build header row
          month_headers = @export_data[:months].map { |m| m.strftime("%b %Y") }
          header_row = [ "Category" ] + month_headers + [ "Total" ]

          # Income section
          if @export_data[:income].any?
            pdf.text "INCOME", size: 14, style: :bold
            pdf.move_down 10

            income_table_data = [ header_row ]

            @export_data[:income].each do |category_data|
              row = [ category_data[:category] ]

              @export_data[:months].each do |month|
                amount = category_data[:months][month] || 0
                row << Money.new(amount, Current.family.currency).format
              end

              row << Money.new(category_data[:total], Current.family.currency).format
              income_table_data << row
            end

            # Income totals row
            totals_row = [ "TOTAL INCOME" ]
            @export_data[:months].each do |month|
              month_total = @export_data[:income].sum { |c| c[:months][month] || 0 }
              totals_row << Money.new(month_total, Current.family.currency).format
            end
            grand_income_total = @export_data[:income].sum { |c| c[:total] }
            totals_row << Money.new(grand_income_total, Current.family.currency).format
            income_table_data << totals_row

            pdf.table(income_table_data, header: true, width: pdf.bounds.width, cell_style: { size: 8 }) do
              row(0).font_style = :bold
              row(0).background_color = "CCFFCC"
              row(-1).font_style = :bold
              row(-1).background_color = "99FF99"
              columns(0).align = :left
              columns(1..-1).align = :right
              self.row_colors = [ "FFFFFF", "F9F9F9" ]
            end

            pdf.move_down 20
          end

          # Expenses section
          if @export_data[:expenses].any?
            pdf.text "EXPENSES", size: 14, style: :bold
            pdf.move_down 10

            expenses_table_data = [ header_row ]

            @export_data[:expenses].each do |category_data|
              row = [ category_data[:category] ]

              @export_data[:months].each do |month|
                amount = category_data[:months][month] || 0
                row << Money.new(amount, Current.family.currency).format
              end

              row << Money.new(category_data[:total], Current.family.currency).format
              expenses_table_data << row
            end

            # Expenses totals row
            totals_row = [ "TOTAL EXPENSES" ]
            @export_data[:months].each do |month|
              month_total = @export_data[:expenses].sum { |c| c[:months][month] || 0 }
              totals_row << Money.new(month_total, Current.family.currency).format
            end
            grand_expenses_total = @export_data[:expenses].sum { |c| c[:total] }
            totals_row << Money.new(grand_expenses_total, Current.family.currency).format
            expenses_table_data << totals_row

            pdf.table(expenses_table_data, header: true, width: pdf.bounds.width, cell_style: { size: 8 }) do
              row(0).font_style = :bold
              row(0).background_color = "FFCCCC"
              row(-1).font_style = :bold
              row(-1).background_color = "FF9999"
              columns(0).align = :left
              columns(1..-1).align = :right
              self.row_colors = [ "FFFFFF", "F9F9F9" ]
            end
          end
        else
          pdf.text "No transactions found for this period.", size: 12
        end
      end.render
    end

    # Export Authentication - handles both session and API key auth
    def authenticate_for_export
      if api_key_present?
        # Use API key authentication
        authenticate_with_api_key
      else
        # Use normal session authentication
        authenticate_user!
      end
    end

    # API Key Authentication Methods
    def api_key_present?
      params[:api_key].present? || request.headers["X-Api-Key"].present?
    end

    def authenticate_with_api_key
      api_key_value = params[:api_key] || request.headers["X-Api-Key"]

      unless api_key_value
        render plain: "API key is required", status: :unauthorized
        return false
      end

      @api_key = ApiKey.find_by_value(api_key_value)

      unless @api_key && @api_key.active?
        render plain: "Invalid or expired API key", status: :unauthorized
        return false
      end

      # Check if API key has read permissions
      unless @api_key.scopes&.include?("read") || @api_key.scopes&.include?("read_write")
        render plain: "API key does not have read permission", status: :forbidden
        return false
      end

      # Set up the current user and session context
      @current_user = @api_key.user
      @api_key.update_last_used!

      # Set up Current context for API requests (similar to Api::V1::BaseController)
      # Return false if setup fails to halt the filter chain
      return false unless setup_current_context_for_api_key

      true
    end

    def setup_current_context_for_api_key
      unless @current_user
        render plain: "User not found for API key", status: :internal_server_error
        return false
      end

      # Find or create a session for this API request
      # We need to find or create a persisted session so that Current.user delegation works properly
      session = @current_user.sessions.first_or_create!(
        user_agent: request.user_agent,
        ip_address: request.ip
      )

      Current.session = session

      # Verify the delegation chain works
      unless Current.user
        render plain: "Failed to establish user context", status: :internal_server_error
        return false
      end

      # Ensure we have a valid family context
      unless Current.family
        render plain: "User does not have an associated family", status: :internal_server_error
        return false
      end

      true
    end
end
