class Account::ReconciliationManager
  attr_reader :account

  def initialize(account)
    @account = account
  end

  # Reconciles balance by creating a Valuation entry. If existing valuation is provided, it will be updated instead of creating a new one.
  def reconcile_balance(balance:, date: Date.current, dry_run: false, existing_valuation_entry: nil)
    old_balance_components = old_balance_components(reconciliation_date: date, existing_valuation_entry: existing_valuation_entry)
    prepared_valuation = prepare_reconciliation(balance, date, existing_valuation_entry)

    unless dry_run
      prepared_valuation.save!
    end

    ReconciliationResult.new(
      success?: true,
      old_cash_balance: old_balance_components[:cash_balance],
      old_balance: old_balance_components[:balance],
      new_cash_balance: derived_cash_balance(date: date, total_balance: prepared_valuation.amount),
      new_balance: prepared_valuation.amount,
      error_message: nil
    )
  rescue => e
    ReconciliationResult.new(
      success?: false,
      error_message: e.message
    )
  end

  private
    # Returns before -> after OR error message
    ReconciliationResult = Struct.new(
      :success?,
      :old_cash_balance,
      :old_balance,
      :new_cash_balance,
      :new_balance,
      :error_message,
      keyword_init: true
    )

    def prepare_reconciliation(balance, date, existing_valuation)
      valuation_record = existing_valuation ||
                         account.entries.valuations.find_by(date: date) || # In case of conflict, where existing valuation is not passed as arg, but one exists
                         account.entries.build(
                                  name: Valuation.build_reconciliation_name(account.accountable_type),
                                  entryable: Valuation.new(kind: "reconciliation")
                                )

      valuation_record.assign_attributes(
        date: date,
        amount: balance,
        currency: account.currency
      )

      valuation_record
    end

    def derived_cash_balance(date:, total_balance:)
      balance_components_for_reconciliation_date = get_balance_components_for_date(date)

      return nil unless balance_components_for_reconciliation_date[:balance] && balance_components_for_reconciliation_date[:cash_balance]

      # We calculate the existing non-cash balance, which for investments would represents "holdings" for the date of reconciliation
      # Since the user is setting "total balance", we have to subtract the existing non-cash balance from the total balance to get the new cash balance
      existing_non_cash_balance = balance_components_for_reconciliation_date[:balance] - balance_components_for_reconciliation_date[:cash_balance]

      total_balance - existing_non_cash_balance
    end

    def old_balance_components(reconciliation_date:, existing_valuation_entry: nil)
      if existing_valuation_entry
        get_balance_components_for_date(existing_valuation_entry.date)
      else
        get_balance_components_for_date(reconciliation_date)
      end
    end

    def get_balance_components_for_date(date)
      balance_record = account.balances.find_by(date: date, currency: account.currency)

      {
        cash_balance: balance_record&.end_cash_balance,
        balance: balance_record&.end_balance
      }
    end
end
