defmodule Mobilizon.Federation.ActivityPub.Actor do
  @moduledoc """
  Module to handle ActivityPub Actor interactions
  """

  alias Mobilizon.Actors
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay}
  alias Mobilizon.Federation.WebFinger
  alias Mobilizon.Web.Endpoint
  require Logger
  import Mobilizon.Federation.ActivityPub.Utils, only: [are_same_origin?: 2]

  @doc """
  Getting an actor from url, eventually creating it if we don't have it locally or if it needs an update
  """
  @spec get_or_fetch_actor_by_url(url :: String.t(), options :: Keyword.t()) ::
          {:ok, Actor.t()}
          | {:error, make_actor_errors}
          | {:error, :no_internal_relay_actor}
          | {:error, :url_nil}
  def get_or_fetch_actor_by_url(url, options \\ [])

  def get_or_fetch_actor_by_url("https://www.w3.org/ns/activitystreams#Public", _preload) do
    %Actor{url: url} = Relay.get_actor()
    get_or_fetch_actor_by_url(url)
  end

  def get_or_fetch_actor_by_url(url, options) when is_binary(url) and is_list(options) do
    Logger.debug("Getting or fetching actor by URL #{url}")
    preload = Keyword.get(options, :preload, false)

    case Actors.get_actor_by_url(url, preload) do
      {:ok, %Actor{} = cached_actor} ->
        if Actors.needs_update?(cached_actor) do
          case __MODULE__.make_actor_from_url(url, options) do
            {:ok, %Actor{} = actor} -> {:ok, actor}
            {:error, _} -> {:ok, cached_actor}
          end
        else
          {:ok, cached_actor}
        end

      {:error, :actor_not_found} ->
        # For tests, see https://github.com/jjh42/mock#not-supported---mocking-internal-function-calls and Mobilizon.Federation.ActivityPubTest
        __MODULE__.make_actor_from_url(url, options)
    end
  end

  def get_or_fetch_actor_by_url(nil, _preload), do: {:error, :url_nil}

  @type make_actor_errors :: Fetcher.fetch_actor_errors() | :actor_is_local

  @doc """
  Create an actor locally by its URL (AP ID)
  """
  @spec make_actor_from_url(url :: String.t(), options :: Keyword.t()) ::
          {:ok, Actor.t()} | {:error, make_actor_errors | Ecto.Changeset.t()}
  def make_actor_from_url(url, options \\ []) do
    Logger.debug("Making actor from url #{url}")

    if are_same_origin?(url, Endpoint.url()) do
      {:error, :actor_is_local}
    else
      case Fetcher.fetch_and_prepare_actor_from_url(url, options) do
        {:ok, data} when is_map(data) ->
          Actors.upsert_actor(data, Keyword.get(options, :preload, false))

        # Request returned 410
        {:error, :actor_deleted} ->
          Logger.info("Actor #{url} was deleted")
          {:error, :actor_deleted}

        {:error, err} ->
          {:error, err}
      end
    end
  end

  @doc """
  Find an actor in our local database or call WebFinger to find what's its AP ID is and then fetch it
  """
  @spec find_or_make_actor_from_nickname(nickname :: String.t(), type :: atom() | nil) ::
          {:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()}
  def find_or_make_actor_from_nickname(nickname, type \\ nil) do
    Logger.debug("Finding or making actor from nickname #{nickname}")

    case Actors.get_actor_by_name_with_preload(nickname, type) do
      %Actor{url: actor_url} = cached_actor ->
        if Actors.needs_update?(cached_actor) do
          case __MODULE__.make_actor_from_url(actor_url, preload: true) do
            {:ok, %Actor{} = actor} -> {:ok, actor}
            {:error, _} -> {:ok, cached_actor}
          end
        else
          {:ok, cached_actor}
        end

      nil ->
        make_actor_from_nickname(nickname, preload: true)
    end
  end

  @spec find_or_make_group_from_nickname(nick :: String.t()) ::
          {:error, make_actor_errors | WebFinger.finger_errors()}
  def find_or_make_group_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Group)

  @doc """
  Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it
  """
  @spec make_actor_from_nickname(nickname :: String.t(), options :: Keyword.t()) ::
          {:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()}
  def make_actor_from_nickname(nickname, options \\ []) do
    Logger.debug("Fingering actor from nickname #{nickname}")

    case WebFinger.finger(nickname) do
      {:ok, url} when is_binary(url) ->
        Logger.debug("Matched #{nickname} to URL #{url}, now making actor")
        make_actor_from_url(url, options)

      {:error, e} ->
        {:error, e}
    end
  end
end
