defmodule Mobilizon.GraphQL.API.Search do
  @moduledoc """
  API for search.
  """

  alias Mobilizon.Actors
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Events
  alias Mobilizon.Events.Event
  alias Mobilizon.Federation.ActivityPub
  alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
  alias Mobilizon.Service.GlobalSearch
  alias Mobilizon.Storage.Page
  import Mobilizon.GraphQL.Resolvers.Event.Utils

  require Logger

  @doc """
  Searches actors.
  """
  @spec search_actors(map(), integer | nil, integer | nil, atom()) ::
          {:ok, Page.t(Actor.t())} | {:error, String.t()}
  def search_actors(%{term: term} = args, page \\ 1, limit \\ 10, result_type) do
    term = String.trim(term)

    cond do
      # Some URLs could be domain.tld/@username, so keep this condition above
      # the `is_handle` function
      is_url(term) ->
        # skip, if it's not an actor
        case process_from_url(term) do
          %Page{total: _total, elements: [%Actor{} = _actor]} = page ->
            {:ok, page}

          _ ->
            {:ok, %{total: 0, elements: []}}
        end

      is_handle(term) ->
        {:ok, process_from_username(term)}

      true ->
        if is_global_search(args) do
          service = GlobalSearch.service()

          {:ok, service.search_groups(Keyword.new(args, fn {k, v} -> {k, v} end))}
        else
          page =
            Actors.search_actors(
              term,
              [
                actor_type: result_type,
                radius: Map.get(args, :radius),
                location: Map.get(args, :location),
                bbox: Map.get(args, :bbox),
                minimum_visibility: Map.get(args, :minimum_visibility, :public),
                current_actor_id: Map.get(args, :current_actor_id),
                exclude_my_groups: Map.get(args, :exclude_my_groups, false),
                exclude_stale_actors: true
              ],
              page,
              limit
            )

          {:ok, page}
        end
    end
  end

  @doc """
  Search events
  """
  @spec search_events(map(), integer | nil, integer | nil) ::
          {:ok, Page.t(Event.t())}
  def search_events(%{term: term} = args, page \\ 1, limit \\ 10) do
    term = String.trim(term)

    if is_url(term) do
      # skip, if it's not an event
      case process_from_url(term) do
        %Page{total: _total, elements: [%Event{} = event]} = page ->
          if Map.get(args, :current_user) != nil || check_event_access?(event) do
            {:ok, page}
          else
            {:ok, %{total: 0, elements: []}}
          end

        _ ->
          {:ok, %{total: 0, elements: []}}
      end
    else
      if is_global_search(args) do
        service = GlobalSearch.service()

        {:ok, service.search_events(Keyword.new(args, fn {k, v} -> {k, v} end))}
      else
        {:ok, Events.build_events_for_search(Map.put(args, :term, term), page, limit)}
      end
    end
  end

  @spec interact(String.t()) :: {:ok, struct()} | {:error, :not_found}
  def interact(uri) do
    case ActivityPub.fetch_object_from_url(uri) do
      {:ok, object} ->
        {:ok, object}

      {:error, _err} ->
        Logger.debug(fn -> "Unable to find or make object from URI '#{uri}'" end)

        {:error, :not_found}
    end
  end

  # If the search string is an username
  @spec process_from_username(String.t()) :: Page.t(Actor.t())
  defp process_from_username(search) do
    case ActivityPubActor.find_or_make_actor_from_nickname(search) do
      {:ok, %Actor{type: :Group} = actor} ->
        %Page{total: 1, elements: [actor]}

      # Don't return anything else than groups
      {:ok, %Actor{}} ->
        %Page{total: 0, elements: []}

      {:error, _err} ->
        Logger.debug(fn -> "Unable to find or make actor '#{search}'" end)

        %Page{total: 0, elements: []}
    end
  end

  # If the search string is an URL
  @spec process_from_url(String.t()) :: Page.t(struct())
  defp process_from_url(search) do
    case ActivityPub.fetch_object_from_url(search) do
      {:ok, object} ->
        %Page{total: 1, elements: [object]}

      {:error, _err} ->
        Logger.debug(fn -> "Unable to find or make object from URL '#{search}'" end)

        %Page{total: 0, elements: []}
    end
  end

  @spec is_url(String.t()) :: boolean
  defp is_url(search), do: String.starts_with?(search, ["http://", "https://"])

  @spec is_handle(String.t()) :: boolean
  defp is_handle(search), do: String.match?(search, ~r/@/)

  defp is_global_search(%{search_target: :global}) do
    global_search_enabled?()
  end

  defp is_global_search(_), do: global_search_enabled?() && global_search_default?()

  defp global_search_enabled? do
    Application.get_env(:mobilizon, :search) |> get_in([:global]) |> get_in([:is_enabled])
  end

  defp global_search_default? do
    Application.get_env(:mobilizon, :search) |> get_in([:global]) |> get_in([:is_default_search])
  end
end
