# frozen_string_literal: true

class PostAction < ActiveRecord::Base
  include RateLimiter::OnCreateRecord
  include Trashable

  belongs_to :post
  belongs_to :user
  belongs_to :post_action_type
  belongs_to :related_post, class_name: "Post"
  belongs_to :target_user, class_name: "User"

  rate_limit :post_action_rate_limiter

  scope :spam_flags, -> { where(post_action_type_id: PostActionType.types[:spam]) }
  scope :flags, -> { where(post_action_type_id: PostActionType.notify_flag_type_ids) }
  scope :publics, -> { where(post_action_type_id: PostActionType.public_type_ids) }
  scope :active, -> { where(disagreed_at: nil, deferred_at: nil, agreed_at: nil, deleted_at: nil) }

  after_save :update_counters
  validate :ensure_unique_actions, on: :create

  def self.counts_for(collection, user)
    return {} if collection.blank? || !user

    collection_ids = collection.map(&:id)
    user_id = user.try(:id) || 0

    post_actions = PostAction.where(post_id: collection_ids, user_id: user_id)

    user_actions = {}
    post_actions.each do |post_action|
      user_actions[post_action.post_id] ||= {}
      user_actions[post_action.post_id][post_action.post_action_type_id] = post_action
    end

    user_actions
  end

  def self.lookup_for(user, topics, post_action_type_id)
    return if topics.blank?
    # in critical path 2x faster than AR
    #
    topic_ids = topics.map(&:id)
    map = {}

    builder = DB.build <<~SQL
      SELECT p.topic_id, p.post_number
      FROM post_actions pa
      JOIN posts p ON pa.post_id = p.id
      WHERE p.deleted_at IS NULL AND pa.deleted_at IS NULL AND
         pa.post_action_type_id = :post_action_type_id AND
         pa.user_id = :user_id AND
         p.topic_id IN (:topic_ids)
      ORDER BY p.topic_id, p.post_number
    SQL

    builder
      .query(user_id: user.id, post_action_type_id: post_action_type_id, topic_ids: topic_ids)
      .each { |row| (map[row.topic_id] ||= []) << row.post_number }

    map
  end

  def self.count_per_day_for_type(post_action_type, opts = nil)
    opts ||= {}
    result = unscoped.where(post_action_type_id: post_action_type)
    result =
      result.where(
        "post_actions.created_at >= ?",
        opts[:start_date] || (opts[:since_days_ago] || 30).days.ago,
      )
    result = result.where("post_actions.created_at <= ?", opts[:end_date]) if opts[:end_date]
    if opts[:category_id]
      if opts[:include_subcategories]
        result =
          result.joins(post: :topic).where(
            "topics.category_id IN (?)",
            Category.subcategory_ids(opts[:category_id]),
          )
      else
        result = result.joins(post: :topic).where("topics.category_id = ?", opts[:category_id])
      end
    end

    if opts[:group_ids]
      result =
        result
          .joins("INNER JOIN users ON users.id = post_actions.user_id")
          .joins("INNER JOIN group_users ON group_users.user_id = users.id")
          .where("group_users.group_id IN (?)", opts[:group_ids])
    end

    result.group("date(post_actions.created_at)").order("date(post_actions.created_at)").count
  end

  def add_moderator_post_if_needed(moderator, disposition, delete_post = false)
    return if !SiteSetting.auto_respond_to_flag_actions
    return if related_post.nil? || related_post.topic.nil?
    return if staff_already_replied?(related_post.topic)
    message_key = +"flags_dispositions.#{disposition}"
    message_key << "_and_deleted" if delete_post

    I18n.with_locale(SiteSetting.default_locale) do
      related_post.topic.add_moderator_post(moderator, I18n.t(message_key))
    end

    # archive message for moderators
    GroupArchivedMessage.archive!(Group[:moderators].id, related_post.topic)
  end

  def staff_already_replied?(topic)
    topic
      .posts
      .where(
        "user_id IN (SELECT id FROM users WHERE moderator OR admin) OR (post_type != :regular_post_type)",
        regular_post_type: Post.types[:regular],
      )
      .exists?
  end

  def self.limit_action!(user, post, post_action_type_id)
    RateLimiter.new(user, "post_action-#{post.id}_#{post_action_type_id}", 4, 1.minute).performed!
  end

  def self.copy(original_post, target_post)
    cols_to_copy = (column_names - %w[id post_id]).join(", ")

    DB.exec <<~SQL
    INSERT INTO post_actions(post_id, #{cols_to_copy})
    SELECT #{target_post.id}, #{cols_to_copy}
    FROM post_actions
    WHERE post_id = #{original_post.id}
    SQL

    target_post.post_actions.each { |post_action| post_action.update_counters }
  end

  def remove_act!(user)
    trash!(user)
    # NOTE: save is called to ensure all callbacks are called
    # trash will not trigger callbacks, and triggering after_commit
    # is not trivial
    save
  end

  def is_like?
    post_action_type_id == PostActionType.types[:like]
  end

  def is_flag?
    !!PostActionType.notify_flag_types[post_action_type_id]
  end

  def is_private_message?
    post_action_type_id == PostActionType.types[:notify_user] ||
      post_action_type_id == PostActionType.types[:notify_moderators]
  end

  # A custom rate limiter for this model
  def post_action_rate_limiter
    return unless is_flag? || is_like?

    return @rate_limiter if @rate_limiter.present?

    %w[like flag].each do |type|
      if public_send("is_#{type}?")
        limit = SiteSetting.get("max_#{type}s_per_day")

        if (is_flag? || is_like?) && user && user.trust_level >= 2
          multiplier =
            SiteSetting.get("tl#{user.trust_level}_additional_#{type}s_per_day_multiplier").to_f
          multiplier = 1.0 if multiplier < 1.0

          limit = (limit * multiplier).to_i
        end

        @rate_limiter = RateLimiter.new(user, "create_#{type}", limit, 1.day.to_i)
        return @rate_limiter
      end
    end
  end

  def ensure_unique_actions
    post_action_type_ids = is_flag? ? PostActionType.notify_flag_types.values : post_action_type_id

    acted =
      PostAction
        .where(user_id: user_id)
        .where(post_id: post_id)
        .where(post_action_type_id: post_action_type_ids)
        .where(deleted_at: nil)
        .where(disagreed_at: nil)
        .where(targets_topic: targets_topic)
        .exists?

    errors.add(:post_action_type_id) if acted
  end

  def post_action_type_key
    PostActionType.types[post_action_type_id]
  end

  def update_counters
    # Update denormalized counts
    column = "#{post_action_type_key}_count"
    count = PostAction.where(post_id: post_id).where(post_action_type_id: post_action_type_id).count

    # We probably want to refactor this method to something cleaner.
    case post_action_type_key
    when :like
      # 'like_score' is weighted higher for staff accounts
      score =
        PostAction
          .joins(:user)
          .where(post_id: post_id)
          .sum(
            "CASE WHEN users.moderator OR users.admin THEN #{SiteSetting.staff_like_weight} ELSE 1 END",
          )
      Post.where(id: post_id).update_all [
                     "like_count = :count, like_score = :score",
                     count: count,
                     score: score,
                   ]
    else
      if ActiveRecord::Base.connection.column_exists?(:posts, column)
        Post.where(id: post_id).update_all ["#{column} = ?", count]
      end
    end

    topic_id = Post.with_deleted.where(id: post_id).pick(:topic_id)

    # topic_user
    if post_action_type_key == :like
      TopicUser.update_post_action_cache(
        user_id: user_id,
        topic_id: topic_id,
        post_action_type: post_action_type_key,
      )
    end

    Topic.find_by(id: topic_id)&.update_action_counts if column == "like_count"
  end
end

# == Schema Information
#
# Table name: post_actions
#
#  id                  :integer          not null, primary key
#  post_id             :integer          not null
#  user_id             :integer          not null
#  post_action_type_id :integer          not null
#  deleted_at          :datetime
#  created_at          :datetime         not null
#  updated_at          :datetime         not null
#  deleted_by_id       :integer
#  related_post_id     :integer
#  staff_took_action   :boolean          default(FALSE), not null
#  deferred_by_id      :integer
#  targets_topic       :boolean          default(FALSE), not null
#  agreed_at           :datetime
#  agreed_by_id        :integer
#  deferred_at         :datetime
#  disagreed_at        :datetime
#  disagreed_by_id     :integer
#
# Indexes
#
#  idx_unique_actions                                          (user_id,post_action_type_id,post_id,targets_topic) UNIQUE WHERE ((deleted_at IS NULL) AND (disagreed_at IS NULL) AND (deferred_at IS NULL))
#  idx_unique_flags                                            (user_id,post_id,targets_topic) UNIQUE WHERE ((deleted_at IS NULL) AND (disagreed_at IS NULL) AND (deferred_at IS NULL) AND (post_action_type_id = ANY (ARRAY[3, 4, 7, 8])))
#  index_post_actions_on_post_action_type_id_and_disagreed_at  (post_action_type_id,disagreed_at) WHERE (disagreed_at IS NULL)
#  index_post_actions_on_post_id                               (post_id)
#  index_post_actions_on_user_id                               (user_id)
#  index_post_actions_on_user_id_and_post_action_type_id       (user_id,post_action_type_id) WHERE (deleted_at IS NULL)
#