module Follows
  class UpdatePointsWorker
    include Sidekiq::Job
    sidekiq_options queue: :low_priority, retry: 10, lock: :until_executing

    def perform(article_id, user_id)
      article = Article.find_by(id: article_id)
      user = User.find_by(id: user_id)
      return unless article && user

      adjust_other_tag_follows_of_user(user.id)
      followed_tag_names =
        user.cached_followed_tag_names + user.cached_antifollowed_tag_names
      article.tag_list.each do |tag_name|
        if followed_tag_names.include?(tag_name)
          recalculate_tag_follow_points(tag_name, user)
        end
      end
    end

    def recalculate_tag_follow_points(tag_name, user)
      tag = Tag.find_by(name: tag_name)
      follow = Follow.follower_tag(user.id).where(followable_id: tag.id).last

      follow.implicit_points = calculate_implicit_points(tag, user)
      follow.save
    end

    def calculate_implicit_points(tag, user)
      last_100_reactable_ids = user.reactions.where(reactable_type: "Article", points: 0..)
        .pluck(:reactable_id).last(100)
      last_100_long_page_view_article_ids = user.page_views.where(time_tracked_in_seconds: 45..)
        .pluck(:article_id).last(100)
      articles = Article.where(id: last_100_reactable_ids + last_100_long_page_view_article_ids)
      tags = articles.map(&:tag_list).flatten
      occurrences = tags.count(tag.name)
      bonus = inverse_popularity_bonus(tag)
      finalized_points(occurrences, bonus)
    end

    def adjust_other_tag_follows_of_user(user_id)
      # As we bump one follow up, we should also give a slight penalty
      # to other follows to ensure re-balancing of overall points
      # This will help stale tags fade after a temporary interest bump

      # 0.98 is used to ensure this is "a very small amount"
      # And percentage seems to makes sense, as it will be relative to
      # size of current points.

      # That number could be adjusted at any point if we have reason to
      # believe it is too much or too little.
      Follow.follower_tag(user_id).order(Arel.sql("RANDOM()")).limit(5).each do |follow|
        follow.update_column(:points, (follow.points * 0.98))
      end
    end

    def inverse_popularity_bonus(tag)
      # Let's give a bonus to "less popular" tags on the platform
      # To help balance the weight of popular topic.
      # On DEV, javascript has way more taggings than rust, for example, so we can
      # help rust outweigh JS in this calculation slightly.
      # The bonus will be applied to the logarithmic scale, as to blunt any outsized impact.
      top_100_tag_names = cached_app_wide_top_tag_names
      top_100_tag_names.index(tag.name) || (top_100_tag_names.size * 1.5)
    end

    def finalized_points(occurrences, bonus)
      Math.log(occurrences + bonus + 1) # + 1 in all cases is to avoid log(0) => -infinity
    end

    def cached_app_wide_top_tag_names
      Rails.cache.fetch("top-100-tags") do
        Tag.order(hotness_score: :desc).limit(100).pluck(:name)
      end
    end
  end
end
