# frozen_string_literal: true

class ReviewableFlaggedPost < Reviewable
  include ReviewableActionBuilder

  scope :pending_and_default_visible, -> { pending.default_visible }

  # Penalties are handled by the modal after the action is performed
  def self.action_aliases
    {
      agree_and_keep_hidden: :agree_and_keep,
      agree_and_silence: :agree_and_keep,
      agree_and_suspend: :agree_and_keep,
      agree_and_edit: :agree_and_keep,
      disagree_and_restore: :disagree,
      ignore_and_do_nothing: :ignore,
      delete_user_block: :delete_and_block_user, # legacy name mapped to concern method
    }
  end

  def self.counts_for(posts)
    result = {}

    counts = DB.query(<<~SQL, pending: statuses[:pending])
      SELECT r.target_id AS post_id,
        rs.reviewable_score_type,
        count(*) as total
      FROM reviewables AS r
      INNER JOIN reviewable_scores AS rs ON rs.reviewable_id = r.id
      WHERE r.type = 'ReviewableFlaggedPost'
        AND r.status = :pending
      GROUP BY r.target_id, rs.reviewable_score_type
    SQL

    counts.each do |c|
      result[c.post_id] ||= {}
      result[c.post_id][c.reviewable_score_type] = c.total
    end

    result
  end

  def post
    @post ||= (target || Post.with_deleted.find_by(id: target_id))
  end

  def build_actions(actions, guardian, args)
    return unless pending?
    return if post.blank?
    super
  end

  def build_combined_actions(actions, guardian, args)
    # existing combined logic
    agree_bundle =
      actions.add_bundle("#{id}-agree", icon: "thumbs-up", label: "reviewables.actions.agree.title")

    if !post.user_deleted? && !post.hidden?
      build_action(actions, :agree_and_hide, icon: "far-eye-slash", bundle: agree_bundle)
    end

    if post.hidden?
      build_action(actions, :agree_and_keep_hidden, icon: "far-eye-slash", bundle: agree_bundle)
    else
      build_action(actions, :agree_and_keep, icon: "far-eye", bundle: agree_bundle)
      build_action(
        actions,
        :agree_and_edit,
        icon: "pencil",
        bundle: agree_bundle,
        client_action: "edit",
      )
    end

    if guardian.can_delete_post_or_topic?(post)
      build_action(actions, :delete_and_agree, icon: "trash-can", bundle: agree_bundle)

      if post.reply_count > 0
        build_action(
          actions,
          :delete_and_agree_replies,
          icon: "trash-can",
          bundle: agree_bundle,
          confirm: true,
        )
      end
    end

    if guardian.can_suspend?(target_created_by)
      build_action(
        actions,
        :agree_and_silence,
        icon: "microphone-slash",
        bundle: agree_bundle,
        client_action: "silence",
      )
      build_action(
        actions,
        :agree_and_suspend,
        icon: "ban",
        bundle: agree_bundle,
        client_action: "suspend",
      )
    end

    if (potential_spam? || potentially_illegal?) && guardian.can_delete_user?(target_created_by)
      delete_user_actions(actions, agree_bundle)
    end

    if post.user_deleted?
      build_action(actions, :agree_and_restore, icon: "far-eye", bundle: agree_bundle)
    end

    post_visible_or_system_user = !post.hidden? || guardian.user.is_system_user?
    can_delete_post_or_topic = guardian.can_delete_post_or_topic?(post)

    # We must return early in this case otherwise we can end up with a bundle
    # with no associated actions, which is not valid on the client.
    return if !can_delete_post_or_topic && !post_visible_or_system_user && post.hidden?

    disagree_bundle =
      actions.add_bundle(
        "#{id}-disagree",
        icon: "far-eye",
        label: "reviewables.actions.disagree_bundle.title",
      )

    if post.hidden?
      build_action(actions, :disagree_and_restore, icon: "far-eye", bundle: disagree_bundle)
    else
      build_action(actions, :disagree, icon: "far-eye", bundle: disagree_bundle)
    end

    if post_visible_or_system_user
      build_action(actions, :ignore_and_do_nothing, icon: "xmark", bundle: disagree_bundle)
    end
    if can_delete_post_or_topic
      build_action(actions, :delete_and_ignore, icon: "trash-can", bundle: disagree_bundle)
      if post.reply_count > 0
        build_action(
          actions,
          :delete_and_ignore_replies,
          icon: "trash-can",
          confirm: true,
          bundle: disagree_bundle,
        )
      end
    end
  end

  def perform_ignore(performed_by, args)
    perform_ignore_and_do_nothing(performed_by, args)
  end

  def post_action_type_view
    @post_action_type_view ||= PostActionTypeView.new
  end

  def perform_ignore_and_do_nothing(performed_by, args)
    actions =
      PostAction
        .active
        .where(post_id: target_id)
        .where(post_action_type_id: post_action_type_view.notify_flag_type_ids)

    actions.each do |action|
      action.deferred_at = Time.zone.now
      action.deferred_by_id = performed_by.id
      # so callback is called
      action.save
      unless args[:expired]
        action.add_moderator_post_if_needed(performed_by, :ignored, args[:post_was_deleted])
      end
    end

    if actions.first.present?
      unassign_topic performed_by, post
      DiscourseEvent.trigger(:flag_reviewed, post)
      DiscourseEvent.trigger(:flag_deferred, actions.first)
    end

    create_result(:success, :ignored, actions.map(&:user_id), false)
  end

  def perform_agree_and_keep(performed_by, args)
    agree(performed_by, args)
  end

  def perform_delete_user(performed_by, args)
    super
    agree(performed_by, args)
  end

  def perform_delete_and_block_user(performed_by, args)
    super
    agree(performed_by, args)
  end

  def perform_agree_and_hide(performed_by, args)
    agree(performed_by, args) { |pa| post.hide!(pa.post_action_type_id) }
  end

  def perform_agree_and_restore(performed_by, args)
    agree(performed_by, args) { PostDestroyer.new(performed_by, post).recover }
  end

  def perform_disagree(performed_by, args)
    # -1 is the automatic system clear
    action_type_ids =
      if performed_by.id == Discourse::SYSTEM_USER_ID
        post_action_type_view.auto_action_flag_types.values
      else
        post_action_type_view.notify_flag_type_ids
      end

    actions =
      PostAction.active.where(post_id: target_id).where(post_action_type_id: action_type_ids)

    actions.each do |action|
      action.disagreed_at = Time.zone.now
      action.disagreed_by_id = performed_by.id
      # so callback is called
      action.save
      action.add_moderator_post_if_needed(performed_by, :disagreed)
    end

    # reset all cached counters
    cached = {}
    action_type_ids.each do |atid|
      column = "#{post_action_type_view.types[atid]}_count"
      cached[column] = 0 if ActiveRecord::Base.connection.column_exists?(:posts, column)
    end

    Post.with_deleted.where(id: target_id).update_all(cached)

    if actions.first.present?
      unassign_topic performed_by, post
      DiscourseEvent.trigger(:flag_reviewed, post)
      DiscourseEvent.trigger(:flag_disagreed, actions.first)
    end

    # Undo hide/silence if applicable
    if post&.hidden?
      notify_poster(performed_by)
      post.unhide!
      UserSilencer.unsilence(post.user) if UserSilencer.was_silenced_for?(post)
    end

    create_result(:success, :rejected, actions.map(&:user_id), false)
  end

  def perform_delete_and_ignore(performed_by, args)
    result = perform_ignore_and_do_nothing(performed_by, args)
    destroyer(performed_by, post).destroy
    result
  end

  def perform_delete_and_ignore_replies(performed_by, args)
    result = perform_ignore_and_do_nothing(performed_by, args)
    PostDestroyer.delete_with_replies(performed_by, post, self)

    result
  end

  def perform_delete_and_agree(performed_by, args)
    result = agree(performed_by, args)
    destroyer(performed_by, post).destroy
    result
  end

  def perform_delete_and_agree_replies(performed_by, args)
    result = agree(performed_by, args)
    PostDestroyer.delete_with_replies(performed_by, post, self)
    result
  end

  protected

  def agree(performed_by, args)
    actions =
      PostAction
        .active
        .where(post_id: target_id)
        .where(post_action_type_id: post_action_type_view.notify_flag_types.values)

    trigger_spam = false
    actions.each do |action|
      ActiveRecord::Base.transaction do
        action.agreed_at = Time.zone.now
        action.agreed_by_id = performed_by.id
        # so callback is called
        action.save
        DB.after_commit do
          action.add_moderator_post_if_needed(performed_by, :agreed, args[:post_was_deleted])
          trigger_spam = true if action.post_action_type_id == post_action_type_view.types[:spam]
        end
      end
    end

    DiscourseEvent.trigger(:confirmed_spam_post, post) if trigger_spam

    if actions.first.present?
      unassign_topic performed_by, post
      DiscourseEvent.trigger(:flag_reviewed, post)
      DiscourseEvent.trigger(:flag_agreed, actions.first)
      yield(actions.first) if block_given?
    end

    create_result(:success, :approved, actions.map(&:user_id), false)
  end

  def unassign_topic(performed_by, post)
    topic = post.topic
    return unless topic && performed_by && SiteSetting.reviewable_claiming != "disabled"
    deleted_count = ReviewableClaimedTopic.where(topic_id: topic.id, automatic: false).delete_all
    if deleted_count > 0
      topic.reviewables.find_each { |reviewable| reviewable.log_history(:unclaimed, performed_by) }
    end

    user_ids = User.staff.pluck(:id)

    if SiteSetting.enable_category_group_moderation? && topic.category
      user_ids.concat(
        GroupUser
          .joins(
            "INNER JOIN category_moderation_groups ON category_moderation_groups.group_id = group_users.group_id",
          )
          .where("category_moderation_groups.category_id": topic.category.id)
          .distinct
          .pluck(:user_id),
      )
      user_ids.uniq!
    end

    data = {
      topic_id: topic.id,
      user: BasicUserSerializer.new(performed_by, root: false).as_json,
      automatic: false,
      claimed: false,
    }

    MessageBus.publish("/reviewable_claimed", data, user_ids: user_ids)
  end

  private

  def destroyer(performed_by, post)
    PostDestroyer.new(performed_by, post, reviewable: self)
  end

  def notify_poster(performed_by)
    return unless performed_by.human? && performed_by.staff?

    Jobs.enqueue(
      :send_system_message,
      user_id: post.user_id,
      message_type: "flags_disagreed",
      message_options: {
        flagged_post_raw_content: post.raw,
        url: post.url,
      },
    )
  end
end

# == Schema Information
#
# Table name: reviewables
#
#  id                      :bigint           not null, primary key
#  type                    :string           not null
#  type_source             :string           default("unknown"), not null
#  status                  :integer          default("pending"), not null
#  created_by_id           :integer          not null
#  reviewable_by_moderator :boolean          default(FALSE), not null
#  category_id             :integer
#  topic_id                :integer
#  score                   :float            default(0.0), not null
#  potential_spam          :boolean          default(FALSE), not null
#  target_id               :integer
#  target_type             :string
#  target_created_by_id    :integer
#  payload                 :json
#  version                 :integer          default(0), not null
#  latest_score            :datetime
#  created_at              :datetime         not null
#  updated_at              :datetime         not null
#  force_review            :boolean          default(FALSE), not null
#  reject_reason           :text
#  potentially_illegal     :boolean          default(FALSE)
#
# Indexes
#
#  idx_reviewables_score_desc_created_at_desc                  (score,created_at)
#  index_reviewables_on_reviewable_by_group_id                 (reviewable_by_group_id)
#  index_reviewables_on_status_and_created_at                  (status,created_at)
#  index_reviewables_on_status_and_score                       (status,score)
#  index_reviewables_on_status_and_type                        (status,type)
#  index_reviewables_on_target_id_where_post_type_eq_post      (target_id) WHERE ((target_type)::text = 'Post'::text)
#  index_reviewables_on_topic_id_and_status_and_created_by_id  (topic_id,status,created_by_id)
#  index_reviewables_on_type_and_target_id                     (type,target_id) UNIQUE
#
