class Holding::ReverseCalculator
  attr_reader :account, :portfolio_snapshot

  def initialize(account, portfolio_snapshot:, security_ids: nil)
    @account = account
    @portfolio_snapshot = portfolio_snapshot
    @security_ids = security_ids
  end

  def calculate
    Rails.logger.tagged("Holding::ReverseCalculator") do
      precompute_cost_basis
      holdings = calculate_holdings
      Holding.gapfill(holdings)
    end
  end

  private
    # Reverse calculators will use the existing holdings as a source of security ids and prices
    # since it is common for a provider to supply "current day" holdings but not all the historical
    # trades that make up those holdings.
    def portfolio_cache
      @portfolio_cache ||= Holding::PortfolioCache.new(account, use_holdings: true, security_ids: @security_ids)
    end

    def calculate_holdings
      # Start with the portfolio snapshot passed in from the materializer
      current_portfolio = portfolio_snapshot.to_h
      previous_portfolio = {}

      holdings = []

      Date.current.downto(account.start_date).each do |date|
        today_trades = portfolio_cache.get_trades(date: date)
        previous_portfolio = transform_portfolio(current_portfolio, today_trades, direction: :reverse)

        # If current day, always use holding prices (since that's what Plaid gives us).  For historical values, use market data (since Plaid doesn't supply historical prices)
        holdings += build_holdings(current_portfolio, date, price_source: date == Date.current ? "holding" : nil)
        current_portfolio = previous_portfolio
      end

      holdings
    end

    def transform_portfolio(previous_portfolio, trade_entries, direction: :forward)
      new_quantities = previous_portfolio.dup

      trade_entries.each do |trade_entry|
        trade = trade_entry.entryable
        security_id = trade.security_id
        qty_change = trade.qty
        qty_change = qty_change * -1 if direction == :reverse
        new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change
      end

      new_quantities
    end

    def build_holdings(portfolio, date, price_source: nil)
      portfolio.map do |security_id, qty|
        next if @security_ids && !@security_ids.include?(security_id)

        price = portfolio_cache.get_price(security_id, date, source: price_source)

        if price.nil?
          next
        end

        Holding.new(
          account_id: account.id,
          security_id: security_id,
          date: date,
          qty: qty,
          price: price.price,
          currency: price.currency,
          amount: qty * price.price,
          cost_basis: cost_basis_for(security_id, date)
        )
      end.compact
    end

    # Pre-compute cost basis for all securities at all dates using forward pass through trades
    # Stores: { security_id => { date => cost_basis } }
    def precompute_cost_basis
      @cost_basis_by_date = Hash.new { |h, k| h[k] = {} }
      tracker = Hash.new { |h, k| h[k] = { total_cost: BigDecimal("0"), total_qty: BigDecimal("0") } }

      trades = portfolio_cache.get_trades.sort_by(&:date)
      trade_index = 0

      account.start_date.upto(Date.current).each do |date|
        # Process all trades up to and including this date
        while trade_index < trades.size && trades[trade_index].date <= date
          trade_entry = trades[trade_index]
          trade = trade_entry.entryable

          if trade.qty > 0 # Only track buys
            security_id = trade.security_id
            trade_price = Money.new(trade.price, trade.currency)
            begin
              converted_price = trade_price.exchange_to(account.currency).amount
            rescue Money::ConversionError
              converted_price = trade.price
            end

            tracker[security_id][:total_cost] += converted_price * trade.qty
            tracker[security_id][:total_qty] += trade.qty
          end
          trade_index += 1
        end

        # Store current cost basis snapshot for each security at this date
        tracker.each do |security_id, data|
          next if data[:total_qty].zero?
          @cost_basis_by_date[security_id][date] = data[:total_cost] / data[:total_qty]
        end
      end
    end

    def cost_basis_for(security_id, date)
      @cost_basis_by_date.dig(security_id, date)
    end
end
