defmodule Mix.Tasks.Mobilizon.Maintenance.DetectSpam do
  @moduledoc """
  Task to scan all profiles and events against spam detector and report them
  """
  use Mix.Task
  alias Mobilizon.{Actors, Config, Events, Users}
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Events.Event
  alias Mobilizon.Service.AntiSpam
  import Mix.Tasks.Mobilizon.Common
  alias Mobilizon.Federation.ActivityPub.Actions
  use Mobilizon.Web, :verified_routes

  @shortdoc "Scan all profiles and events against spam detector and report them"

  @impl Mix.Task
  def run(options) do
    {options, [], []} =
      OptionParser.parse(
        options,
        strict: [
          dry_run: :boolean,
          verbose: :boolean,
          forward_reports: :boolean,
          local_only: :boolean,
          only_profiles: :boolean,
          only_events: :boolean
        ],
        aliases: [
          d: :dry_run,
          v: :verbose,
          f: :forward_reports,
          l: :local_only,
          p: :only_profiles,
          e: :only_events
        ]
      )

    start_mobilizon()

    unless anti_spam().ready?() do
      shell_error("Akismet is missing an API key in the configuration")
    end

    anonymous_actor_id = Config.anonymous_actor_id()

    unless only_events?(options) do
      options
      |> Keyword.get(:local_only, false)
      |> profiles()
      |> Stream.flat_map(& &1)
      |> Stream.each(fn profile ->
        process_profile(profile, Keyword.put(options, :anonymous_actor_id, anonymous_actor_id))
      end)
      |> Stream.run()
    end

    unless only_profiles?(options) do
      options
      |> Keyword.get(:local_only, false)
      |> events()
      |> Stream.flat_map(& &1)
      |> Stream.each(fn event ->
        process_event(event, Keyword.put(options, :anonymous_actor_id, anonymous_actor_id))
      end)
      |> Stream.run()
    end
  end

  defp profiles(local_only) do
    shell_info("Starting scanning of profiles")

    Actors.stream_persons("", "", "", local_only || nil, false)
  end

  defp events(local_only) do
    shell_info("Starting scanning of events")

    Events.stream_events(local_only || nil)
  end

  defp process_profile(
         %Actor{preferred_username: preferred_username, summary: summary, user: user, id: id},
         options
       ) do
    email = if(is_nil(user), do: nil, else: user.email)
    ip = if(is_nil(user), do: nil, else: user.current_sign_in_ip || user.last_sign_in_ip)

    case anti_spam().check_profile(preferred_username, summary, email, ip, nil) do
      res when res in [:spam, :discard] ->
        handle_spam_profile(preferred_username, id, options)

      :ham ->
        if verbose?(options) do
          shell_info("Profile #{preferred_username} is fine")
        end

      err ->
        shell_error(inspect(err))
    end
  end

  defp process_event(
         %Event{
           description: event_description,
           organizer_actor: organizer_actor,
           id: event_id,
           title: title,
           uuid: uuid
         },
         options
       ) do
    {email, ip} =
      if organizer_actor.user_id do
        user = Users.get_user(organizer_actor.user_id)
        email = if(is_nil(user), do: nil, else: user.email)
        ip = if(is_nil(user), do: nil, else: user.current_sign_in_ip || user.last_sign_in_ip)
        {email, ip}
      else
        {nil, nil}
      end

    case anti_spam().check_event(
           event_description,
           organizer_actor.preferred_username,
           email,
           ip,
           nil
         ) do
      res when res in [:spam, :discard] ->
        handle_spam_event(event_id, title, uuid, organizer_actor.id, options)

      :ham ->
        if verbose?(options) do
          shell_info("Event #{title} is fine")
        end

      err ->
        shell_error(inspect(err))
    end
  end

  defp handle_spam_profile(preferred_username, organizer_actor_id, options) do
    shell_info("Detected profile #{preferred_username} as spam")

    unless dry_run?(options) do
      report_spam_profile(preferred_username, organizer_actor_id, options)
    end
  end

  defp report_spam_profile(profile_preferred_username, organizer_actor_id, options) do
    shell_info("Reporting profile #{profile_preferred_username} as spam")

    Actions.Flag.flag(
      %{
        reported_id: organizer_actor_id,
        reporter_id: Keyword.fetch!(options, :anonymous_actor_id),
        content: "This is an automatic report issued by Akismet"
      },
      Keyword.get(options, :forward_reports, false)
    )
  end

  defp handle_spam_event(event_id, event_title, event_uuid, organizer_actor_id, options) do
    shell_info("Detected event #{event_title} as spam: #{url(~p"/events/#{event_uuid}")}")

    unless dry_run?(options) do
      report_spam_event(event_id, event_title, organizer_actor_id, options)
    end
  end

  defp report_spam_event(event_id, event_title, organizer_actor_id, options) do
    shell_info("Reporting event #{event_title} as spam")

    Actions.Flag.flag(
      %{
        reported_id: organizer_actor_id,
        reporter_id: Keyword.fetch!(options, :anonymous_actor_id),
        event_id: event_id,
        content: "This is an automatic report issued by Akismet"
      },
      Keyword.get(options, :forward_reports, false)
    )
  end

  defp verbose?(options), do: Keyword.get(options, :verbose, false)
  defp dry_run?(options), do: Keyword.get(options, :dry_run, false)
  defp only_profiles?(options), do: Keyword.get(options, :only_profiles, false)
  defp only_events?(options), do: Keyword.get(options, :only_events, false)

  defp anti_spam, do: AntiSpam.service()
end