# frozen_string_literal: true

class TopicHotScore < ActiveRecord::Base
  belongs_to :topic

  DEFAULT_BATCH_SIZE = 1000
  CACHE_KEY = "hot_topics_ids"

  def self.hottest_topic_ids
    Discourse.cache.read(CACHE_KEY).presence || Set.new
  end

  def self.recreate_hottest_topic_ids_cache
    hot_topics =
      Topic
        .where(archetype: Archetype.default)
        .where(visible: true)
        .where.not(id: Category.topic_ids)

    # 10% of the topics with activity since the hot topics cutoff.
    limit =
      hot_topics
        .where("last_posted_at > ?", SiteSetting.hot_topics_recent_days.days.ago)
        .count
        .to_i * 0.1

    # capped at 100 topics
    limit = [limit, 100].min

    sql = <<~SQL
      SELECT topic_id
      FROM topic_hot_scores
      WHERE topic_id IN (#{hot_topics.select(:id).to_sql})
      ORDER BY score DESC
      LIMIT :limit
    SQL

    Discourse.cache.write(CACHE_KEY, DB.query_single(sql, limit:).to_set)
  end

  def self.update_scores(max = DEFAULT_BATCH_SIZE)
    # score is
    # (total likes - 1) / (age in hours + 2) ^ gravity

    # 1. insert a new record if one does not exist (up to batch size)
    # 2. update recently created (up to batch size)
    # 3. update all top scoring topics (up to batch size)

    now = Time.zone.now

    args = {
      now: now,
      gravity: SiteSetting.hot_topics_gravity,
      max: max,
      private_message: Archetype.private_message,
      recent_cutoff: now - SiteSetting.hot_topics_recent_days.days,
      regular: Post.types[:regular],
    }

    # insert up to BATCH_SIZE records that are missing from table
    DB.exec(<<~SQL, args)
      INSERT INTO topic_hot_scores (
        topic_id,
        score,
        recent_likes,
        recent_posters,
        created_at,
        updated_at
      )
      SELECT
        topics.id,
        0.0,
        0,
        0,
        :now,
        :now

      FROM topics
      LEFT OUTER JOIN topic_hot_scores ON topic_hot_scores.topic_id = topics.id
      WHERE topic_hot_scores.topic_id IS NULL
        AND topics.deleted_at IS NULL
        AND topics.archetype <> :private_message
        AND topics.created_at <= :now
        AND topics.visible = true
      ORDER BY
        CASE WHEN topics.pinned_at IS NOT NULL THEN 0 ELSE 1 END ASC,
        topics.bumped_at desc
      LIMIT :max
    SQL

    # update recent counts for batch
    DB.exec(<<~SQL, args)
      UPDATE topic_hot_scores thsOrig
      SET
          recent_likes = COALESCE(new_values.likes_count, 0),
          recent_posters = COALESCE(new_values.unique_participants, 0),
          recent_first_bumped_at = COALESCE(new_values.first_bumped_at, ths.recent_first_bumped_at)
      FROM
        topic_hot_scores ths
        LEFT OUTER JOIN
        (
          SELECT
              t.id AS topic_id,
              COUNT(DISTINCT p.user_id) AS unique_participants,
              (
                SELECT COUNT(distinct pa.user_id)
                FROM post_actions pa
                JOIN posts p2 ON p2.id = pa.post_id
                WHERE p2.topic_id = t.id
                  AND p2.post_type = :regular
                  AND p2.deleted_at IS NULL
                  AND p2.user_deleted = false
                  AND pa.post_action_type_id = 2 -- action_type for 'like'
                  AND pa.created_at >= :recent_cutoff
                  AND pa.deleted_at IS NULL
              ) AS likes_count,
              MIN(p.created_at) AS first_bumped_at
          FROM
              topics t
          JOIN
              posts p ON t.id = p.topic_id
          WHERE
              p.created_at >= :recent_cutoff
              AND t.archetype <> 'private_message'
              AND t.deleted_at IS NULL
              AND t.visible = true
              AND p.deleted_at IS NULL
              AND p.user_deleted = false
              AND t.created_at <= :now
              AND t.bumped_at >= :recent_cutoff
              AND p.created_at < :now
              AND p.created_at >= :recent_cutoff
              AND p.post_type = :regular
          GROUP BY
              t.id
        ) AS new_values
      ON ths.topic_id = new_values.topic_id

      WHERE thsOrig.topic_id = ths.topic_id
    SQL

    # we may end up update 2x batch size, this is ok
    # we need to update 1 batch of high scoring topics
    # we need to update a second batch of recently bumped topics
    sql = <<~SQL
      WITH topic_ids AS (
        SELECT topic_id FROM (
          SELECT th3.topic_id FROM topic_hot_scores th3
          JOIN topics t3 on t3.id = th3.topic_id
          ORDER BY t3.bumped_at DESC
          LIMIT :max
        ) Y

        UNION ALL

        SELECT topic_id FROM (
          SELECT th2.topic_id FROM topic_hot_scores th2
          ORDER BY th2.score DESC, th2.recent_first_bumped_at DESC NULLS LAST
          LIMIT :max
        ) X
      )
      UPDATE topic_hot_scores ths
      SET score = CASE WHEN topics.visible = false THEN 0 ELSE
        ((
          CASE WHEN topics.created_at > :recent_cutoff
            THEN ths.recent_likes ELSE topics.like_count END
          ) /
          (EXTRACT(EPOCH FROM (:now - topics.created_at)) / 3600 + 2) ^ :gravity
   +
          CASE WHEN ths.recent_first_bumped_at IS NULL THEN 0 ELSE
            (ths.recent_likes + ths.recent_posters - 1) /
            (EXTRACT(EPOCH FROM (:now - recent_first_bumped_at)) / 3600 + 2) ^ :gravity
          END
        ) * CASE WHEN topics.closed THEN 0.5 ELSE 1 END
        END,
        updated_at = :now

      FROM topics
      WHERE topics.id IN (
        SELECT topic_id FROM topic_ids
      ) AND ths.topic_id = topics.id AND topics.created_at <= :now
    SQL

    DB.exec(sql, args)

    recreate_hottest_topic_ids_cache

    DiscourseEvent.trigger(:topic_hot_scores_updated)
  end
end

# == Schema Information
#
# Table name: topic_hot_scores
#
#  id                     :bigint           not null, primary key
#  topic_id               :integer          not null
#  score                  :float            default(0.0), not null
#  recent_likes           :integer          default(0), not null
#  recent_posters         :integer          default(0), not null
#  recent_first_bumped_at :datetime
#  created_at             :datetime         not null
#  updated_at             :datetime         not null
#
# Indexes
#
#  index_topic_hot_scores_on_score_and_topic_id  (score,topic_id) UNIQUE
#  index_topic_hot_scores_on_topic_id            (topic_id) UNIQUE
#
