# frozen_string_literal: true

# Recency-weighted averaging of rebased exchange rates. Rates within the grace period carry full weight.
# Beyond it, weight decays exponentially so stale rates contribute less without a hard cutoff. Rates marked
# `excluded: true` (e.g. consensus outliers) are surfaced in the providers list but do not contribute to the rate.
class WeightedAverage
  DECAY_GRACE_DAYS = 3
  DECAY_RATE = 0.5

  def initialize(rates)
    @rates = rates
  end

  def calculate
    @rates.each { |r| r[:date] = Date.parse(r[:date]) unless r[:date].is_a?(Date) }

    reference_date = @rates.map { |r| r[:date] }.max
    return [] unless reference_date

    @rates.group_by { |r| r[:quote] }.sort.filter_map do |_, group|
      contributors = group.reject { |r| r[:excluded] }
      next if contributors.empty?

      weighted = contributors.map { |r| [r, recency_weight(reference_date - r[:date])] }
      total_weight = weighted.sum(&:last)
      rate = weighted.sum { |r, w| r[:rate] * w } / total_weight

      providers = group
        .group_by { |r| r[:provider] }
        .map do |key, rows|
          latest = rows.max_by { |r| r[:date] }
          entry = { key: key, rate: latest[:rate] }
          entry[:excluded] = true if latest[:excluded]
          entry
        end
        .sort_by { |p| p[:key] }

      contributors.max_by { |r| r[:date] }.merge(rate:, providers:)
    end
  end

  private

  def recency_weight(days_old)
    Math.exp(-DECAY_RATE * [days_old - DECAY_GRACE_DAYS, 0].max)
  end
end
