defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
  @moduledoc false
  alias Mobilizon.{Actors, Instances}
  alias Mobilizon.Actors.{Actor, Follower, Member}
  alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay}
  alias Mobilizon.Federation.ActivityPub.Types.Entity
  alias Mobilizon.Federation.ActivityStream
  alias Mobilizon.Federation.ActivityStream.Convertible
  alias Mobilizon.GraphQL.API.Utils, as: APIUtils
  alias Mobilizon.Service.Activity.Group, as: GroupActivity
  alias Mobilizon.Service.Formatter.HTML
  alias Mobilizon.Service.Notifications.Scheduler
  alias Mobilizon.Web.Email.Follow, as: FollowMailer
  alias Mobilizon.Web.Endpoint
  import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]

  @behaviour Entity

  @impl Entity
  @spec create(map(), map()) ::
          {:ok, Actor.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
  def create(args, additional) do
    args = prepare_args_for_actor(args)

    case Actors.create_actor(args) do
      {:ok, %Actor{} = actor} ->
        GroupActivity.insert_activity(actor,
          subject: "group_created",
          actor_id: args.creator_actor_id
        )

        actor_as_data = Convertible.model_to_as(actor)
        audience = %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []}
        create_data = make_create_data(actor_as_data, Map.merge(audience, additional))
        {:ok, actor, create_data}

      {:error, %Ecto.Changeset{} = err} ->
        {:error, err}
    end
  end

  @impl Entity
  @spec update(Actor.t(), map, map) ::
          {:ok, Actor.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
  def update(%Actor{} = old_actor, args, additional) do
    updater_actor = Map.get(args, :updater_actor) || Map.get(additional, :updater_actor)

    if Map.get(additional, :local, false) == true or not match?(%Actor{domain: nil}, old_actor) or
         match?(%Actor{type: :Group}, old_actor) do
      case Actors.update_actor(old_actor, args) do
        {:ok, %Actor{} = new_actor} ->
          GroupActivity.insert_activity(new_actor,
            subject: "group_updated",
            old_group: old_actor,
            updater_actor: updater_actor
          )

          actor_as_data = Convertible.model_to_as(new_actor)
          Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}")
          audience = Audience.get_audience(new_actor)

          additional = Map.merge(additional, %{"actor" => (updater_actor || old_actor).url})

          update_data = make_update_data(actor_as_data, Map.merge(audience, additional))
          {:ok, new_actor, update_data}

        {:error, %Ecto.Changeset{} = err} ->
          {:error, err}
      end
    else
      {:error, :update_not_allowed}
    end
  end

  @public_ap "https://www.w3.org/ns/activitystreams#Public"

  @impl Entity
  @spec delete(Actor.t(), Actor.t(), boolean, map) ::
          {:ok, ActivityStream.t(), Actor.t(), Actor.t()} | {:error, Ecto.Changeset.t()}
  def delete(
        %Actor{
          followers_url: followers_url,
          members_url: members_url,
          url: target_actor_url,
          type: type,
          domain: domain
        } = target_actor,
        %Actor{url: actor_url, id: author_id} = actor,
        _local,
        additionnal
      ) do
    to = [@public_ap, followers_url]

    {to, cc} =
      if type == :Group do
        {to ++ [members_url], [target_actor_url]}
      else
        {to, []}
      end

    activity_data = %{
      "type" => "Delete",
      "actor" => actor_url,
      "object" => %{
        "type" => "Tombstone",
        "formerType" => to_string(type),
        "deleted" => DateTime.utc_now(),
        "id" => target_actor_url
      },
      "id" => target_actor_url <> "/delete",
      "to" => to,
      "cc" => cc
    }

    suspension = Map.get(additionnal, :suspension, false)

    case Actors.delete_actor(target_actor,
           # We completely delete the actor if the actor is remote
           reserve_username: is_nil(domain),
           suspension: suspension,
           author_id: author_id
         ) do
      {:ok, %Oban.Job{}} ->
        {:ok, activity_data, actor, target_actor}

      {:error, %Ecto.Changeset{} = err} ->
        {:error, err}
    end
  end

  @spec actor(Actor.t()) :: Actor.t() | nil
  def actor(%Actor{} = actor), do: actor

  @spec group_actor(Actor.t()) :: Actor.t() | nil
  def group_actor(%Actor{} = actor), do: actor

  @spec permissions(Actor.t()) :: Permission.t()
  def permissions(%Actor{} = _group) do
    %Permission{
      access: :member,
      create: nil,
      update: :administrator,
      delete: :administrator
    }
  end

  @spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, ActivityStreams.t(), Member.t()}
  def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do
    role =
      additional
      |> Map.get(:metadata, %{})
      |> Map.get(:role, Mobilizon.Actors.get_default_member_role(group))

    case Mobilizon.Actors.create_member(%{
           role: role,
           parent_id: group.id,
           actor_id: actor.id,
           url: Map.get(additional, :url),
           metadata:
             additional
             |> Map.get(:metadata, %{})
             |> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
         }) do
      {:ok, %Member{} = member} ->
        subject =
          case Mobilizon.Actors.get_default_member_role(group) do
            :not_approved -> "member_request"
            :member -> "member_joined"
          end

        Mobilizon.Service.Activity.Member.insert_activity(member, subject: subject)

        Absinthe.Subscription.publish(Endpoint, actor,
          group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id]
        )

        join_data = %{
          "type" => "Join",
          "id" => member.url,
          "actor" => actor.url,
          "object" => group.url
        }

        audience = Audience.get_audience(member)

        approve_if_default_role_is_member(
          group,
          actor,
          Map.merge(join_data, audience),
          member,
          role
        )

      {:error, %Ecto.Changeset{} = err} ->
        {:error, err}
    end
  end

  @spec follow(Actor.t(), Actor.t(), boolean, map) ::
          {:accept, any}
          | {:ok, ActivityStreams.t(), Follower.t()}
          | {:error,
             :person_no_follow | :already_following | :followed_suspended | Ecto.Changeset.t()}
  def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional)
      when type != :Person do
    case Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false) do
      {:ok, %Follower{actor: %Actor{type: actor_type}} = follower} ->
        # We refresh the instance materialized view to make sure the instance page will be available
        # when the admin clicks on the email link and access it
        if actor_type == :Application do
          Instances.refresh()
        end

        FollowMailer.send_notification_to_admins(follower)
        follower_as_data = Convertible.model_to_as(follower)
        approve_if_manually_approves_followers(follower, follower_as_data)

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

  # "Only group and instances can be followed"
  def follow(_, _, _, _), do: {:error, :person_no_follow}

  @spec prepare_args_for_actor(map) :: map
  defp prepare_args_for_actor(args) do
    args
    |> maybe_sanitize_username()
    |> maybe_sanitize_summary()
  end

  @spec maybe_sanitize_username(map()) :: map()
  defp maybe_sanitize_username(%{preferred_username: preferred_username} = args) do
    Map.put(args, :preferred_username, preferred_username |> HTML.strip_tags() |> String.trim())
  end

  defp maybe_sanitize_username(args), do: args

  @spec maybe_sanitize_summary(map()) :: map()
  defp maybe_sanitize_summary(%{summary: summary} = args) do
    {summary, _mentions, _tags} =
      summary
      |> String.trim()
      |> APIUtils.make_content_html([], "text/html")

    Map.put(args, :summary, summary)
  end

  defp maybe_sanitize_summary(args), do: args

  # Set the participant to approved if the default role for new participants is :participant
  @spec approve_if_default_role_is_member(
          Actor.t(),
          Actor.t(),
          ActivityStreams.t(),
          Member.t(),
          atom()
        ) ::
          {:ok, ActivityStreams.t(), Member.t()}
  defp approve_if_default_role_is_member(
         %Actor{type: :Group} = group,
         %Actor{} = actor,
         activity_data,
         %Member{} = member,
         role
       ) do
    if is_nil(group.domain) && !is_nil(actor.domain) do
      cond do
        Mobilizon.Actors.get_default_member_role(group) == :member &&
            role == :member ->
          {:accept,
           Actions.Accept.accept(
             :join,
             member,
             true,
             %{"actor" => group.url}
           )}

        Mobilizon.Actors.get_default_member_role(group) == :not_approved &&
            role == :not_approved ->
          Scheduler.pending_membership_notification(group)
          {:ok, activity_data, member}

        true ->
          {:ok, activity_data, member}
      end
    else
      {:ok, activity_data, member}
    end
  end

  @spec approve_if_manually_approves_followers(
          follower :: Follower.t(),
          follow_as_data :: ActivityStreams.t()
        ) ::
          {:accept, any} | {:ok, ActivityStreams.t(), Follower.t()}
  defp approve_if_manually_approves_followers(
         %Follower{} = follower,
         follow_as_data
       ) do
    %Actor{id: relay_id} = Relay.get_actor()

    unless follower.target_actor.manually_approves_followers or
             follower.target_actor.id == relay_id do
      require Logger
      Logger.debug("Target doesn't manually approves followers, we can accept right away")

      {:accept,
       Actions.Accept.accept(
         :follow,
         follower,
         true,
         %{"actor" => follower.actor.url}
       )}
    end

    {:ok, follow_as_data, follower}
  end
end
