defmodule AprsmeWeb.Plugs.RateLimiter do
  @moduledoc """
  Rate limiting plug to prevent DoS attacks
  """
  import Phoenix.Controller
  import Plug.Conn

  @type key_type :: :ip | :user_agent | (Plug.Conn.t() -> String.t()) | String.t()
  @type init_opts :: [
          scale: integer(),
          limit: integer(),
          key: key_type(),
          prefix: String.t(),
          error_message: String.t()
        ]

  @spec init(Keyword.t()) :: init_opts()
  def init(opts) do
    # Default options
    Keyword.merge(
      [
        # 1 minute
        scale: 60_000,
        # 100 requests per minute
        limit: 100,
        # Rate limit by IP address
        key: :ip,
        # Prefix for ETS key namespace
        prefix: "rate_limit",
        error_message: "Too many requests"
      ],
      opts
    )
  end

  @spec call(Plug.Conn.t(), init_opts()) :: Plug.Conn.t()
  def call(conn, opts) do
    key = get_key(conn, opts[:key])
    scale = opts[:scale]
    limit = opts[:limit]
    prefix = opts[:prefix]
    error_message = opts[:error_message]

    case Aprsme.RateLimiter.hit("#{prefix}:#{key}", scale, limit) do
      {:allow, _count} ->
        conn

      {:deny, _retry_after} ->
        conn
        |> put_status(:too_many_requests)
        |> json(%{error: error_message})
        |> halt()
    end
  end

  # ---------------------------------------------------------------------------
  # Public helpers for use outside the plug pipeline (LiveViews, channels, etc.)
  # ---------------------------------------------------------------------------

  @doc """
  Returns the client IP address from a LiveView socket.

  Uses `get_connect_info/2` to extract `:peer_data` — ensure the endpoint
  socket declaration includes `:peer_data` in its `connect_info` list.
  Falls back to `"unknown"` when peer_data is unavailable.
  """
  @spec extract_socket_ip(Phoenix.LiveView.Socket.t()) :: String.t()
  def extract_socket_ip(socket) do
    case Phoenix.LiveView.get_connect_info(socket, :peer_data) do
      %{address: address} when not is_nil(address) ->
        address |> :inet.ntoa() |> to_string()

      _ ->
        # Fall back to socket assign set during mount (dead render path)
        Map.get(socket.assigns, :client_ip, "unknown")
    end
  end

  @doc """
  Minimal rate-limit check for use in LiveView event handlers and channels.

  Returns `{:allow, count}` or `{:deny, retry_after_ms}`.
  """
  @spec check_rate_limit(String.t(), integer(), integer()) ::
          {:allow, non_neg_integer()} | {:deny, non_neg_integer()}
  def check_rate_limit(key, scale, limit) do
    Aprsme.RateLimiter.hit(key, scale, limit)
  end

  # ---------------------------------------------------------------------------
  # Private key-extraction helpers
  # ---------------------------------------------------------------------------

  @spec get_key(Plug.Conn.t(), key_type()) :: String.t()
  defp get_key(conn, :ip) do
    # Check headers in order of preference
    case {get_req_header(conn, "cf-connecting-ip"), get_req_header(conn, "x-forwarded-for"),
          get_req_header(conn, "x-real-ip")} do
      # Cloudflare header takes precedence
      {[cf | _], _, _} ->
        cf

      # Then standard X-Forwarded-For header
      {[], [forwarded | _], _} ->
        forwarded |> String.split(",") |> List.first() |> String.trim()

      # Then X-Real-IP header
      {[], [], [real | _]} ->
        real

      # Fall back to remote_ip
      {[], [], []} ->
        conn.remote_ip |> :inet.ntoa() |> to_string()
    end
  end

  defp get_key(conn, :user_agent) do
    case get_req_header(conn, "user-agent") do
      [ua | _] -> ua
      [] -> "unknown"
    end
  end

  defp get_key(conn, custom_key) when is_function(custom_key) do
    custom_key.(conn)
  end

  defp get_key(_conn, key) when is_binary(key) do
    key
  end
end
