defmodule Mobilizon.Instances do
  @moduledoc """
  The instances context
  """
  alias Ecto.Adapters.SQL
  alias Mobilizon.Actors.{Actor, Follower}
  alias Mobilizon.Federation.ActivityPub.Relay
  alias Mobilizon.Instances.{Instance, InstanceActor}
  alias Mobilizon.Storage.{Page, Repo}
  import Ecto.Query

  @is_null_fragment "CASE WHEN ? IS NULL THEN FALSE ELSE TRUE END"

  @spec instances(Keyword.t()) :: Page.t(Instance.t())
  def instances(options) do
    page = Keyword.get(options, :page, 1)
    limit = Keyword.get(options, :limit, 10)
    order_by = Keyword.get(options, :order_by, :event_count)
    direction = Keyword.get(options, :direction, :desc)
    filter_domain = Keyword.get(options, :filter_domain)
    # suspend_status = Keyword.get(options, :filter_suspend_status, :all)
    follow_status = Keyword.get(options, :filter_follow_status, :all)

    order_by_options = Keyword.new([{direction, order_by}])

    %Actor{id: relay_id} = Relay.get_actor()

    query =
      Instance
      |> join(:left, [i], ia in InstanceActor, on: i.domain == ia.domain)
      |> join(:left, [_i, ia], a in Actor, on: ia.actor_id == a.id)
      # following
      |> join(:left, [_i, _ia, a], f1 in Follower, on: f1.target_actor_id == a.id)
      # followed
      |> join(:left, [_i, _ia, a], f2 in Follower, on: f2.actor_id == a.id)
      |> select([i, ia, a, f1, f2], %{
        instance: i,
        instance_actor: ia,
        actor: a,
        domain: a.domain,
        has_relay: fragment(@is_null_fragment, a.id),
        following: fragment(@is_null_fragment, f2.id),
        following_approved: f2.approved,
        follower: fragment(@is_null_fragment, f1.id),
        follower_approved: f1.approved
      })
      |> order_by(^order_by_options)

    query =
      if is_nil(filter_domain) or filter_domain == "" do
        query
      else
        where(
          query,
          [i, ia],
          like(i.domain, ^"%#{filter_domain}%") or like(ia.instance_name, ^"%#{filter_domain}%")
        )
      end

    query =
      case follow_status do
        :following ->
          where(query, [_i, _ia, _a, f1], f1.actor_id == ^relay_id and f1.approved == true)

        :followed ->
          where(
            query,
            [_i, _ia, _a, _f1, f2],
            f2.target_actor_id == ^relay_id and f2.approved == true
          )

        :all ->
          query
      end

    %Page{elements: elements} = paged_instances = Page.build_page(query, page, limit)

    %Page{
      paged_instances
      | elements: Enum.map(elements, &convert_instance_meta/1)
    }
  end

  @spec instance(String.t()) :: Instance.t() | nil
  def instance(domain) do
    Instance
    |> where(domain: ^domain)
    |> Repo.one()
  end

  @spec all_domains :: list(Instance.t())
  def all_domains do
    Instance
    |> distinct(true)
    |> select([:domain])
    |> Repo.all()
  end

  @spec refresh :: %{
          :rows => nil | [[term()] | binary()],
          :num_rows => non_neg_integer(),
          optional(atom()) => any()
        }
  def refresh do
    SQL.query!(Repo, "REFRESH MATERIALIZED VIEW instances")
  end

  defp convert_instance_meta(%{
         instance: instance,
         instance_actor: instance_meta,
         actor: instance_actor,
         domain: _domain,
         follower: follower,
         follower_approved: follower_approved,
         following: following,
         following_approved: following_approved,
         has_relay: has_relay
       }) do
    instance
    |> Map.put(:follower_status, follow_status(following, following_approved))
    |> Map.put(:followed_status, follow_status(follower, follower_approved))
    |> Map.put(:has_relay, has_relay)
    |> Map.put(
      :relay_address,
      if(is_nil(instance_actor),
        do: nil,
        else: Actor.preferred_username_and_domain(instance_actor)
      )
    )
    |> Map.put(:instance_actor, instance_actor)
    |> add_metadata_details(instance_meta)
  end

  @spec add_metadata_details(map(), InstanceActor.t() | nil) :: map()
  defp add_metadata_details(instance, nil), do: instance

  defp add_metadata_details(instance, instance_meta) do
    instance
    |> Map.put(:instance_name, instance_meta.instance_name)
    |> Map.put(:instance_description, instance_meta.instance_description)
    |> Map.put(:software, instance_meta.software)
    |> Map.put(:software_version, instance_meta.software_version)
  end

  defp follow_status(true, true), do: :approved
  defp follow_status(true, false), do: :pending
  defp follow_status(false, _), do: :none
  defp follow_status(nil, _), do: :none

  @spec get_instance_actor(String.t()) :: InstanceActor.t() | nil
  def get_instance_actor(domain) do
    InstanceActor
    |> Repo.get_by(domain: domain)
    |> Repo.preload(:actor)
  end

  @doc """
  Creates an instance actor.
  """
  @spec create_instance_actor(map) :: {:ok, InstanceActor.t()} | {:error, Ecto.Changeset.t()}
  def create_instance_actor(attrs \\ %{}) do
    with {:ok, %InstanceActor{} = instance_actor} <-
           %InstanceActor{}
           |> InstanceActor.changeset(attrs)
           |> Repo.insert(on_conflict: :replace_all, conflict_target: :domain) do
      {:ok, Repo.preload(instance_actor, :actor)}
    end
  end

  @doc """
  Updates an instance actor.
  """
  @spec update_instance_actor(InstanceActor.t(), map) ::
          {:ok, InstanceActor.t()} | {:error, Ecto.Changeset.t()}
  def update_instance_actor(%InstanceActor{} = instance_actor, attrs) do
    with {:ok, %InstanceActor{} = instance_actor} <-
           instance_actor
           |> Repo.preload(:actor)
           |> InstanceActor.changeset(attrs)
           |> Repo.update() do
      {:ok, Repo.preload(instance_actor, :actor)}
    end
  end

  @doc """
  Deletes a post
  """
  @spec delete_instance_actor(InstanceActor.t()) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
  def delete_instance_actor(%InstanceActor{} = instance_actor) do
    Repo.delete(instance_actor)
  end
end