defmodule Mobilizon.Service.ActorSuspension do
  @moduledoc """
  Handle actor suspensions
  """

  alias Ecto.Multi
  alias Mobilizon.{Actors, Events, Medias, Users}
  alias Mobilizon.Actors.{Actor, Member}
  alias Mobilizon.Discussions.{Comment, Discussion}
  alias Mobilizon.Events.{Event, Participant}
  alias Mobilizon.Medias.File
  alias Mobilizon.Posts.Post
  alias Mobilizon.Resources.Resource
  alias Mobilizon.Service.ErrorReporting.Sentry
  alias Mobilizon.Service.Export.Cachable
  alias Mobilizon.Storage.Repo
  alias Mobilizon.Users.User
  alias Mobilizon.Web.Email.Actor, as: ActorEmail
  alias Mobilizon.Web.Email.Group
  require Logger
  import Ecto.Query

  @actor_preloads [:user, :organized_events, :participations, :comments]
  @delete_actor_default_options [reserve_username: true, suspension: false]
  @valid_actor_types [:Person, :Group]

  @doc """
  Deletes an actor.
  """
  @spec suspend_actor(Actor.t(), Keyword.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
  def suspend_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do
    Logger.info("Going to delete actor #{actor.url}")
    actor = Repo.preload(actor, @actor_preloads)

    delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
    Logger.debug(inspect(delete_actor_options))

    send_suspension_notification(actor)

    Logger.debug(
      "Sending suspension notifications to participants from events created by this actor"
    )

    notify_event_participants_from_suspension(actor)
    delete_participations(actor)

    multi =
      Multi.new()
      |> maybe_reset_actor_id(actor)
      |> delete_actor_empty_comments(actor)
      |> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end)
      |> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end)

    multi =
      if Keyword.get(delete_actor_options, :reserve_username, true) do
        multi
        |> delete_actor_events(actor)
        |> delete_posts(actor)
        |> delete_ressources(actor)
        |> delete_discussions(actor)
        |> delete_members(actor)
        |> Multi.update(:actor, Actor.delete_changeset(actor))
      else
        Multi.delete(multi, :actor, actor)
      end

    Logger.debug("Going to run the transaction")

    case Repo.transaction(multi, timeout: 60_000) do
      {:ok, %{actor: %Actor{} = actor}} ->
        {:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
        Cachable.clear_all_caches(actor)
        Logger.info("Deleted actor #{actor.url}")
        {:ok, actor}

      {:error, remove, error, _} when remove in [:remove_banner, :remove_avatar] ->
        Logger.error("Error while deleting actor's banner or avatar")

        Sentry.capture_message("Error while deleting actor's banner or avatar",
          extra: %{err: error}
        )

        Logger.debug(inspect(error, pretty: true))
        {:error, error}

      err ->
        Logger.error("Unknown error while deleting actor")

        Sentry.capture_message("Error while deleting actor's banner or avatar",
          extra: %{err: err}
        )

        Logger.debug(inspect(err, pretty: true))
        {:error, err}
    end
  end

  # When deleting a profile, reset default_actor_id
  @spec maybe_reset_actor_id(Multi.t(), Actor.t()) :: Multi.t()
  defp maybe_reset_actor_id(%Multi{} = multi, %Actor{type: :Person} = actor) do
    Multi.run(multi, :reset_default_actor_id, fn _, _ ->
      reset_default_actor_id(actor)
    end)
  end

  defp maybe_reset_actor_id(%Multi{} = multi, %Actor{type: :Group} = _actor) do
    multi
  end

  defp delete_actor_empty_comments(%Multi{} = multi, %Actor{id: actor_id}) do
    now = DateTime.utc_now() |> DateTime.truncate(:second)

    Multi.update_all(multi, :empty_comments, where(Comment, [c], c.actor_id == ^actor_id),
      set: [
        text: nil,
        actor_id: nil,
        deleted_at: now
      ]
    )
  end

  @spec notify_event_participants_from_suspension(Actor.t()) :: :ok
  defp notify_event_participants_from_suspension(%Actor{id: actor_id, type: actor_type} = actor)
       when actor_type in @valid_actor_types do
    actor
    |> get_actor_organizer_events_participations()
    |> preload([:actor, :event])
    |> Repo.all()
    |> Enum.filter(fn %Participant{actor: %Actor{id: participant_actor_id}} ->
      participant_actor_id != actor_id
    end)
    |> Enum.map(fn %Participant{} = participant ->
      ActorEmail.send_notification_event_participants_from_suspension(participant, actor)
      participant
    end)
    |> Enum.each(&Events.delete_participant/1)
  end

  defp notify_event_participants_from_suspension(_), do: :ok

  @spec get_actor_organizer_events_participations(Actor.t()) :: Ecto.Query.t()
  defp get_actor_organizer_events_participations(%Actor{type: :Person, id: actor_id}) do
    do_get_actor_organizer_events_participations()
    |> where([_p, e], e.organizer_actor_id == ^actor_id)
  end

  defp get_actor_organizer_events_participations(%Actor{type: :Group, id: actor_id}) do
    do_get_actor_organizer_events_participations()
    |> where([_p, e], e.attributed_to_id == ^actor_id)
  end

  @spec do_get_actor_organizer_events_participations :: Ecto.Query.t()
  defp do_get_actor_organizer_events_participations do
    Participant
    |> join(:inner, [p], e in Event, on: p.event_id == e.id)
    |> where([_p, e], e.begins_on > ^DateTime.utc_now())
    |> where([p, _e], p.role in [:participant, :moderator, :administrator])
  end

  @spec delete_actor_events(Ecto.Multi.t(), Actor.t()) :: Ecto.Multi.t()
  defp delete_actor_events(%Multi{} = multi, %Actor{type: :Person, id: actor_id}) do
    Logger.debug("Delete profile's events")
    Multi.delete_all(multi, :delete_events, where(Event, [e], e.organizer_actor_id == ^actor_id))
  end

  defp delete_actor_events(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
    Logger.debug("Delete group's events")
    Multi.delete_all(multi, :delete_events, where(Event, [e], e.attributed_to_id == ^actor_id))
  end

  defp delete_posts(%Multi{} = multi, %Actor{type: :Person, id: actor_id}) do
    Logger.debug("Delete profile's posts")
    Multi.delete_all(multi, :delete_posts, where(Post, [e], e.author_id == ^actor_id))
  end

  defp delete_posts(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
    Logger.debug("Delete group's posts")
    Multi.delete_all(multi, :delete_posts, where(Post, [e], e.attributed_to_id == ^actor_id))
  end

  defp delete_ressources(%Multi{} = multi, %Actor{type: :Person, id: actor_id}) do
    Logger.debug("Delete profile's resources")
    Multi.delete_all(multi, :delete_resources, where(Resource, [e], e.creator_id == ^actor_id))
  end

  defp delete_ressources(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
    Logger.debug("Delete group's resources")
    Multi.delete_all(multi, :delete_resources, where(Resource, [e], e.actor_id == ^actor_id))
  end

  # Keep discussions just in case, comments are already emptied
  defp delete_discussions(%Multi{} = multi, %Actor{type: :Person}) do
    multi
  end

  defp delete_discussions(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
    Logger.debug("Delete group's discussions")

    multi =
      Multi.run(multi, :group_discussion_comments, fn _, _ ->
        group_comments_ids =
          Comment
          |> join(:inner, [c], d in Discussion, on: c.discussion_id == d.id)
          |> where([_c, d], d.actor_id == ^actor_id)
          |> select([c], [c.id])
          |> Repo.all()
          |> Enum.concat()

        {:ok, group_comments_ids}
      end)

    multi =
      Multi.delete_all(
        multi,
        :delete_discussions_comments,
        fn %{group_discussion_comments: group_discussion_comments} ->
          where(Comment, [c], c.id in ^group_discussion_comments)
        end
      )

    Multi.delete_all(multi, :delete_discussions, where(Discussion, [e], e.actor_id == ^actor_id))
  end

  @spec delete_participations(Actor.t()) :: :ok
  defp delete_participations(%Actor{type: :Person} = actor) do
    Logger.debug("Delete participations from events created by this actor")
    %Actor{participations: participations} = Repo.preload(actor, [:participations])
    Enum.each(participations, &Events.delete_participant/1)
  end

  defp delete_participations(%Actor{type: :Group}), do: :ok

  @spec delete_members(Multi.t(), Actor.t()) :: Multi.t()
  defp delete_members(%Multi{} = multi, %Actor{type: :Person, id: actor_id}) do
    Multi.delete_all(multi, :delete_members, where(Member, [e], e.actor_id == ^actor_id))
  end

  defp delete_members(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
    Multi.delete_all(multi, :delete_members, where(Member, [e], e.parent_id == ^actor_id))
  end

  @spec reset_default_actor_id(Actor.t()) :: {:ok, User.t() | nil} | {:error, :user_not_found}
  defp reset_default_actor_id(%Actor{type: :Person, user: %User{} = user, id: actor_id}) do
    Logger.debug("reset_default_actor_id")

    new_actor =
      user
      |> Users.get_actors_for_user()
      |> Enum.find(&(&1.id !== actor_id))

    {:ok, Users.update_user_default_actor(user, new_actor)}
  rescue
    _e in Ecto.NoResultsError ->
      {:error, :user_not_found}
  end

  defp reset_default_actor_id(%Actor{type: :Person, user: nil}), do: {:ok, nil}

  @spec remove_banner(Actor.t()) :: {:ok, Actor.t()}
  defp remove_banner(%Actor{banner: nil} = actor), do: {:ok, actor}

  defp remove_banner(%Actor{banner: %File{url: url}} = actor) do
    safe_remove_file(url, actor)
    {:ok, actor}
  end

  @spec remove_avatar(Actor.t()) :: {:ok, Actor.t()}
  defp remove_avatar(%Actor{avatar: avatar} = actor) do
    case avatar do
      %File{url: url} ->
        safe_remove_file(url, actor)
        {:ok, actor}

      nil ->
        {:ok, actor}
    end
  end

  @spec safe_remove_file(String.t(), Actor.t()) :: {:ok, Actor.t()}
  defp safe_remove_file(url, %Actor{} = actor) do
    case Medias.delete_user_profile_media_by_url(url) do
      {:ok, _value} ->
        {:ok, actor}

      {:error, error} ->
        Logger.error("Error while removing an upload file",
          error: inspect(error),
          actor: Actor.preferred_username_and_domain(actor),
          file_url: url
        )

        Logger.debug(inspect(error))

        {:ok, actor}
    end
  end

  @spec send_suspension_notification(Actor.t()) :: :ok
  defp send_suspension_notification(%Actor{type: :Group} = group) do
    Logger.debug("Sending suspension notifications to group members")

    group
    |> Actors.list_all_local_members_for_group()
    |> Enum.each(&Group.send_group_suspension_notification/1)
  end

  defp send_suspension_notification(%Actor{} = _actor), do: :ok
end
