# frozen_string_literal: true

class ReviewablesController < ApplicationController
  requires_login

  PER_PAGE = 10

  before_action :version_required, only: %i[update perform]
  before_action :ensure_can_see, except: [:destroy]

  around_action :with_deleted_content,
                only: %i[index show],
                if: ->(controller) { controller.guardian.is_staff? }

  def index
    offset = params[:offset].to_i

    if params[:type].present?
      unless Reviewable.valid_filter_type?(params[:type])
        raise Discourse::InvalidParameters.new(:type)
      end
    end

    status = (params[:status] || "pending").to_sym
    raise Discourse::InvalidParameters.new(:status) if allowed_statuses.exclude?(status)

    topic_id = params[:topic_id] ? params[:topic_id].to_i : nil
    category_id = params[:category_id] ? params[:category_id].to_i : nil

    custom_keys = Reviewable.custom_filters.map(&:first)
    additional_filters =
      JSON.parse(params.fetch(:additional_filters, {}), symbolize_names: true).slice(*custom_keys)
    filters = {
      ids: params[:ids],
      status: status,
      category_id: category_id,
      topic_id: topic_id,
      additional_filters: additional_filters.reject { |_, v| v.blank? },
    }

    %i[
      priority
      username
      reviewed_by
      claimed_by
      from_date
      to_date
      type
      sort_order
      flagged_by
      score_type
    ].each { |filter_key| filters[filter_key] = params[filter_key] }

    total_rows = Reviewable.list_for(current_user, **filters).count
    reviewables =
      Reviewable.list_for(current_user, **filters.merge(limit: PER_PAGE, offset: offset)).to_a

    claimed_topics = ReviewableClaimedTopic.claimed_hash(reviewables.map { |r| r.topic_id }.uniq)
    Reviewable.preload_author_penalties(reviewables)

    # This is a bit awkward, but ActiveModel serializers doesn't seem to serialize STI. Note `hash`
    # is mutated by the serializer and contains the side loaded records which must be merged in the end.
    hash = {}
    json = {
      reviewables:
        reviewables.map! do |r|
          result =
            r
              .serializer
              .new(r, root: nil, hash: hash, scope: guardian, claimed_topics: claimed_topics)
              .as_json
          hash[:bundled_actions].uniq!
          (hash["actions"] || []).uniq!
          result
        end,
      meta:
        filters.merge(
          total_rows_reviewables: total_rows,
          types: meta_types,
          reviewable_types:
            Reviewable.types + Reviewable.custom_filter_type_options.map { |option| option[:id] },
          unknown_reviewable_types_and_sources: Reviewable.unknown_types_and_sources,
          score_types:
            ReviewableScore
              .types
              .filter { |k, v| k != :notify_user }
              .map { |k, v| { id: v, name: ReviewableScore.type_title(k) } }
              .concat(
                Reviewable.custom_reason_filter_options.map { |option| option.slice(:id, :name) },
              ),
          reviewable_count: current_user.reviewable_count,
          unseen_reviewable_count: Reviewable.unseen_reviewable_count(current_user),
        ),
    }
    if (offset + PER_PAGE) < total_rows
      json[:meta][:load_more_reviewables] = review_path(filters.merge(offset: offset + PER_PAGE))
    end
    json.merge!(hash)

    render_json_dump(json, rest_serializer: true)
  end

  def user_menu_list
    json = {
      reviewables:
        Reviewable.basic_serializers_for_list(
          Reviewable.user_menu_list_for(current_user),
          current_user,
        ).as_json,
      reviewable_count: current_user.reviewable_count,
    }
    render_json_dump(json, rest_serializer: true)
  end

  def count
    render_json_dump(count: Reviewable.pending_count(current_user))
  end

  def topics
    topic_ids = Set.new

    stats = {}
    unique_users = {}

    # topics isn't indexed on `reviewable_score` and doesn't know what the current user can see,
    # so let's query from the inside out.
    pending = Reviewable.viewable_by(current_user).pending
    pending = pending.where("score >= ?", Reviewable.min_score_for_priority)

    pending.each do |r|
      topic_ids << r.topic_id

      meta = stats[r.topic_id] ||= { count: 0, unique_users: 0 }
      users = unique_users[r.topic_id] ||= Set.new

      r.reviewable_scores.each do |rs|
        users << rs.user_id
        meta[:count] += 1
      end
      meta[:unique_users] = users.size
    end

    topics = Topic.where(id: topic_ids).order("reviewable_score DESC")
    render_serialized(
      topics,
      ReviewableTopicSerializer,
      root: "reviewable_topics",
      stats: stats,
      claimed_topics: ReviewableClaimedTopic.claimed_hash(topic_ids),
      rest_serializer: true,
      meta: {
        types: meta_types,
      },
    )
  end

  def explain
    reviewable = find_reviewable

    render_serialized(
      { reviewable: reviewable, scores: reviewable.explain_score },
      ReviewableExplanationSerializer,
      rest_serializer: true,
      root: "reviewable_explanation",
    )
  end

  def show
    reviewable = find_reviewable

    render_serialized(
      reviewable,
      reviewable.serializer,
      rest_serializer: true,
      claimed_topics: ReviewableClaimedTopic.claimed_hash([reviewable.topic_id]),
      root: "reviewable",
      meta: {
        types: meta_types,
      },
    )
  end

  def destroy
    user = fetch_target_user

    reviewable =
      Reviewable.find_by_flagger_or_queued_post_creator(
        id: params[:reviewable_id],
        user_id: user.id,
      )
    raise Discourse::NotFound.new if reviewable.blank?

    reviewable.perform(current_user, :delete, { guardian: @guardian })

    render json: success_json
  end

  def scrub
    raise Discourse::InvalidAccess unless @guardian.is_admin?

    params.require(:reason)

    reviewable =
      Reviewable.find_by(
        id: params[:reviewable_id],
        status: Reviewable.statuses[:rejected],
        type: Reviewable.scrubbable_types,
      )
    raise Discourse::NotFound if reviewable.blank?

    raise Discourse::InvalidAccess if reviewable.payload["scrubbed_by"].present?

    reviewable.scrub(params[:reason], @guardian)

    render json: success_json
  end

  def update
    reviewable = find_reviewable
    if error = claim_error?(reviewable)
      return render_json_error(error)
    end

    editable = reviewable.editable_for(guardian)
    raise Discourse::InvalidAccess.new if editable.blank?

    # Validate parameters are all editable
    edit_params = params[:reviewable] || {}
    edit_params.each do |name, value|
      if value.is_a?(ActionController::Parameters)
        value.each do |pay_name, pay_value|
          raise Discourse::InvalidAccess.new unless editable.has?("#{name}.#{pay_name}")
        end
      else
        raise Discourse::InvalidAccess.new unless editable.has?(name)
      end
    end

    begin
      if reviewable.update_fields(edit_params, current_user, version: params[:version].to_i)
        result = edit_params.merge(version: reviewable.version)
        if reviewable.is_a?(ReviewableQueuedPost) && reviewable.payload.present?
          result[:cooked] = PrettyText.cook(reviewable.payload["raw"]) if reviewable.payload["raw"]
          if reviewable.payload["title"]
            result[:fancy_title] = ERB::Util.html_escape(reviewable.payload["title"])
          end
        end
        render json: result
      else
        render_json_error(reviewable.errors)
      end
    rescue Reviewable::UpdateConflict
      render_json_error(I18n.t("reviewables.conflict"), status: 409)
    end
  end

  def perform
    args = { version: params[:version].to_i }

    result = nil
    begin
      reviewable = find_reviewable

      if error = claim_error?(reviewable)
        return render_json_error(error)
      end

      if reviewable.type_class.respond_to?(:additional_args)
        args.merge!(reviewable.type_class.additional_args(params) || {})
      end

      plugin_params =
        DiscoursePluginRegistry.reviewable_params.select do |reviewable_param|
          reviewable.type == reviewable_param[:type].to_s.classify
        end
      args.merge!(params.slice(*plugin_params.map { |pp| pp[:param] }).permit!)

      result = reviewable.perform(current_user, params[:action_id].to_sym, args)
    rescue Reviewable::InvalidAction => e
      if reviewable.type == "ReviewableUser" && !reviewable.pending? && reviewable.target.blank?
        raise Discourse::NotFound.new(
                e.message,
                custom_message: "reviewables.already_handled_and_user_not_exist",
              )
      else
        # Consider InvalidAction an InvalidAccess
        raise Discourse::InvalidAccess.new(e.message)
      end
    rescue Reviewable::UpdateConflict
      return render_json_error(I18n.t("reviewables.conflict"), status: 409)
    end

    if result.success?
      render_serialized(result, ReviewablePerformResultSerializer)
    else
      render_json_error(result)
    end
  end

  def settings
    raise Discourse::InvalidAccess.new unless current_user.admin?

    post_action_types = PostActionType.where(id: PostActionType.flag_types.values).order("id")

    if request.put?
      params[:reviewable_priorities].each do |id, priority|
        if !priority.nil? && Reviewable.priorities.has_value?(priority.to_i)
          # For now, the score bonus is equal to the priority. In the future we might want
          # to calculate it a different way.
          PostActionType.where(id: id).update_all(
            reviewable_priority: priority.to_i,
            score_bonus: priority.to_f,
          )
        end
      end
    end

    data = { reviewable_score_types: post_action_types }
    render_serialized(data, ReviewableSettingsSerializer, rest_serializer: true)
  end

  protected

  def claim_error?(reviewable)
    return if SiteSetting.reviewable_claiming == "disabled" || reviewable.topic_id.blank?

    claimed_by_id = ReviewableClaimedTopic.where(topic_id: reviewable.topic_id).pluck(:user_id)[0]

    if SiteSetting.reviewable_claiming == "required" && claimed_by_id.blank?
      I18n.t("reviewables.must_claim")
    elsif claimed_by_id.present? && claimed_by_id != current_user.id
      I18n.t("reviewables.user_claimed")
    end
  end

  def find_reviewable
    reviewable = Reviewable.viewable_by(current_user).where(id: params[:reviewable_id]).first
    raise Discourse::NotFound.new if reviewable.blank?
    reviewable
  end

  def allowed_statuses
    @allowed_statuses ||= (%i[reviewed all] + Reviewable.statuses.symbolize_keys.keys)
  end

  def version_required
    render_json_error(I18n.t("reviewables.missing_version"), status: 422) if params[:version].blank?
  end

  def meta_types
    {
      created_by: "user",
      target_created_by: "user",
      target_deleted_by: "user",
      reviewed_by: "user",
      claimed_by: "claimed_by",
    }
  end

  def ensure_can_see
    Guardian.new(current_user).ensure_can_see_review_queue!
  end

  def with_deleted_content
    Post.unscoped { Topic.unscoped { PostAction.unscoped { yield } } }
  end
end
