defmodule Mobilizon.Federation.NodeInfo do
  @moduledoc """
  Performs NodeInfo requests
  """

  alias Mobilizon.Service.HTTP.WebfingerClient
  require Logger
  import Mobilizon.Service.HTTP.Utils, only: [content_type_matches?: 2]

  @application_uri "https://www.w3.org/ns/activitystreams#Application"
  @nodeinfo_rel_2_0 "http://nodeinfo.diaspora.software/ns/schema/2.0"
  @nodeinfo_rel_2_1 "http://nodeinfo.diaspora.software/ns/schema/2.1"

  @env Application.compile_env(:mobilizon, :env)

  @spec application_actor(String.t()) :: String.t() | nil
  def application_actor(host) do
    Logger.debug("Fetching application actor from NodeInfo data for domain #{host}")

    case fetch_nodeinfo_endpoint(host) do
      {:ok, body} ->
        extract_application_actor(body)

      {:error, _err} ->
        nil
    end
  end

  @spec nodeinfo(String.t()) :: {:ok, map()} | {:error, atom()}
  def nodeinfo(host) do
    Logger.debug("Fetching NodeInfo details for domain #{host}")

    with {:ok, endpoint} when is_binary(endpoint) <- fetch_nodeinfo_details(host),
         :ok <- Logger.debug("Going to get NodeInfo information from URL #{endpoint}"),
         {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 <-
           WebfingerClient.get(endpoint),
         {:ok, body} <- validate_json_response(body, headers) do
      Logger.debug("Found nodeinfo information for domain #{host}")
      {:ok, body}
    else
      {:error, err} ->
        {:error, err}

      err ->
        Logger.debug("Failed to fetch NodeInfo data from endpoint #{inspect(err)}")
        {:error, :node_info_endpoint_http_error}
    end
  end

  defp extract_application_actor(body) do
    body
    |> Map.get("links", [])
    |> Enum.find(%{"rel" => @application_uri, "href" => nil}, fn %{"rel" => rel, "href" => href} ->
      rel == @application_uri and is_binary(href)
    end)
    |> Map.get("href")
  end

  @spec fetch_nodeinfo_endpoint(String.t()) :: {:ok, map()} | {:error, atom()}
  defp fetch_nodeinfo_endpoint(host) do
    prefix = if @env !== :dev, do: "https", else: "http"

    case WebfingerClient.get("#{prefix}://#{host}/.well-known/nodeinfo") do
      {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
        validate_json_response(body, headers)

      err ->
        Logger.debug("Failed to fetch NodeInfo data #{inspect(err)}")
        {:error, :node_info_meta_http_error}
    end
  end

  @spec fetch_nodeinfo_details(String.t()) :: {:ok, String.t()} | {:error, atom()}
  defp fetch_nodeinfo_details(host) do
    with {:ok, body} <- fetch_nodeinfo_endpoint(host) do
      extract_nodeinfo_endpoint(body)
    end
  end

  @spec extract_nodeinfo_endpoint(map()) ::
          {:ok, String.t()}
          | {:error, :no_node_info_endpoint_found | :no_valid_node_info_endpoint_found}
  defp extract_nodeinfo_endpoint(body) do
    links = Map.get(body, "links", [])

    relation =
      find_nodeinfo_relation(links, @nodeinfo_rel_2_1) ||
        find_nodeinfo_relation(links, @nodeinfo_rel_2_0)

    if is_nil(relation) do
      {:error, :no_node_info_endpoint_found}
    else
      endpoint = Map.get(relation, "href")

      if is_nil(endpoint) do
        {:error, :no_valid_node_info_endpoint_found}
      else
        {:ok, endpoint}
      end
    end
  end

  defp find_nodeinfo_relation(links, relation) do
    Enum.find(links, fn %{"rel" => rel, "href" => href} ->
      rel == relation and is_binary(href)
    end)
  end

  @spec validate_json_response(map() | String.t(), list()) ::
          {:ok, String.t()} | {:error, :bad_content_type | :body_not_json}
  defp validate_json_response(body, headers) do
    cond do
      !content_type_matches?(headers, "application/json") ->
        {:error, :bad_content_type}

      !is_map(body) ->
        {:error, :body_not_json}

      true ->
        {:ok, body}
    end
  end
end