defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
  @moduledoc false
  alias Mobilizon.{Actors, Discussions, Events}
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Discussions.{Comment, Discussion}
  alias Mobilizon.Events.{Event, EventOptions}
  alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
  alias Mobilizon.Federation.ActivityPub.Types.Entity
  alias Mobilizon.Federation.ActivityStream
  alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
  alias Mobilizon.Federation.ActivityStream.Convertible
  alias Mobilizon.GraphQL.API.Utils, as: APIUtils
  alias Mobilizon.Service.Activity.Comment, as: CommentActivity
  alias Mobilizon.Service.LanguageDetection
  alias Mobilizon.Share
  alias Mobilizon.Tombstone
  alias Mobilizon.Web.Endpoint
  import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
  require Logger

  @behaviour Entity

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

    if event_allows_commenting?(args) do
      case Discussions.create_comment(args) do
        {:ok, %Comment{discussion_id: discussion_id} = comment} ->
          CommentActivity.insert_activity(comment,
            subject: "comment_posted"
          )

          maybe_publish_graphql_subscription(discussion_id)
          comment_as_data = Convertible.model_to_as(comment)
          audience = Audience.get_audience(comment)
          create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
          {:ok, comment, create_data}

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

  @impl Entity
  @spec update(Comment.t(), map(), map()) ::
          {:ok, Comment.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
  def update(%Comment{} = old_comment, args, additional) do
    args = prepare_args_for_comment_update(args)

    case Discussions.update_comment(old_comment, args) do
      {:ok, %Comment{} = new_comment} ->
        {:ok, true} = Cachex.del(:activity_pub, "comment_#{new_comment.uuid}")
        comment_as_data = Convertible.model_to_as(new_comment)
        audience = Audience.get_audience(new_comment)
        update_data = make_update_data(comment_as_data, Map.merge(audience, additional))
        {:ok, new_comment, update_data}

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

  @impl Entity
  @spec delete(Comment.t(), Actor.t(), boolean, map()) ::
          {:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Comment.t()}
  def delete(
        %Comment{url: url, id: comment_id},
        %Actor{} = actor,
        _local,
        options \\ %{}
      ) do
    comment = Discussions.get_comment_with_preload(comment_id)

    activity_data = %{
      "type" => "Delete",
      "actor" => actor.url,
      "object" => %{
        "type" => "Tombstone",
        "formerType" => "Note",
        "deleted" => DateTime.utc_now(),
        "id" => url
      },
      "id" => url <> "/delete",
      "to" => ["https://www.w3.org/ns/activitystreams#Public"]
    }

    force_deletion = Map.get(options, :force, false)

    audience = Audience.get_audience(comment)

    case Discussions.delete_comment(comment, force: force_deletion) do
      {:ok, %Comment{} = updated_comment} ->
        Cachex.del(:activity_pub, "comment_#{comment.uuid}")
        Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id})
        Share.delete_all_by_uri(comment.url)
        {:ok, Map.merge(activity_data, audience), actor, updated_comment}

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

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

  def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id),
    do: Actors.get_actor(actor_id)

  def actor(_), do: nil

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

  def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
    do: Actors.get_actor(attributed_to_id)

  def group_actor(_), do: nil

  @spec permissions(Comment.t()) :: Permission.t()
  def permissions(%Comment{}),
    do: %Permission{
      access: :member,
      create: :member,
      update: :administrator,
      delete: :administrator
    }

  # Prepare and sanitize arguments for comments
  @spec prepare_args_for_comment(map) :: map
  defp prepare_args_for_comment(args) do
    with in_reply_to_comment <-
           args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(),
         event <- args |> Map.get(:event_id) |> handle_event_for_comment(),
         args <- Map.update(args, :visibility, :public, & &1),
         {text, mentions, tags} <-
           APIUtils.make_content_html(
             args |> Map.get(:text, "") |> String.trim(),
             # Can't put additional tags on a comment
             [],
             "text/html"
           ),
         tags <- ConverterUtils.fetch_tags(tags),
         mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
         lang <- Map.get(args, :language, "und") do
      Map.merge(args, %{
        actor_id: Map.get(args, :actor_id),
        text: text,
        mentions: mentions,
        tags: tags,
        event: event,
        in_reply_to_comment: in_reply_to_comment,
        in_reply_to_comment_id:
          if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)),
        origin_comment_id:
          if(is_nil(in_reply_to_comment),
            do: nil,
            else: Comment.get_thread_id(in_reply_to_comment)
          ),
        language: if(lang == "und", do: LanguageDetection.detect(:comment, args), else: lang)
      })
    end
  end

  @spec prepare_args_for_comment_update(map) :: map
  defp prepare_args_for_comment_update(args) do
    with {text, mentions, tags} <-
           APIUtils.make_content_html(
             args |> Map.get(:text, "") |> String.trim(),
             # Can't put additional tags on a comment
             [],
             "text/html"
           ),
         tags <- ConverterUtils.fetch_tags(tags),
         mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions) do
      Map.merge(args, %{text: text, mentions: mentions, tags: tags})
    end
  end

  @spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
  defp handle_event_for_comment(event_id) when not is_nil(event_id) do
    case Events.get_event_with_preload(event_id) do
      {:ok, %Event{} = event} -> event
      {:error, :event_not_found} -> nil
    end
  end

  defp handle_event_for_comment(nil), do: nil

  @spec maybe_publish_graphql_subscription(String.t() | integer() | nil) :: :ok
  defp maybe_publish_graphql_subscription(nil), do: :ok

  defp maybe_publish_graphql_subscription(discussion_id) do
    case Discussions.get_discussion(discussion_id) do
      %Discussion{} = discussion ->
        Absinthe.Subscription.publish(Endpoint, discussion,
          discussion_comment_changed: discussion.slug
        )

        :ok

      nil ->
        :ok
    end
  end

  @spec event_allows_commenting?(%{
          required(:actor_id) => String.t() | integer,
          required(:event) => Event.t() | nil,
          optional(atom) => any()
        }) :: boolean
  defp event_allows_commenting?(%{
         actor_id: actor_id,
         event: %Event{
           options: %EventOptions{comment_moderation: comment_moderation},
           organizer_actor_id: organizer_actor_id
         }
       }) do
    comment_moderation != :closed ||
      to_string(actor_id) == to_string(organizer_actor_id)
  end

  # Comments not attached to events
  defp event_allows_commenting?(_), do: true
end