defmodule TarakanWeb.Plugs.CodeBrowserRateLimit do
  @moduledoc """
  Per-IP budget for browser traffic, which includes the code browser.

  Anonymous code browsing spawns git subprocesses against local mirrors and
  can enqueue outbound mirror fetches, so each client IP gets a generous
  request budget (default 300/min). IPv6 clients bucket by their /64
  prefix. Platform admins and moderators are exempt (operational tooling);
  the plug runs after the session scope is fetched so the exemption can see
  the current role.
  """

  import Plug.Conn

  alias Tarakan.RateLimiter
  alias TarakanWeb.Plugs.ClientIp

  @defaults [limit: 300, window_seconds: 60]
  @unlimited_roles ~w(admin moderator)

  @doc false
  def init(opts) do
    configured = Application.get_env(:tarakan, __MODULE__, [])
    @defaults |> Keyword.merge(configured) |> Keyword.merge(opts)
  end

  @doc false
  def call(conn, opts) do
    if unlimited_actor?(conn) do
      conn
    else
      limit = Keyword.fetch!(opts, :limit)
      window_seconds = Keyword.fetch!(opts, :window_seconds)
      key = {:code_browser_ip, ClientIp.remote_ip_bucket(conn)}

      case RateLimiter.check(key, limit, window_seconds) do
        :ok -> conn
        {:error, _reason, retry_after} -> reject(conn, retry_after)
      end
    end
  end

  defp unlimited_actor?(conn) do
    case conn.assigns[:current_scope] do
      %{platform_role: role} when role in @unlimited_roles -> true
      %{account: %{platform_role: role}} when role in @unlimited_roles -> true
      _ -> false
    end
  end

  defp reject(conn, retry_after) do
    conn
    |> put_resp_header("retry-after", Integer.to_string(retry_after))
    |> put_resp_content_type("text/plain")
    |> send_resp(429, "too many requests")
    |> halt()
  end
end
