defmodule Mobilizon.Service.AntiSpam.Akismet do
  @moduledoc """
  Validate user data
  """

  alias Exkismet.Comment, as: AkismetComment
  alias Mobilizon.{Actors, Discussions, Events, Users}
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Discussions.Comment
  alias Mobilizon.Events.Event
  alias Mobilizon.Reports.Report
  alias Mobilizon.Service.AntiSpam.Provider
  alias Mobilizon.Users.User
  alias Mobilizon.Web.Endpoint
  require Logger

  @behaviour Provider

  @env Application.compile_env(:mobilizon, :env)

  @impl Provider
  @spec check_user(String.t(), String.t(), String.t()) ::
          :ham | :spam | :discard | {:error, HTTPoison.Response.t()}
  def check_user(email, ip, user_agent) do
    check_content(%AkismetComment{
      blog: homepage(),
      user_ip: ip,
      comment_author_email: email,
      user_agent: user_agent,
      comment_type: "signup"
    })
  end

  @impl Provider
  @spec check_profile(String.t(), String.t(), String.t() | nil, String.t(), String.t()) ::
          :ham | :spam | :discard | {:error, HTTPoison.Response.t()}
  def check_profile(username, summary, email \\ nil, ip \\ "127.0.0.1", user_agent \\ nil) do
    check_content(%AkismetComment{
      blog: homepage(),
      user_ip: ip,
      comment_author: username,
      comment_author_email: email,
      comment_content: summary,
      user_agent: user_agent,
      comment_type: "signup"
    })
  end

  @impl Provider
  @spec check_event(String.t(), String.t(), String.t() | nil, String.t(), String.t()) ::
          :ham | :spam | :discard | {:error, HTTPoison.Response.t()}
  def check_event(event_body, username, email \\ nil, ip \\ "127.0.0.1", user_agent \\ nil) do
    check_content(%AkismetComment{
      blog: homepage(),
      user_ip: ip,
      comment_author: username,
      comment_author_email: email,
      comment_content: event_body,
      user_agent: user_agent,
      comment_type: "blog-post"
    })
  end

  @impl Provider
  @spec check_comment(String.t(), String.t(), boolean(), String.t() | nil, String.t(), String.t()) ::
          :ham | :spam | :discard | {:error, HTTPoison.Response.t()}
  def check_comment(
        comment_body,
        username,
        is_reply?,
        email \\ nil,
        ip \\ "127.0.0.1",
        user_agent \\ nil
      ) do
    check_content(%AkismetComment{
      blog: homepage(),
      user_ip: ip,
      comment_author: username,
      comment_author_email: email,
      comment_content: comment_body,
      user_agent: user_agent,
      comment_type: if(is_reply?, do: "reply", else: "comment")
    })
  end

  @spec report_ham(Report.t()) :: :ok | {:error, atom()} | {:error, HTTPoison.Response.t()}
  def report_ham(%Report{} = report) do
    report
    |> report_to_akismet_comment()
    |> submit_ham()
  end

  @spec report_spam(Report.t()) :: :ok | {:error, atom()} | {:error, HTTPoison.Response.t()}
  def report_spam(%Report{} = report) do
    report
    |> report_to_akismet_comment()
    |> submit_spam()
  end

  @spec homepage() :: String.t()
  defp homepage do
    Endpoint.url()
  end

  defp check_content(%AkismetComment{} = comment) do
    if @env != :test and ready?() do
      comment
      |> Exkismet.comment_check(key: api_key())
    else
      :ham
    end
  end

  defp api_key do
    Application.get_env(:mobilizon, __MODULE__) |> get_in([:key])
  end

  @impl Provider
  def ready?, do: !is_nil(api_key())

  @spec report_to_akismet_comment(Report.t()) :: AkismetComment.t() | {:error, atom()}
  defp report_to_akismet_comment(%Report{comments: [comment | _]}) do
    with %Comment{text: body, actor: %Actor{} = actor} <-
           Discussions.get_comment_with_preload(comment.id),
         {email, preferred_username, ip} <- actor_details(actor) do
      %AkismetComment{
        blog: homepage(),
        comment_content: body,
        comment_author_email: email,
        comment_author: preferred_username,
        user_ip: ip
      }
    else
      {:error, err} ->
        {:error, err}

      err ->
        {:error, err}
    end
  end

  defp report_to_akismet_comment(%Report{events: [%Event{id: event_id} | _]}) do
    with %Event{description: body, organizer_actor: %Actor{} = actor} <-
           Events.get_event_with_preload!(event_id),
         {email, preferred_username, ip} <- actor_details(actor) do
      %AkismetComment{
        blog: homepage(),
        comment_content: body,
        comment_author_email: email,
        comment_author: preferred_username,
        user_ip: ip
      }
    else
      {:error, err} ->
        {:error, err}

      err ->
        {:error, err}
    end
  end

  defp report_to_akismet_comment(%Report{reported_id: reported_id}) do
    case reported_id |> Actors.get_actor_with_preload!() |> actor_details() do
      {email, preferred_username, ip} ->
        %AkismetComment{
          blog: homepage(),
          comment_author_email: email,
          comment_author: preferred_username,
          user_ip: ip
        }

      {:error, err} ->
        {:error, err}

      err ->
        {:error, err}
    end
  end

  @spec actor_details(Actor.t()) :: {String.t(), String.t(), any()} | {:error, :invalid_actor}
  defp actor_details(%Actor{
         type: :Person,
         preferred_username: preferred_username,
         user: %User{
           current_sign_in_ip: current_sign_in_ip,
           last_sign_in_ip: last_sign_in_ip,
           email: email
         }
       }) do
    {email, preferred_username, current_sign_in_ip || last_sign_in_ip}
  end

  defp actor_details(%Actor{
         type: :Person,
         preferred_username: preferred_username,
         user_id: user_id
       })
       when not is_nil(user_id) do
    case user_id |> Users.get_user() |> user_details() do
      {email, ip} ->
        {preferred_username, email, ip}

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

  defp actor_details(%Actor{
         type: :Person,
         preferred_username: preferred_username,
         user_id: nil
       }) do
    {nil, preferred_username, "127.0.0.1"}
  end

  defp actor_details(_) do
    {:error, :invalid_actor}
  end

  @spec user_details(User.t()) :: {String.t(), any()} | {:error, :user_not_found}
  defp user_details(%User{
         current_sign_in_ip: current_sign_in_ip,
         last_sign_in_ip: last_sign_in_ip,
         email: email
       }) do
    {email, current_sign_in_ip || last_sign_in_ip}
  end

  defp user_details(_), do: {:error, :user_not_found}

  @spec submit_spam(AkismetComment.t() | :error) ::
          :ok | {:error, atom()} | {:error, HTTPoison.Response.t()}
  defp submit_spam(%AkismetComment{} = comment) do
    comment
    |> tap(fn comment ->
      Logger.info("Submitting content to Akismet as spam: #{inspect(comment)}")
    end)
    |> Exkismet.submit_spam(key: api_key())
    |> log_response()
  end

  defp submit_spam({:error, err}), do: {:error, err}

  @spec submit_ham(AkismetComment.t() | :error) ::
          :ok | {:error, atom()} | {:error, HTTPoison.Response.t()}
  defp submit_ham(%AkismetComment{} = comment) do
    comment
    |> tap(fn comment ->
      Logger.info("Submitting content to Akismet as ham: #{inspect(comment)}")
    end)
    |> Exkismet.submit_ham(key: api_key())
    |> log_response()
  end

  defp submit_ham({:error, err}), do: {:error, err}

  defp log_response(res),
    do: tap(res, fn res -> Logger.debug("Return from Akismet is: #{inspect(res)}") end)
end