defmodule WhoNeedHelp.Support do
  @moduledoc "Public and authenticated support, privacy, and account-lifecycle requests."

  import Ecto.Query

  alias WhoNeedHelp.Accounts
  alias WhoNeedHelp.Accounts.{Scope, User}
  alias WhoNeedHelp.Pagination
  alias WhoNeedHelp.Repo
  alias WhoNeedHelp.Support.{Notifier, SupportRequest}
  alias WhoNeedHelp.Trust
  alias WhoNeedHelp.Trust.RateLimiter

  @access_salt "support-request-access"

  def change_request(%SupportRequest{} = request, attrs \\ %{}) do
    SupportRequest.submission_changeset(request, attrs)
  end

  def create_request(scope, attrs) when is_map(attrs) do
    user = scope_user(scope)
    attrs = normalize_keys(attrs)
    contact_email = if user, do: user.email, else: attrs["contact_email"]
    attrs = Map.put(attrs, "contact_email", contact_email)

    with {:ok, _limit} <- RateLimiter.check(:support_request, rate_scope(user, contact_email)) do
      Repo.transact(fn ->
        with {:ok, request} <-
               %SupportRequest{
                 reference: unique_reference("SUP"),
                 requester_id: user && user.id,
                 contact_verified_at: user && DateTime.utc_now(:second)
               }
               |> SupportRequest.submission_changeset(attrs)
               |> Repo.insert(),
             {:ok, _audit} <-
               Trust.audit(
                 user && user.id,
                 "support_request.created",
                 "support_request",
                 request.id,
                 %{
                   "kind" => to_string(request.kind)
                 }
               ) do
          {:ok, request}
        end
      end)
      |> notify_received()
    end
  end

  def create_account_deletion_request(%Scope{user: %User{}} = scope, details \\ nil) do
    create_request(scope, %{
      "kind" => "account_deletion",
      "subject" => "Delete my Who Need Help account",
      "details" => details || "Please delete my account and associated personal data."
    })
  end

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

  def get_for_user(%Scope{user: %User{id: user_id}}, id) do
    with {:ok, id} <- Ecto.UUID.cast(id),
         %SupportRequest{} = request <-
           Repo.get_by(SupportRequest, id: id, requester_id: user_id) do
      {:ok, request}
    else
      _ -> {:error, :not_found}
    end
  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),
         %SupportRequest{} = request <- Repo.get(SupportRequest, id) do
      verify_contact(request)
    else
      _ -> {:error, :not_found}
    end
  end

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

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

  def status_url(%SupportRequest{} = request) do
    token = access_token(request)

    WhoNeedHelpWeb.Endpoint.url() <>
      "/support/cases/#{request.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)

      SupportRequest
      |> maybe_kind(Keyword.get(options, :kind))
      |> before(cursor)
      |> order_by([request], desc: request.inserted_at, desc: request.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 ->
        request =
          SupportRequest
          |> where([request], request.id == ^id)
          |> lock("FOR UPDATE")
          |> Repo.one()

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

  defp notify_received({:ok, request}) do
    _ = Notifier.deliver_received(request, status_url(request))
    _ = Notifier.deliver_operator_alert(request)
    {:ok, request}
  end

  defp notify_received(result), do: result

  defp notify_decision({:ok, %SupportRequest{contact_verified_at: nil} = request}),
    do: {:ok, request}

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

      _error ->
        {:ok, request}
    end
  end

  defp notify_decision(result), do: result

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

  defp verify_contact(%SupportRequest{} = request), do: {:ok, request}

  defp before(query, nil), do: query

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

  defp maybe_kind(query, nil), do: query
  defp maybe_kind(query, kind), do: where(query, [request], request.kind == ^kind)

  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 unique_reference(prefix) do
    suffix = 8 |> :crypto.strong_rand_bytes() |> Base.encode32(case: :upper, padding: false)
    "#{prefix}-#{suffix}"
  end

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