defmodule WhoNeedHelp.Trust do
  @moduledoc "Reviews, reports, blocks, reputation, abuse signals, and auditable moderation."

  import Ecto.Query

  alias WhoNeedHelp.Accounts
  alias WhoNeedHelp.Accounts.{Scope, User}
  alias WhoNeedHelp.Activities
  alias WhoNeedHelp.Activities.{Activity, Participant}
  alias WhoNeedHelp.Activities.Message, as: ActivityMessage
  alias WhoNeedHelp.Help
  alias WhoNeedHelp.Help.{Assignment, HelpRequest}
  alias WhoNeedHelp.Messaging.Message
  alias WhoNeedHelp.Pagination
  alias WhoNeedHelp.Repo
  alias WhoNeedHelp.Tracking.{Position, TrackingSession}

  alias WhoNeedHelp.Trust.{
    AbuseSignal,
    AuditEvent,
    Block,
    RateLimiter,
    Report,
    Review
  }

  def authorize_action(%Scope{user: %User{} = user}, action) do
    if Accounts.eligible_user_id?(user.id) do
      case RateLimiter.check(action, user.id) do
        {:error, :rate_limited} = error ->
          create_velocity_signal_once(user.id, action)
          error

        result ->
          result
      end
    else
      {:error, :account_not_eligible}
    end
  end

  def authorize_action(_, _action), do: {:error, :account_not_eligible}

  def eligible?(%Scope{user: %User{id: id}}), do: Accounts.eligible_user_id?(id)
  def eligible?(_scope), do: false

  def submit_review(%Scope{user: user} = scope, %Assignment{} = assignment, attrs) do
    attrs = stringify_keys(attrs)

    with {:ok, _limit} <- authorize_action(scope, :review) do
      result =
        Repo.transact(fn ->
          current =
            Assignment
            |> where([current], current.id == ^assignment.id)
            |> lock("FOR UPDATE")
            |> Repo.one()

          if current do
            request = Repo.get!(HelpRequest, current.request_id)

            cond do
              current.status != :completed ->
                {:error, :forbidden}

              user.id not in [current.helper_id, request.requester_id] ->
                {:error, :forbidden}

              true ->
                reviewee_id =
                  if user.id == current.helper_id,
                    do: request.requester_id,
                    else: current.helper_id

                with {:ok, review} <-
                       %Review{}
                       |> Review.changeset(
                         Map.merge(attrs, %{
                           "assignment_id" => current.id,
                           "reviewer_id" => user.id,
                           "reviewee_id" => reviewee_id
                         })
                       )
                       |> Repo.insert(),
                     reviews <- Repo.all(from r in Review, where: r.assignment_id == ^current.id),
                     true <- length(reviews) <= 2,
                     revealed? <- length(reviews) == 2,
                     {_count, _rows} <-
                       maybe_reveal_reviews(current.id, revealed?),
                     {:ok, _audit} <-
                       audit(user.id, "review.submitted", "assignment", current.id, %{
                         "revealed" => revealed?
                       }) do
                  {:ok, review}
                else
                  false -> {:error, :invalid_review_count}
                  other -> other
                end
            end
          else
            {:error, :not_found}
          end
        end)

      with {:ok, review} <- result do
        :ok = Help.notify_request_updated(assignment.request_id)
        {:ok, review}
      end
    end
  end

  def review_submitted?(%Scope{user: %User{id: user_id}}, %Assignment{id: assignment_id}) do
    Repo.exists?(
      from review in Review,
        where: review.assignment_id == ^assignment_id and review.reviewer_id == ^user_id
    )
  end

  def review_submitted?(_scope, _assignment), do: false

  defp maybe_reveal_reviews(assignment_id, true) do
    Repo.update_all(from(r in Review, where: r.assignment_id == ^assignment_id),
      set: [revealed_at: DateTime.utc_now(:second)]
    )
  end

  defp maybe_reveal_reviews(_assignment_id, false), do: {0, nil}

  def visible_reviews(user_id) do
    paginate_visible_reviews(user_id).entries
  end

  def paginate_visible_reviews(user_id, options \\ []) do
    limit = Pagination.limit(options)
    cursor = Pagination.cursor(options)
    public_user = Accounts.public_user_query()

    Review
    |> where([review], review.reviewee_id == ^user_id and not is_nil(review.revealed_at))
    |> before_review(cursor)
    |> order_by([review], desc: review.inserted_at, desc: review.id)
    |> limit(^(limit + 1))
    |> preload([review], reviewer: ^public_user)
    |> Repo.all()
    |> Pagination.page(limit, &{&1.inserted_at, &1.id})
  end

  @empty_reputation %{
    completed: 0,
    unique_people: 0,
    verified_handovers: 0,
    location_supported: 0,
    rating: nil
  }

  def reputation(user_id), do: Map.fetch!(reputations([user_id]), user_id)

  def reputations([]), do: %{}

  def reputations(user_ids) when is_list(user_ids) do
    user_ids = Enum.uniq(user_ids)

    helper_rows =
      Assignment
      |> join(:inner, [assignment], request in HelpRequest,
        on: request.id == assignment.request_id
      )
      |> where(
        [assignment],
        assignment.status == :completed and assignment.helper_id in ^user_ids
      )
      |> select([assignment, request], %{
        id: assignment.id,
        user_id: assignment.helper_id,
        counterpart_id: request.requester_id,
        verified: not is_nil(assignment.handover_verified_at),
        location_supported:
          not is_nil(assignment.proximity_observed_at) and
            not is_nil(assignment.helper_movement_observed_at)
      })

    requester_rows =
      Assignment
      |> join(:inner, [assignment], request in HelpRequest,
        on: request.id == assignment.request_id
      )
      |> where(
        [assignment, request],
        assignment.status == :completed and request.requester_id in ^user_ids and
          assignment.helper_id != request.requester_id
      )
      |> select([assignment, request], %{
        id: assignment.id,
        user_id: request.requester_id,
        counterpart_id: assignment.helper_id,
        verified: not is_nil(assignment.handover_verified_at),
        location_supported: not is_nil(assignment.proximity_observed_at)
      })

    aggregates =
      helper_rows
      |> union_all(^requester_rows)
      |> subquery()
      |> group_by([row], row.user_id)
      |> select([row], %{
        user_id: row.user_id,
        completed: count(row.id),
        unique_people: count(row.counterpart_id, :distinct),
        verified_handovers: filter(count(row.id), row.verified),
        location_supported: filter(count(row.id), row.location_supported)
      })

    ratings =
      Review
      |> where(
        [review],
        review.reviewee_id in ^user_ids and not is_nil(review.revealed_at)
      )
      |> group_by([review], review.reviewee_id)
      |> select([review], %{user_id: review.reviewee_id, rating: avg(review.rating)})

    rows =
      aggregates
      |> subquery()
      |> join(:left, [aggregate], rating in subquery(ratings),
        on: rating.user_id == aggregate.user_id
      )
      |> select([aggregate, rating], %{
        user_id: aggregate.user_id,
        completed: aggregate.completed,
        unique_people: aggregate.unique_people,
        verified_handovers: aggregate.verified_handovers,
        location_supported: aggregate.location_supported,
        rating: rating.rating
      })
      |> Repo.all()

    Enum.reduce(rows, Map.new(user_ids, &{&1, @empty_reputation}), fn row, reputations ->
      Map.put(
        reputations,
        row.user_id,
        row
        |> Map.delete(:user_id)
        |> Map.update!(:rating, &decimal_average/1)
      )
    end)
  end

  def leaderboard do
    paginate_leaderboard().entries
  end

  def paginate_leaderboard(options \\ []) do
    limit = Pagination.limit(options)
    cursor = decode_leaderboard_cursor(Keyword.get(options, :after))
    rows = leaderboard_rows(cursor, limit)

    page_rows = Enum.take(rows, limit)
    users_by_id = users_by_id(Enum.map(page_rows, & &1.helper_id))

    entries =
      Enum.map(page_rows, fn row ->
        %{
          user: Map.fetch!(users_by_id, row.helper_id),
          completed: row.completed,
          unique_people: row.unique_people,
          location_supported_people: row.location_supported_people,
          verified_people: row.verified_people,
          rating: decimal_average(row.rating)
        }
      end)

    next_cursor =
      if length(rows) > limit do
        page_rows
        |> List.last()
        |> encode_leaderboard_cursor()
      end

    %Pagination.Page{entries: entries, next_cursor: next_cursor}
  end

  def report(%Scope{user: user} = scope, attrs) do
    attrs = stringify_keys(attrs)

    with {:ok, _limit} <- authorize_action(scope, :report),
         :ok <- authorize_report_target(scope, attrs) do
      Repo.transact(fn ->
        with {:ok, report} <-
               %Report{reporter_id: user.id}
               |> Report.changeset(attrs)
               |> Repo.insert(),
             {:ok, _audit} <-
               audit(user.id, "report.created", "report", report.id, %{
                 "reason" => to_string(report.reason)
               }) do
          {:ok, report}
        end
      end)
    end
  end

  def list_reports(%Scope{user: user}, status \\ nil) do
    paginate_reports(%Scope{user: user}, status).entries
  end

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

      Report
      |> maybe_status(status)
      |> before_report(cursor)
      |> order_by([report], desc: report.inserted_at, desc: report.id)
      |> limit(^(limit + 1))
      |> preload([
        :reporter,
        :reviewed_by,
        request: :requester,
        assignment: :helper,
        message: :sender,
        activity: :creator,
        activity_message: :sender
      ])
      |> Repo.all()
      |> Pagination.page(limit, &{&1.inserted_at, &1.id})
    else
      %Pagination.Page{}
    end
  end

  def moderate_report(%Scope{user: moderator}, report_id, attrs) do
    with {:ok, report_id} <- cast_id(report_id),
         true <- Accounts.moderator_authorized?(moderator) do
      attrs =
        attrs
        |> stringify_keys()
        |> Map.merge(%{
          "reviewed_at" => DateTime.utc_now(:second),
          "reviewed_by_id" => moderator.id
        })

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

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

  def report_evidence(%Scope{user: moderator}, report_id) do
    with {:ok, report_id} <- cast_id(report_id),
         true <- Accounts.moderator_authorized?(moderator) do
      report =
        Report
        |> Repo.get(report_id)
        |> Repo.preload([
          :reporter,
          :request,
          :assignment,
          :activity,
          message: :assignment,
          activity_message: :activity
        ])

      if report do
        assignment_id =
          cond do
            report.assignment_id -> report.assignment_id
            report.message -> report.message.assignment_id
            true -> nil
          end

        messages =
          if assignment_id do
            Message
            |> where([message], message.assignment_id == ^assignment_id)
            |> order_by([message], desc: message.inserted_at, desc: message.id)
            |> limit(200)
            |> preload(:sender)
            |> Repo.all()
            |> Enum.reverse()
          else
            []
          end

        activity_id =
          case report.activity_message do
            %ActivityMessage{activity_id: activity_id} -> activity_id
            _ -> nil
          end

        activity_messages =
          if activity_id do
            ActivityMessage
            |> where([message], message.activity_id == ^activity_id)
            |> order_by([message], desc: message.inserted_at, desc: message.id)
            |> limit(200)
            |> preload(:sender)
            |> Repo.all()
            |> Enum.reverse()
          else
            []
          end

        with {:ok, _audit} <-
               audit(moderator.id, "report.evidence_viewed", "report", report.id, %{
                 "assignment_id" => assignment_id,
                 "activity_id" => activity_id,
                 "message_count" => length(messages),
                 "activity_message_count" => length(activity_messages)
               }) do
          {:ok, %{report: report, messages: messages, activity_messages: activity_messages}}
        end
      else
        {:error, :not_found}
      end
    else
      false -> {:error, :forbidden}
      {:error, :not_found} = error -> error
    end
  end

  def block(%Scope{user: user} = scope, blocked_id) do
    with {:ok, blocked_id} <- cast_id(blocked_id),
         {:ok, _limit} <- authorize_action(scope, :block),
         %User{} <- Repo.get(User, blocked_id) do
      result =
        Repo.transact(fn ->
          with :ok <- lock_user_pair(user.id, blocked_id),
               {:ok, block} <-
                 %Block{}
                 |> Block.changeset(%{blocker_id: user.id, blocked_id: blocked_id})
                 |> Repo.insert(),
               {:ok, _audit} <- audit(user.id, "user.blocked", "user", blocked_id) do
            tracking = clear_pair_tracking(user.id, blocked_id)
            activity_ids = pair_activity_ids(user.id, blocked_id)
            {:ok, %{block: block, tracking: tracking, activity_ids: activity_ids}}
          end
        end)

      with {:ok, %{block: block, tracking: tracking, activity_ids: activity_ids}} <- result do
        Enum.each(tracking.stopped, fn {assignment_id, stopped_user_id} ->
          Phoenix.PubSub.broadcast(
            WhoNeedHelp.PubSub,
            "tracking:#{assignment_id}",
            {:tracking_stopped, stopped_user_id}
          )
        end)

        Enum.each(tracking.request_ids, &Help.notify_request_updated/1)
        Enum.each(activity_ids, &Activities.notify_activity_updated/1)
        {:ok, block}
      end
    else
      nil -> {:error, :not_found}
      other -> other
    end
  end

  defp pair_activity_ids(first_user_id, second_user_id) do
    Activity
    |> join(:inner, [activity], participant in Participant,
      on: participant.activity_id == activity.id
    )
    |> where(
      [activity, participant],
      participant.status == :approved and
        ((activity.creator_id == ^first_user_id and participant.user_id == ^second_user_id) or
           (activity.creator_id == ^second_user_id and participant.user_id == ^first_user_id))
    )
    |> select([activity], activity.id)
    |> distinct(true)
    |> Repo.all()
  end

  def unblock(%Scope{user: user}, blocked_id) do
    with {:ok, blocked_id} <- cast_id(blocked_id) do
      Repo.transact(fn ->
        :ok = lock_user_pair(user.id, blocked_id)

        case Repo.get_by(Block, blocker_id: user.id, blocked_id: blocked_id) do
          nil ->
            {:ok, :already_unblocked}

          block ->
            with {:ok, _block} <- Repo.delete(block),
                 {:ok, _audit} <- audit(user.id, "user.unblocked", "user", blocked_id) do
              {:ok, :unblocked}
            end
        end
      end)
    end
  end

  def list_blocks(%Scope{user: user}) do
    paginate_blocks(%Scope{user: user}).entries
  end

  def paginate_blocks(%Scope{user: user}, options \\ []) do
    limit = Pagination.limit(options)
    cursor = Pagination.cursor(options)
    public_user = Accounts.public_user_query()

    Block
    |> where([block], block.blocker_id == ^user.id)
    |> before_block(cursor)
    |> order_by([block], desc: block.inserted_at, desc: block.id)
    |> limit(^(limit + 1))
    |> preload([block], blocked: ^public_user)
    |> Repo.all()
    |> Pagination.page(limit, &{&1.inserted_at, &1.id})
  end

  def blocked_between?(first_user_id, second_user_id) do
    Block
    |> where(
      [block],
      (block.blocker_id == ^first_user_id and block.blocked_id == ^second_user_id) or
        (block.blocker_id == ^second_user_id and block.blocked_id == ^first_user_id)
    )
    |> Repo.exists?()
  end

  def blocked_by?(blocker_id, blocked_id) do
    Repo.exists?(
      from block in Block,
        where: block.blocker_id == ^blocker_id and block.blocked_id == ^blocked_id
    )
  end

  @doc false
  def lock_user_pair(first_user_id, second_user_id) do
    User
    |> where([user], user.id in ^Enum.sort([first_user_id, second_user_id]))
    |> order_by([user], asc: user.id)
    |> lock("FOR UPDATE")
    |> Repo.all()

    :ok
  end

  def record_completion_signals(%Assignment{} = assignment) do
    request =
      case assignment.request do
        %HelpRequest{} = request -> request
        _ -> Repo.get!(HelpRequest, assignment.request_id)
      end

    repeated_pair? =
      Assignment
      |> join(:inner, [candidate], req in HelpRequest, on: req.id == candidate.request_id)
      |> where(
        [candidate, req],
        candidate.status == :completed and candidate.id != ^assignment.id and
          candidate.helper_id == ^assignment.helper_id and
          req.requester_id == ^request.requester_id
      )
      |> Repo.exists?()

    if repeated_pair? do
      create_signal_once(:repeated_pair, assignment.helper_id, assignment.id, %{
        "counterpart_id" => request.requester_id
      })
    end

    reciprocal? =
      Assignment
      |> join(:inner, [candidate], req in HelpRequest, on: req.id == candidate.request_id)
      |> where(
        [candidate, req],
        candidate.status == :completed and candidate.helper_id == ^request.requester_id and
          req.requester_id == ^assignment.helper_id
      )
      |> Repo.exists?()

    if reciprocal? do
      create_signal_once(:reciprocal_ring, assignment.helper_id, assignment.id, %{
        "counterpart_id" => request.requester_id,
        "factual_signal_only" => true
      })
    end

    if is_nil(assignment.proximity_observed_at) do
      create_signal_once(
        :handover_without_location_evidence,
        assignment.helper_id,
        assignment.id,
        %{"tracking_optional" => true}
      )
    end

    if is_nil(assignment.helper_movement_observed_at) do
      create_signal_once(:location_without_movement, assignment.helper_id, assignment.id, %{
        "tracking_optional" => true
      })
    end

    :ok
  end

  def list_abuse_signals(%Scope{user: user}, status \\ :open) do
    paginate_abuse_signals(%Scope{user: user}, status).entries
  end

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

      AbuseSignal
      |> where([signal], signal.status == ^status)
      |> before_abuse_signal(cursor)
      |> order_by([signal], desc: signal.inserted_at, desc: signal.id)
      |> limit(^(limit + 1))
      |> preload([:subject, :assignment, :reviewed_by])
      |> Repo.all()
      |> Pagination.page(limit, &{&1.inserted_at, &1.id})
    else
      %Pagination.Page{}
    end
  end

  def moderate_signal(%Scope{user: moderator}, signal_id, attrs) do
    with {:ok, signal_id} <- cast_id(signal_id),
         true <- Accounts.moderator_authorized?(moderator) do
      attrs =
        attrs
        |> stringify_keys()
        |> Map.merge(%{
          "reviewed_at" => DateTime.utc_now(:second),
          "reviewed_by_id" => moderator.id
        })

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

        if signal do
          with {:ok, signal} <- signal |> AbuseSignal.moderation_changeset(attrs) |> Repo.update(),
               {:ok, _audit} <-
                 audit(moderator.id, "abuse_signal.moderated", "abuse_signal", signal.id, %{
                   "status" => to_string(signal.status)
                 }) do
            {:ok, signal}
          end
        else
          {:error, :not_found}
        end
      end)
    else
      false -> {:error, :forbidden}
      {:error, :not_found} = error -> error
    end
  end

  def hide_request(%Scope{user: moderator}, request_id, reason) do
    result =
      with {:ok, request_id} <- cast_id(request_id),
           true <- Accounts.moderator_authorized?(moderator) do
        Repo.transact(fn ->
          request =
            HelpRequest
            |> where([request], request.id == ^request_id)
            |> lock("FOR UPDATE")
            |> Repo.one()

          if request do
            with {:ok, request} <-
                   request
                   |> HelpRequest.moderation_changeset(%{
                     hidden_at: DateTime.utc_now(:second),
                     hidden_reason: reason
                   })
                   |> Repo.update(),
                 {:ok, _audit} <-
                   audit(moderator.id, "request.hidden", "request", request.id, %{
                     "reason" => reason
                   }) do
              {:ok, request}
            end
          else
            {:error, :not_found}
          end
        end)
      else
        false -> {:error, :forbidden}
        {:error, :not_found} = error -> error
      end

    with {:ok, request} <- result do
      Help.notify_request_updated(request.id)
      {:ok, request}
    end
  end

  def restore_request(%Scope{user: moderator}, request_id) do
    result =
      with {:ok, request_id} <- cast_id(request_id),
           true <- Accounts.moderator_authorized?(moderator) do
        Repo.transact(fn ->
          request =
            HelpRequest
            |> where([request], request.id == ^request_id)
            |> lock("FOR UPDATE")
            |> Repo.one()

          if request do
            with {:ok, request} <-
                   request
                   |> HelpRequest.moderation_changeset(%{hidden_at: nil, hidden_reason: nil})
                   |> Repo.update(),
                 {:ok, _audit} <-
                   audit(moderator.id, "request.restored", "request", request.id) do
              {:ok, request}
            end
          else
            {:error, :not_found}
          end
        end)
      else
        false -> {:error, :forbidden}
        {:error, :not_found} = error -> error
      end

    with {:ok, request} <- result do
      Help.notify_request_updated(request.id)
      {:ok, request}
    end
  end

  def hide_activity(%Scope{user: moderator}, activity_id, reason) do
    result =
      with {:ok, activity_id} <- cast_id(activity_id),
           true <- Accounts.moderator_authorized?(moderator) do
        Repo.transact(fn ->
          activity =
            Activity
            |> where([activity], activity.id == ^activity_id)
            |> lock("FOR UPDATE")
            |> Repo.one()

          if activity do
            with {:ok, activity} <-
                   activity
                   |> Activity.moderation_changeset(%{
                     hidden_at: DateTime.utc_now(:second),
                     hidden_reason: reason
                   })
                   |> Repo.update(),
                 {:ok, _audit} <-
                   audit(moderator.id, "activity.hidden", "activity", activity.id, %{
                     "reason" => reason
                   }) do
              {:ok, activity}
            end
          else
            {:error, :not_found}
          end
        end)
      else
        false -> {:error, :forbidden}
        {:error, :not_found} = error -> error
      end

    with {:ok, activity} <- result do
      Activities.notify_activity_updated(activity.id)
      {:ok, activity}
    end
  end

  def restore_activity(%Scope{user: moderator}, activity_id) do
    result =
      with {:ok, activity_id} <- cast_id(activity_id),
           true <- Accounts.moderator_authorized?(moderator) do
        Repo.transact(fn ->
          activity =
            Activity
            |> where([activity], activity.id == ^activity_id)
            |> lock("FOR UPDATE")
            |> Repo.one()

          if activity do
            with {:ok, activity} <-
                   activity
                   |> Activity.moderation_changeset(%{hidden_at: nil, hidden_reason: nil})
                   |> Repo.update(),
                 {:ok, _audit} <-
                   audit(moderator.id, "activity.restored", "activity", activity.id) do
              {:ok, activity}
            end
          else
            {:error, :not_found}
          end
        end)
      else
        false -> {:error, :forbidden}
        {:error, :not_found} = error -> error
      end

    with {:ok, activity} <- result do
      Activities.notify_activity_updated(activity.id)
      {:ok, activity}
    end
  end

  def moderate_user(%Scope{user: moderator}, user_id, attrs) do
    result =
      Repo.transact(fn ->
        with {:ok, %{user: user} = moderation} <-
               Accounts.moderate_user(moderator, user_id, attrs),
             {:ok, _audit} <-
               audit(moderator.id, "user.moderated", "user", user.id, %{
                 "moderation_status" => to_string(user.moderation_status)
               }) do
          tracking =
            if user.moderation_status == :suspended,
              do: clear_user_tracking(user.id),
              else: %{stopped: [], request_ids: []}

          {:ok, Map.put(moderation, :tracking, tracking)}
        end
      end)

    with {:ok, %{user: user, expired_session_tokens: tokens, tracking: tracking}} <-
           result do
      Enum.each(tokens, fn token ->
        token
        |> WhoNeedHelpWeb.UserAuth.live_socket_id()
        |> WhoNeedHelpWeb.Endpoint.broadcast("disconnect", %{})
      end)

      Enum.each(tracking.stopped, fn {assignment_id, stopped_user_id} ->
        Phoenix.PubSub.broadcast(
          WhoNeedHelp.PubSub,
          "tracking:#{assignment_id}",
          {:tracking_stopped, stopped_user_id}
        )
      end)

      Enum.each(tracking.request_ids, &Help.notify_request_updated/1)

      {:ok, user}
    end
  end

  def moderate_role(%Scope{user: admin}, user_id, attrs) do
    Repo.transact(fn ->
      with {:ok, user} <- Accounts.change_user_role(admin, user_id, attrs),
           {:ok, _audit} <-
             audit(admin.id, "user.role_changed", "user", user.id, %{
               "role" => to_string(user.role)
             }) do
        {:ok, user}
      end
    end)
  end

  def audit(actor_id, action, target_type, target_id, metadata \\ %{}) do
    %AuditEvent{}
    |> AuditEvent.changeset(%{
      actor_id: actor_id,
      action: action,
      target_type: target_type,
      target_id: target_id,
      metadata: metadata
    })
    |> Repo.insert()
  end

  defp users_by_id([]), do: %{}

  defp users_by_id(ids) do
    Accounts.public_user_query()
    |> where([user], user.id in ^ids)
    |> Repo.all()
    |> Map.new(&{&1.id, &1})
  end

  defp leaderboard_rows(cursor, limit) do
    stats =
      Assignment
      |> join(:inner, [assignment], request in HelpRequest,
        on: request.id == assignment.request_id
      )
      |> where([assignment], assignment.status == :completed)
      |> group_by([assignment], assignment.helper_id)
      |> select([assignment, request], %{
        helper_id: assignment.helper_id,
        completed: count(assignment.id),
        unique_people: count(request.requester_id, :distinct),
        location_supported_people:
          filter(
            count(request.requester_id, :distinct),
            not is_nil(assignment.proximity_observed_at) and
              not is_nil(assignment.helper_movement_observed_at)
          ),
        verified_people:
          filter(
            count(request.requester_id, :distinct),
            not is_nil(assignment.handover_verified_at)
          )
      })

    ratings =
      Review
      |> where([review], not is_nil(review.revealed_at))
      |> group_by([review], review.reviewee_id)
      |> select([review], %{reviewee_id: review.reviewee_id, rating: avg(review.rating)})

    stats
    |> subquery()
    |> join(:inner, [stats], helper in User, on: helper.id == stats.helper_id)
    |> join(:left, [stats, _helper], rating in subquery(ratings),
      on: rating.reviewee_id == stats.helper_id
    )
    |> where(^leaderboard_after(cursor))
    |> order_by(
      [stats, helper],
      desc: stats.location_supported_people,
      desc: stats.verified_people,
      desc: stats.unique_people,
      desc: stats.completed,
      asc: fragment("coalesce(lower(?), '')", helper.display_name),
      asc: helper.id
    )
    |> limit(^(limit + 1))
    |> select([stats, helper, rating], %{
      helper_id: helper.id,
      completed: stats.completed,
      unique_people: stats.unique_people,
      location_supported_people: stats.location_supported_people,
      verified_people: stats.verified_people,
      rating: rating.rating,
      normalized_name: fragment("coalesce(lower(?), '')", helper.display_name)
    })
    |> Repo.all()
  end

  defp leaderboard_after(nil), do: dynamic(true)

  defp leaderboard_after(cursor) do
    dynamic(
      [stats, helper],
      stats.location_supported_people < ^cursor.location_supported_people or
        (stats.location_supported_people == ^cursor.location_supported_people and
           stats.verified_people < ^cursor.verified_people) or
        (stats.location_supported_people == ^cursor.location_supported_people and
           stats.verified_people == ^cursor.verified_people and
           stats.unique_people < ^cursor.unique_people) or
        (stats.location_supported_people == ^cursor.location_supported_people and
           stats.verified_people == ^cursor.verified_people and
           stats.unique_people == ^cursor.unique_people and stats.completed < ^cursor.completed) or
        (stats.location_supported_people == ^cursor.location_supported_people and
           stats.verified_people == ^cursor.verified_people and
           stats.unique_people == ^cursor.unique_people and stats.completed == ^cursor.completed and
           fragment("coalesce(lower(?), '')", helper.display_name) > ^cursor.normalized_name) or
        (stats.location_supported_people == ^cursor.location_supported_people and
           stats.verified_people == ^cursor.verified_people and
           stats.unique_people == ^cursor.unique_people and stats.completed == ^cursor.completed and
           fragment("coalesce(lower(?), '')", helper.display_name) == ^cursor.normalized_name and
           helper.id > ^cursor.helper_id)
    )
  end

  defp encode_leaderboard_cursor(row) do
    %{
      "location_supported_people" => row.location_supported_people,
      "verified_people" => row.verified_people,
      "unique_people" => row.unique_people,
      "completed" => row.completed,
      "normalized_name" => row.normalized_name,
      "helper_id" => row.helper_id
    }
    |> Jason.encode!()
    |> Base.url_encode64(padding: false)
  end

  defp decode_leaderboard_cursor(value) when value in [nil, ""], do: nil

  defp decode_leaderboard_cursor(value) when is_binary(value) do
    with {:ok, json} <- Base.url_decode64(value, padding: false),
         {:ok,
          %{
            "location_supported_people" => location_supported_people,
            "verified_people" => verified_people,
            "unique_people" => unique_people,
            "completed" => completed,
            "normalized_name" => normalized_name,
            "helper_id" => helper_id
          }} <- Jason.decode(json),
         true <-
           Enum.all?(
             [location_supported_people, verified_people, unique_people, completed],
             &(is_integer(&1) and &1 >= 0)
           ),
         true <- is_binary(normalized_name) and byte_size(normalized_name) <= 80,
         {:ok, helper_id} <- Ecto.UUID.cast(helper_id) do
      %{
        location_supported_people: location_supported_people,
        verified_people: verified_people,
        unique_people: unique_people,
        completed: completed,
        normalized_name: normalized_name,
        helper_id: helper_id
      }
    else
      _ -> nil
    end
  end

  defp decode_leaderboard_cursor(_value), do: nil

  defp decimal_average(nil), do: nil
  defp decimal_average(value), do: value |> Decimal.to_float() |> Float.round(1)

  defp before_review(query, nil), do: query

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

  defp before_report(query, nil), do: query

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

  defp before_block(query, nil), do: query

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

  defp before_abuse_signal(query, nil), do: query

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

  defp authorize_report_target(%Scope{user: user}, %{"request_id" => request_id})
       when is_binary(request_id) do
    with {:ok, request_id} <- cast_id(request_id) do
      case Repo.get(HelpRequest, request_id) do
        %HelpRequest{requester_id: requester_id} when requester_id != user.id -> :ok
        %HelpRequest{} -> {:error, :cannot_report_self}
        nil -> {:error, :not_found}
      end
    end
  end

  defp authorize_report_target(scope, %{"assignment_id" => assignment_id})
       when is_binary(assignment_id) do
    with {:ok, assignment_id} <- cast_id(assignment_id) do
      case Repo.get(Assignment, assignment_id) do
        %Assignment{} = assignment ->
          if Help.participant?(scope, assignment), do: :ok, else: {:error, :forbidden}

        nil ->
          {:error, :not_found}
      end
    end
  end

  defp authorize_report_target(scope, %{"message_id" => message_id}) when is_binary(message_id) do
    with {:ok, message_id} <- cast_id(message_id) do
      case Message |> Repo.get(message_id) |> Repo.preload(:assignment) do
        %Message{sender_id: sender_id, assignment: assignment} ->
          cond do
            sender_id == scope.user.id -> {:error, :cannot_report_self}
            Help.participant?(scope, assignment) -> :ok
            true -> {:error, :forbidden}
          end

        nil ->
          {:error, :not_found}
      end
    end
  end

  defp authorize_report_target(%Scope{user: user}, %{"activity_id" => activity_id})
       when is_binary(activity_id) do
    with {:ok, activity_id} <- cast_id(activity_id) do
      case Repo.get(Activity, activity_id) do
        %Activity{creator_id: creator_id} when creator_id == user.id ->
          {:error, :cannot_report_self}

        %Activity{} = activity ->
          if is_nil(activity.hidden_at) or activity_participant?(activity.id, user.id),
            do: :ok,
            else: {:error, :not_found}

        nil ->
          {:error, :not_found}
      end
    end
  end

  defp authorize_report_target(
         %Scope{user: user},
         %{"activity_message_id" => message_id}
       )
       when is_binary(message_id) do
    with {:ok, message_id} <- cast_id(message_id) do
      case Repo.get(ActivityMessage, message_id) do
        %ActivityMessage{sender_id: sender_id} when sender_id == user.id ->
          {:error, :cannot_report_self}

        %ActivityMessage{activity_id: activity_id} ->
          if activity_participant?(activity_id, user.id),
            do: :ok,
            else: {:error, :forbidden}

        nil ->
          {:error, :not_found}
      end
    end
  end

  defp authorize_report_target(_scope, _attrs), do: {:error, :invalid_target}

  defp activity_participant?(activity_id, user_id) do
    Repo.exists?(
      from participant in Participant,
        where:
          participant.activity_id == ^activity_id and participant.user_id == ^user_id and
            participant.status == :approved
    )
  end

  defp create_signal_once(kind, subject_id, assignment_id, metadata) do
    query =
      from signal in AbuseSignal,
        where:
          signal.kind == ^kind and signal.subject_id == ^subject_id and
            signal.assignment_id == ^assignment_id

    if Repo.exists?(query) do
      {:ok, :already_recorded}
    else
      %AbuseSignal{}
      |> AbuseSignal.changeset(%{
        kind: kind,
        subject_id: subject_id,
        assignment_id: assignment_id,
        metadata: metadata
      })
      |> Repo.insert()
    end
  end

  defp create_velocity_signal_once(subject_id, action) do
    query =
      from signal in AbuseSignal,
        where:
          signal.kind == :velocity and signal.subject_id == ^subject_id and
            signal.status == :open

    case Repo.one(query) do
      nil ->
        %AbuseSignal{}
        |> AbuseSignal.changeset(%{
          kind: :velocity,
          subject_id: subject_id,
          metadata: %{"action" => to_string(action)}
        })
        |> Repo.insert()

      signal ->
        signal
        |> Ecto.Changeset.change(
          metadata: Map.put(signal.metadata, "latest_action", to_string(action))
        )
        |> Repo.update()
    end
  end

  defp maybe_status(query, nil), do: query
  defp maybe_status(query, status), do: where(query, [report], report.status == ^status)

  defp clear_pair_tracking(first_user_id, second_user_id) do
    assignments =
      Assignment
      |> join(:inner, [assignment], request in HelpRequest,
        on: request.id == assignment.request_id
      )
      |> where(
        [assignment, request],
        assignment.status in [:accepted, :in_progress] and
          ((assignment.helper_id == ^first_user_id and
              request.requester_id == ^second_user_id) or
             (assignment.helper_id == ^second_user_id and
                request.requester_id == ^first_user_id))
      )
      |> select([assignment, request], {assignment.id, request.id})
      |> Repo.all()

    active_assignment_ids = Enum.map(assignments, &elem(&1, 0))

    active_sessions =
      if active_assignment_ids == [] do
        []
      else
        TrackingSession
        |> where([session], session.active and session.assignment_id in ^active_assignment_ids)
        |> select([session], {session.id, session.assignment_id, session.user_id})
        |> Repo.all()
      end

    session_ids = Enum.map(active_sessions, &elem(&1, 0))

    if session_ids != [] do
      Repo.delete_all(
        from position in Position, where: position.tracking_session_id in ^session_ids
      )

      Repo.update_all(
        from(session in TrackingSession, where: session.id in ^session_ids),
        set: [
          active: false,
          ended_at: DateTime.utc_now(:second),
          updated_at: DateTime.utc_now(:second)
        ]
      )
    end

    %{
      request_ids: assignments |> Enum.map(&elem(&1, 1)) |> Enum.uniq(),
      stopped:
        Enum.map(active_sessions, fn {_id, assignment_id, user_id} -> {assignment_id, user_id} end)
    }
  end

  defp clear_user_tracking(user_id) do
    active_sessions =
      TrackingSession
      |> join(:inner, [session], assignment in Assignment,
        on: assignment.id == session.assignment_id
      )
      |> join(:inner, [_session, assignment], request in HelpRequest,
        on: request.id == assignment.request_id
      )
      |> where([session], session.active and session.user_id == ^user_id)
      |> select([session, assignment, request], {session.id, assignment.id, request.id})
      |> Repo.all()

    session_ids = Enum.map(active_sessions, &elem(&1, 0))

    if session_ids != [] do
      now = DateTime.utc_now(:second)

      Repo.delete_all(
        from position in Position, where: position.tracking_session_id in ^session_ids
      )

      Repo.update_all(
        from(session in TrackingSession, where: session.id in ^session_ids),
        set: [active: false, ended_at: now, updated_at: now]
      )
    end

    %{
      stopped:
        Enum.map(active_sessions, fn {_session_id, assignment_id, _request_id} ->
          {assignment_id, user_id}
        end),
      request_ids: active_sessions |> Enum.map(&elem(&1, 2)) |> Enum.uniq()
    }
  end

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

  defp cast_id(value) do
    case Ecto.UUID.cast(value) do
      {:ok, id} -> {:ok, id}
      :error -> {:error, :not_found}
    end
  end
end
