defmodule WhoNeedHelp.ContentRemoval do
  @moduledoc "Separate intake and review queue for electronic content-removal notices."

  import Ecto.Query

  alias WhoNeedHelp.Accounts
  alias WhoNeedHelp.Accounts.{Scope, User}
  alias WhoNeedHelp.ContentRemoval.{Notice, Notifier}
  alias WhoNeedHelp.Pagination
  alias WhoNeedHelp.Repo
  alias WhoNeedHelp.Trust
  alias WhoNeedHelp.Trust.RateLimiter

  @access_salt "content-removal-access"
  @urgent_categories [
    :non_consensual_intimate_media,
    :child_sexual_abuse_material,
    :threat_to_life_or_safety
  ]

  def change_notice(%Notice{} = notice, attrs \\ %{}) do
    Notice.submission_changeset(notice, attrs)
  end

  def create_notice(scope, regime, attrs) when regime in [:general, :dsa, :take_it_down] do
    user = scope_user(scope)
    attrs = normalize_keys(attrs)
    attrs = maybe_use_user_email(attrs, user)
    category = enum_value(Notice, :category, attrs["category"])
    status = if category in @urgent_categories, do: :urgent_review, else: :open

    response_due_at =
      if regime == :take_it_down, do: DateTime.add(DateTime.utc_now(:second), 48, :hour)

    with {:ok, _limit} <-
           RateLimiter.check(:content_removal_notice, rate_scope(user, attrs["contact_email"])) do
      Repo.transact(fn ->
        with {:ok, notice} <-
               %Notice{
                 reference: unique_reference(),
                 regime: regime,
                 status: status,
                 response_due_at: response_due_at,
                 requester_id: user && user.id,
                 contact_verified_at: user && DateTime.utc_now(:second)
               }
               |> Notice.submission_changeset(attrs)
               |> Repo.insert(),
             {:ok, _audit} <-
               Trust.audit(
                 user && user.id,
                 "content_removal_notice.created",
                 "content_removal_notice",
                 notice.id,
                 %{
                   "category" => to_string(notice.category),
                   "regime" => to_string(notice.regime),
                   "status" => to_string(notice.status)
                 }
               ) do
          {:ok, notice}
        end
      end)
      |> notify_received()
    end
  end

  def list_for_user(%Scope{user: %User{id: user_id}}) do
    Notice
    |> where([notice], notice.requester_id == ^user_id)
    |> order_by([notice], desc: notice.inserted_at, desc: notice.id)
    |> Repo.all()
  end

  def get_by_access_token(id, token) when is_binary(token) do
    with {:ok, id} <- Ecto.UUID.cast(id),
         {:ok, ^id} <-
           Phoenix.Token.verify(WhoNeedHelpWeb.Endpoint, @access_salt, token, max_age: 31_536_000),
         %Notice{} = notice <- Repo.get(Notice, id) do
      verify_contact(notice)
    else
      _ -> {:error, :not_found}
    end
  end

  def get_by_access_token(_id, _token), do: {:error, :not_found}

  def access_token(%Notice{id: id}) do
    Phoenix.Token.sign(WhoNeedHelpWeb.Endpoint, @access_salt, id)
  end

  def status_url(%Notice{} = notice) do
    token = access_token(notice)

    WhoNeedHelpWeb.Endpoint.url() <>
      "/legal/content-removal/#{notice.id}?token=#{URI.encode_www_form(token)}"
  end

  def paginate_for_staff(%Scope{user: user}, options \\ []) do
    if Accounts.moderator_authorized?(user) do
      limit = Pagination.limit(options)
      cursor = Pagination.cursor(options)

      Notice
      |> maybe_regime(Keyword.get(options, :regime))
      |> before(cursor)
      |> order_by([notice], desc: notice.inserted_at, desc: notice.id)
      |> limit(^(limit + 1))
      |> preload([:requester, :reviewed_by])
      |> Repo.all()
      |> Pagination.page(limit, &{&1.inserted_at, &1.id})
    else
      %Pagination.Page{}
    end
  end

  def moderate(%Scope{user: moderator}, id, attrs) do
    with {:ok, id} <- Ecto.UUID.cast(id),
         true <- Accounts.moderator_authorized?(moderator) do
      attrs =
        attrs
        |> normalize_keys()
        |> Map.merge(%{
          "reviewed_at" => DateTime.utc_now(:second),
          "reviewed_by_id" => moderator.id
        })

      Repo.transact(fn ->
        notice =
          Notice
          |> where([notice], notice.id == ^id)
          |> lock("FOR UPDATE")
          |> Repo.one()

        if notice do
          with {:ok, notice} <- notice |> Notice.moderation_changeset(attrs) |> Repo.update(),
               {:ok, _audit} <-
                 Trust.audit(
                   moderator.id,
                   "content_removal_notice.moderated",
                   "content_removal_notice",
                   notice.id,
                   %{
                     "status" => to_string(notice.status)
                   }
                 ) do
            {:ok, notice}
          end
        else
          {:error, :not_found}
        end
      end)
      |> notify_decision()
    else
      false -> {:error, :forbidden}
      _ -> {:error, :not_found}
    end
  end

  defp notify_received({:ok, %Notice{contact_email: email} = notice}) when email in [nil, ""],
    do: notify_operator(notice)

  defp notify_received({:ok, notice}) do
    result =
      case Notifier.deliver_received(notice, status_url(notice)) do
        {:ok, _metadata} ->
          notice
          |> Ecto.Changeset.change(acknowledgement_sent_at: DateTime.utc_now(:second))
          |> Repo.update()

        _error ->
          {:ok, notice}
      end

    case result do
      {:ok, current} -> notify_operator(current)
      error -> error
    end
  end

  defp notify_received(result), do: result

  defp notify_operator(notice) do
    _ = Notifier.deliver_operator_alert(notice)
    {:ok, notice}
  end

  defp notify_decision({:ok, %Notice{contact_verified_at: nil} = notice}), do: {:ok, notice}

  defp notify_decision({:ok, notice}) do
    case Notifier.deliver_decision(notice, status_url(notice)) do
      {:ok, _metadata} ->
        notice
        |> Ecto.Changeset.change(decision_sent_at: DateTime.utc_now(:second))
        |> Repo.update()

      _error ->
        {:ok, notice}
    end
  end

  defp notify_decision(result), do: result

  defp verify_contact(%Notice{contact_verified_at: nil} = notice) do
    notice
    |> Ecto.Changeset.change(contact_verified_at: DateTime.utc_now(:second))
    |> Repo.update()
  end

  defp verify_contact(%Notice{} = notice), do: {:ok, notice}

  defp before(query, nil), do: query

  defp before(query, {inserted_at, id}) do
    where(
      query,
      [notice],
      notice.inserted_at < ^inserted_at or
        (notice.inserted_at == ^inserted_at and notice.id < ^id)
    )
  end

  defp maybe_regime(query, nil), do: query
  defp maybe_regime(query, regime), do: where(query, [notice], notice.regime == ^regime)

  defp maybe_use_user_email(attrs, %User{email: email}) do
    Map.put(attrs, "contact_email", email)
  end

  defp maybe_use_user_email(attrs, nil), do: attrs

  defp rate_scope(%User{id: id}, _email), do: "user:#{id}"

  defp rate_scope(nil, email) when is_binary(email),
    do: "email:#{String.downcase(String.trim(email))}"

  defp rate_scope(nil, _email), do: "missing-email"

  defp scope_user(%Scope{user: %User{} = user}), do: user
  defp scope_user(_scope), do: nil

  defp enum_value(module, field, value) do
    Ecto.Enum.cast_value(module, field, value)
    |> case do
      {:ok, enum} -> enum
      :error -> nil
    end
  end

  defp unique_reference do
    suffix = 8 |> :crypto.strong_rand_bytes() |> Base.encode32(case: :upper, padding: false)
    "REM-#{suffix}"
  end

  defp normalize_keys(attrs), do: Map.new(attrs, fn {key, value} -> {to_string(key), value} end)
end
