defmodule Oban.Pro.Limiters.Rate.SlidingWindow do
  @moduledoc false

  # The sliding window uses two time buckets (current and previous) with weighted averaging to
  # provide smooth rate limiting without the sharp edges of fixed windows.

  @behaviour Oban.Pro.Limiters.Rate.Algorithm

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def demand(windows, allowed, period, curr_time, prev_time) do
    weight = div(max(period - (curr_time - prev_time), 0), period)

    total =
      Enum.reduce(windows, 0, fn {_key, val}, acc ->
        acc + val["prev_count"] * weight + val["curr_count"]
      end)

    max(allowed - total, 0)
  end

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def partition_demands(windows, allowed, period, curr_time, prev_time, available_keys) do
    weight = div(max(period - (curr_time - prev_time), 0), period)

    untracked =
      for key <- available_keys,
          not is_map_key(windows, key),
          into: %{},
          do: {key, allowed}

    Enum.reduce(windows, untracked, fn {key, val}, acc ->
      %{"curr_count" => curr, "prev_count" => prev} = val

      case max(allowed - (prev * weight + curr), 0) do
        demand when demand > 0 -> Map.put(acc, key, demand)
        _zero -> acc
      end
    end)
  end

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def flatten(windows) do
    {pacc, cacc} =
      Enum.reduce(windows, {0, 0}, fn {_key, val}, {pacc, cacc} ->
        {pacc + val["prev_count"], cacc + val["curr_count"]}
      end)

    [%{"curr_count" => cacc, "prev_count" => pacc}]
  end

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def merge(windows_a, windows_b) do
    Map.merge(windows_a, windows_b, fn _key, v1, v2 ->
      %{"curr_count" => c1, "prev_count" => p1} = v1
      %{"curr_count" => c2, "prev_count" => p2} = v2
      %{"curr_count" => c1 + c2, "prev_count" => p1 + p2}
    end)
  end

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def track(windows, new_counts, period, prev_time, curr_time, _allowed) do
    unix_diff = curr_time - prev_time

    {mode, next_time} =
      cond do
        unix_diff > period * 2 -> {:dump_prev, curr_time}
        unix_diff > period -> {:swap_prev, curr_time}
        true -> {:bump_curr, prev_time}
      end

    new_windows =
      for {key, count} <- new_counts,
          not is_map_key(windows, key),
          into: %{},
          do: {key, %{"curr_count" => count, "prev_count" => 0}}

    all_windows =
      for {key, window} <- windows,
          not match?(%{"curr_count" => 0, "prev_count" => 0}, window),
          into: new_windows,
          do: {key, update_window(window, Map.get(new_counts, key, 0), mode)}

    {all_windows, next_time}
  end

  defp update_window(window, next_count, mode) do
    %{"curr_count" => curr_count, "prev_count" => prev_count} = window

    {new_curr, new_prev} =
      case mode do
        :dump_prev -> {next_count, 0}
        :swap_prev -> {next_count, curr_count}
        :bump_curr -> {curr_count + next_count, prev_count}
      end

    %{"curr_count" => new_curr, "prev_count" => new_prev}
  end
end
