defmodule Parrhesia.Auth.Nip98 do
  @moduledoc """
  Minimal NIP-98 HTTP auth validation.
  """

  alias Parrhesia.Auth.Nip98ReplayCache
  alias Parrhesia.Protocol.EventValidator

  @max_age_seconds 60

  @spec validate_authorization_header(String.t() | nil, String.t(), String.t()) ::
          {:ok, map()} | {:error, atom()}
  def validate_authorization_header(authorization, method, url) do
    validate_authorization_header(authorization, method, url, [])
  end

  @spec validate_authorization_header(String.t() | nil, String.t(), String.t(), keyword()) ::
          {:ok, map()} | {:error, atom()}
  def validate_authorization_header(nil, _method, _url, _opts),
    do: {:error, :missing_authorization}

  def validate_authorization_header("Nostr " <> encoded_event, method, url, opts)
      when is_binary(method) and is_binary(url) and is_list(opts) do
    with {:ok, event_json} <- decode_base64(encoded_event),
         {:ok, event} <- JSON.decode(event_json),
         :ok <- validate_event_shape(event, opts),
         :ok <- validate_http_binding(event, method, url),
         :ok <- consume_replay_token(event, opts) do
      {:ok, event}
    else
      {:error, reason} -> {:error, reason}
      _other -> {:error, :invalid_authorization}
    end
  end

  def validate_authorization_header(_header, _method, _url, _opts),
    do: {:error, :invalid_authorization}

  defp decode_base64(encoded_event) do
    case Base.decode64(encoded_event) do
      {:ok, event_json} -> {:ok, event_json}
      :error -> Base.url_decode64(encoded_event, padding: false)
    end
  end

  defp validate_event_shape(event, opts) when is_map(event) do
    with :ok <- EventValidator.validate(event),
         :ok <- validate_kind(event),
         :ok <- validate_fresh_created_at(event, opts) do
      :ok
    else
      {:error, :stale_event} -> {:error, :stale_event}
      {:error, _reason} -> {:error, :invalid_event}
    end
  end

  defp validate_event_shape(_event, _opts), do: {:error, :invalid_event}

  defp validate_kind(%{"kind" => 27_235}), do: :ok
  defp validate_kind(_event), do: {:error, :invalid_event}

  defp validate_fresh_created_at(%{"created_at" => created_at}, opts)
       when is_integer(created_at) do
    now = System.system_time(:second)
    max_age_seconds = Keyword.get(opts, :max_age_seconds, @max_age_seconds)

    if abs(now - created_at) <= max_age_seconds do
      :ok
    else
      {:error, :stale_event}
    end
  end

  defp validate_fresh_created_at(_event, _opts), do: {:error, :invalid_event}

  defp validate_http_binding(event, method, url) do
    tags = Map.get(event, "tags", [])

    method_matches? =
      Enum.any?(tags, fn
        ["method", tagged_method | _rest] when is_binary(tagged_method) ->
          String.upcase(tagged_method) == String.upcase(method)

        _tag ->
          false
      end)

    url_matches? =
      Enum.any?(tags, fn
        ["u", tagged_url | _rest] when is_binary(tagged_url) -> tagged_url == url
        _tag -> false
      end)

    cond do
      not method_matches? -> {:error, :invalid_method_tag}
      not url_matches? -> {:error, :invalid_url_tag}
      true -> :ok
    end
  end

  defp consume_replay_token(%{"id" => event_id, "created_at" => created_at}, opts)
       when is_binary(event_id) and is_integer(created_at) do
    case Keyword.get(opts, :replay_cache, Nip98ReplayCache) do
      nil -> :ok
      replay_cache -> Nip98ReplayCache.consume(replay_cache, event_id, created_at, opts)
    end
  end

  defp consume_replay_token(_event, _opts), do: {:error, :invalid_event}
end
