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

  # Fixed window simply resets the count when the period expires, providing a straightforward but
  # potentially bursty rate limit.

  @behaviour Oban.Pro.Limiters.Rate.Algorithm

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def demand(windows, allowed, period, curr_time, prev_time) do
    if window_expired?(curr_time, prev_time, period) do
      allowed
    else
      total = Enum.reduce(windows, 0, fn {_key, val}, acc -> acc + val["count"] end)
      max(allowed - total, 0)
    end
  end

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def partition_demands(windows, allowed, period, curr_time, prev_time, available_keys) do
    if window_expired?(curr_time, prev_time, period) do
      Map.new(available_keys, &{&1, allowed})
    else
      untracked =
        for key <- available_keys,
            not is_map_key(windows, key),
            into: %{},
            do: {key, allowed}

      Enum.reduce(windows, untracked, fn {key, val}, acc ->
        case max(allowed - val["count"], 0) do
          demand when demand > 0 -> Map.put(acc, key, demand)
          _zero -> acc
        end
      end)
    end
  end

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

    [%{"count" => count}]
  end

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def merge(windows_a, windows_b) do
    Map.merge(windows_a, windows_b, fn _key, v1, v2 ->
      %{"count" => v1["count"] + v2["count"]}
    end)
  end

  @impl Oban.Pro.Limiters.Rate.Algorithm
  def track(windows, new_counts, period, prev_time, curr_time, _allowed) do
    new_counts = Map.new(new_counts, fn {key, count} -> {key, %{"count" => count}} end)

    if window_expired?(curr_time, prev_time, period) do
      {new_counts, curr_time}
    else
      all_windows =
        Map.merge(windows, new_counts, fn _key, existing, new ->
          %{"count" => existing["count"] + new["count"]}
        end)

      all_windows =
        for {key, val} <- all_windows,
            val["count"] > 0,
            into: %{},
            do: {key, val}

      {all_windows, prev_time}
    end
  end

  defp window_expired?(curr_time, prev_time, period) do
    curr_time - prev_time >= period
  end
end
