module Articles
  module Feeds
    # The default number of days old that an article can be for us
    # to consider it in the relevance feed.
    #
    # @note I believe that it is likely we would extract this constant
    #       into an administrative setting.  Hence, I want to keep it
    #       a scalar.
    DEFAULT_DAYS_SINCE_PUBLISHED = 7

    # @note I believe that it is likely we would extract this constant
    #       into an administrative setting.  Hence, I want to keep it
    #       a scalar to ease the implementation details of the admin
    #       setting.
    NUMBER_OF_HOURS_TO_OFFSET_USERS_LATEST_ARTICLE_VIEWS =
      (ApplicationConfig["NUMBER_OF_HOURS_TO_OFFSET_USERS_LATEST_ARTICLE_VIEWS"] || 18).to_i

    DEFAULT_USER_EXPERIENCE_LEVEL = 5
    DEFAULT_NEGATIVE_REACTION_THRESHOLD = -10
    DEFAULT_POSITIVE_REACTION_THRESHOLD = 10

    # @api private
    #
    # This method helps answer the question: What are the articles
    # that I should consider as new for the given user?  This method
    # provides a date by which to filter out "stale to the user"
    # articles.
    #
    # @note Do we need to continue using this method?  It's part of
    #       the hot story grab experiment that we ran with the
    #       Article::Feeds::LargeForemExperimental, but may not be
    #       relevant.
    #
    # @param user [User]
    # @param days_since_published [Integer] if someone
    #        hasn't viewed any articles, give them things from the
    #        database seeds.
    #
    # @return [ActiveSupport::TimeWithZone]
    #
    # @note the days_since_published is something carried
    #       over from the LargeForemExperimental and may not be
    #       relevant given that we have the :daily_decay.
    #       However, this further limitation based on a user's
    #       second most recent page view helps further winnow down
    #       the result set.
    def self.oldest_published_at_to_consider_for(user:, days_since_published: DEFAULT_DAYS_SINCE_PUBLISHED)
      time_of_second_latest_page_view = user&.page_views&.second_to_last&.created_at
      return days_since_published.days.ago unless time_of_second_latest_page_view

      # If they have a page view *well in the past*, let's go max 18 days look back.
      time_of_second_latest_page_view = 18.days.ago if time_of_second_latest_page_view < 18.days.ago
      time_of_second_latest_page_view - number_of_hours_to_offset_users_latest_article_views.hours
    end

    # Get the properly configured feed for the given user (and other parameters).
    #
    # @param controller [ApplicationController] used to retrieve the field_test variant
    # @param user [User, NilClass] used to retrieve the variant and how we query the articles
    # @param number_of_articles [Integer] the pagination page size
    # @param page [Integer] the page on which to start pagination
    # @param tag [NilClass, String] not used but carried forward for interface conformance
    #
    # @return [Articles::Feeds::VariantQuery]
    def self.feed_for(controller:, user:, number_of_articles:, page:, tag:, type_of: "discover")
      variant = AbExperiment.get_feed_variant_for(controller: controller, user: user)

      VariantQuery.build_for(
        variant: variant,
        user: user,
        number_of_articles: number_of_articles,
        page: page,
        tag: tag,
        type_of: type_of
      )
    end

    # The available feed levers for this Forem instance.
    #
    # @return [Articles::Feeds::LeverCatalogBuilder]
    def self.lever_catalog
      LEVER_CATALOG
    end

    def self.number_of_hours_to_offset_users_latest_article_views
      (ApplicationConfig["NUMBER_OF_HOURS_TO_OFFSET_USERS_LATEST_ARTICLE_VIEWS"] || 18).to_i
    end

    # rubocop:disable Metrics/BlockLength
    # The available levers for this forem instance.
    LEVER_CATALOG = LeverCatalogBuilder.new do
      order_by_lever(:relevancy_score_and_publication_date,
                     label: "Order by highest calculated relevancy score then latest published at time.",
                     order_by_fragment: "article_relevancies.relevancy_score DESC, articles.published_at DESC")

      order_by_lever(:final_order_by_random_weighted_to_score,
                     label: "Order by conflating a random number and the score (see forem/forem#16128)",
                     order_by_fragment: "article_relevancies.randomized_value " \
                                        "^ (1.0 / greatest(articles.score, 0.1)) DESC")
      order_by_lever(:final_order_by_feed_success_score,
                     label: "Order by feed success score",
                     order_by_fragment: "articles.feed_success_score DESC")
      order_by_lever(:final_order_by_feed_success_score_minus_clickbait_score,
                     label: "Order by feed success score minus clickbait score",
                     order_by_fragment: "articles.feed_success_score - articles.clickbait_score DESC")
      order_by_lever(:final_order_by_feed_success_score_minus_half_of_clickbait_score,
                     label: "Order by feed success score minus half of clickbait score",
                     order_by_fragment: "articles.feed_success_score - (articles.clickbait_score / 2) DESC")
      order_by_lever(:final_order_by_feed_success_score_minus_one_tenth_of_clickbait_score,
                     label: "Order by feed success score minus one tenth of clickbait score",
                     order_by_fragment: "articles.feed_success_score - (articles.clickbait_score / 10) DESC")
      order_by_lever(:final_order_by_feed_success_score_minus_clickbait_score_with_randomness,
                     label: "Order by feed success score minus clickbait score with a randomization factor",
                     order_by_fragment:
                      "(articles.feed_success_score - articles.clickbait_score) *
                      article_relevancies.randomized_value DESC")
      order_by_lever(:final_order_by_feed_success_score_minus_half_of_clickbait_score_with_small_randomness,
                     label: "Order by feed success score minus half of clickbait score with a randomization factor",
                     order_by_fragment:
                       "(articles.feed_success_score - (articles.clickbait_score / 2)) +
                       (article_relevancies.randomized_value / 3) DESC")
      order_by_lever(:final_order_by_feed_success_score_and_primary_score,
                     label: "Order by feed success score and primary score",
                     order_by_fragment: "((articles.feed_success_score + 0.01) * (articles.score / 10)) DESC")

      order_by_lever(:final_order_by_feed_success_score_and_log_of_primary_score,
                     label: "Order by feed success score and log of primary score",
                     order_by_fragment: "((feed_success_score + 0.01) * LOG(GREATEST(score, 1))) DESC")

      order_by_lever(:final_order_by_feed_success_score_and_log_of_comment_score,
                     label: "Order by feed success score and log of comment_score",
                     order_by_fragment: "((feed_success_score + 0.01) * LOG(GREATEST(comment_score, 1))) DESC")

      order_by_lever(:published_at_with_randomization_favoring_public_reactions,
                     label: "Favor recent articles with more reactions, " \
                            "but apply randomness to mitigate stagnation.",
                     order_by_fragment: "(cast(extract(epoch FROM published_at) as integer)) * " \
                                        "(article_relevancies.randomized_value ^ (1.0 / " \
                                        "greatest(0.1, ln(1 + greatest(0, public_reactions_count))))) DESC")

      order_by_lever(:last_comment_at_with_randomization_favoring_public_reactions,
                     label: "Favor articles with recent comments and more reactions, " \
                            "but apply randomness to mitigate stagnation.",
                     order_by_fragment: "(cast(extract(epoch FROM last_comment_at) as integer)) * " \
                                        "(article_relevancies.randomized_value ^ (1.0 / " \
                                        "greatest(0.1, ln(1 + greatest(0, public_reactions_count))))) DESC")

      order_by_lever(:random_pick_of_which_date_to_use_with_randomization_favoring_public_reactions,
                     label: "Favor articles with recent comments or published at and more reactions, " \
                            "but apply randomness to mitigate stagnation.",
                     order_by_fragment: "(cast(extract(epoch FROM " \
                                        "(CASE WHEN RANDOM() > 0.5 THEN published_at ELSE last_comment_at END)) " \
                                        "as integer)) * " \
                                        "(article_relevancies.randomized_value ^ (1.0 / " \
                                        "greatest(0.1, ln(1 + greatest(0, public_reactions_count))))) DESC")

      relevancy_lever(:comments_count_by_those_followed,
                      label: "Weight to give for the number of comments on the article from other users" \
                             "that the given user follows.",
                      range: "[0..∞)",
                      user_required: true,
                      select_fragment: "COUNT(comments_by_followed.id)",
                      joins_fragments: ["LEFT OUTER JOIN follows AS followed_user
                                          ON articles.user_id = followed_user.followable_id
                                            AND followed_user.followable_type = 'User'
                                            AND followed_user.follower_id = :user_id
                                            AND followed_user.follower_type = 'User'",
                                        "LEFT OUTER JOIN comments AS comments_by_followed
                                          ON comments_by_followed.commentable_id = articles.id
                                            AND comments_by_followed.commentable_type = 'Article'
                                            AND followed_user.followable_id = comments_by_followed.user_id
                                            AND followed_user.followable_type = 'User'
                                            AND comments_by_followed.deleted = false
                                            AND comments_by_followed.created_at > :oldest_published_at"])

      relevancy_lever(:comments_count,
                      label: "Weight to give to the number of comments on the article.",
                      range: "[0..∞)",
                      user_required: false,
                      select_fragment: "articles.comments_count",
                      group_by_fragment: "articles.comments_count")

      relevancy_lever(:comments_score,
                      label: "Weight given based on sum of comment scores of an article.",
                      range: "[0..∞)",
                      user_required: false,
                      select_fragment: "SUM(
                        CASE
                          WHEN comments.score is null then 0
                          ELSE comments.score
                        END)",
                      joins_fragments: ["LEFT OUTER JOIN comments
                        ON comments.commentable_id = articles.id
                          AND comments.commentable_type = 'Article'
                          AND comments.deleted = false"])

      relevancy_lever(:daily_decay,
                      label: "Weight given based on the relative age of the article",
                      range: "[0..∞)",
                      user_required: true,
                      select_fragment: "(current_date - articles.published_at::date)",
                      group_by_fragment: "articles.published_at")

      relevancy_lever(:experience,
                      label: "Weight to give based on the difference between experience level of the " \
                             "article and given user.",
                      range: "[0..∞)",
                      user_required: true,
                      select_fragment: "ROUND(ABS(articles.experience_level_rating - (SELECT
                                          (CASE
                                             WHEN experience_level IS NULL THEN :default_user_experience_level
                                             ELSE experience_level END ) AS user_experience_level
                                          FROM users_settings WHERE users_settings.user_id = :user_id)))",
                      group_by_fragment: "articles.experience_level_rating",
                      query_parameter_names: [:default_user_experience_level])

      relevancy_lever(:featured_article,
                      label: "Weight to give for feature or unfeatured articles.  1 is featured.",
                      user_required: false,
                      range: "[0..1]",
                      select_fragment: "(CASE articles.featured WHEN true THEN 1 ELSE 0 END)",
                      group_by_fragment: "articles.featured")

      relevancy_lever(:following_author,
                      label: "Weight to give when the given user follows the article's author." \
                             "1 is followed, 0 is not followed.",
                      range: "[0..1]",
                      user_required: true,
                      select_fragment: "COUNT(followed_user.follower_id)",
                      joins_fragments: ["LEFT OUTER JOIN follows AS followed_user
                                          ON articles.user_id = followed_user.followable_id
                                            AND followed_user.followable_type = 'User'
                                            AND followed_user.follower_id = :user_id
                                            AND followed_user.follower_type = 'User'"])

      relevancy_lever(:following_org,
                      label: "Weight to give to the when the given user follows the article's organization." \
                             "1 is followed, 0 is not followed.",
                      range: "[0..1]",
                      user_required: true,
                      select_fragment: "COUNT(followed_org.follower_id)",
                      joins_fragments: ["LEFT OUTER JOIN follows AS followed_org
                                          ON articles.organization_id = followed_org.followable_id
                                            AND followed_org.followable_type = 'Organization'
                                            AND followed_org.follower_id = :user_id
                                            AND followed_org.follower_type = 'User'"])

      relevancy_lever(:latest_comment,
                      label: "Weight to give an article based on it's most recent comment.",
                      range: "[0..∞)",
                      user_required: false,
                      select_fragment: "(current_date - MAX(comments.created_at)::date)",
                      joins_fragments: ["LEFT OUTER JOIN comments
                                          ON comments.commentable_id = articles.id
                                            AND comments.commentable_type = 'Article'
                                            AND comments.deleted = false
                                            AND comments.created_at > :oldest_published_at"])

      relevancy_lever(:matching_negative_tags_intersection_count,
                      label: "Weight to give the number of intersecting tags of the article and " \
                             "user negative follows",
                      range: "[0..4]",
                      user_required: true,
                      select_fragment: "COUNT(negative_followed_tags.id)",
                      joins_fragments: ["LEFT OUTER JOIN taggings
                                         ON taggings.taggable_id = articles.id
                                           AND taggable_type = 'Article'",
                                        "INNER JOIN tags
                                         ON taggings.tag_id = tags.id",
                                        "LEFT OUTER JOIN follows AS negative_followed_tags
                                         ON tags.id = negative_followed_tags.followable_id
                                           AND negative_followed_tags.followable_type = 'ActsAsTaggableOn::Tag'
                                           AND negative_followed_tags.follower_type = 'User'
                                           AND negative_followed_tags.follower_id = :user_id
                                           AND negative_followed_tags.explicit_points < 0"])

      relevancy_lever(:matching_negative_tags_intersection_points,
                      label: "Weight to give for the sum points of the intersecting tags of the article and " \
                             "user positive follows.",
                      user_required: true,
                      range: "[-10..0]",
                      select_fragment: "LEAST(-10.0, SUM(followed_tags.points))::integer",
                      joins_fragments: ["LEFT OUTER JOIN taggings
                                         ON taggings.taggable_id = articles.id
                                           AND taggable_type = 'Article'",
                                        "INNER JOIN tags
                                         ON taggings.tag_id = tags.id",
                                        "LEFT OUTER JOIN follows AS followed_tags
                                         ON tags.id = followed_tags.followable_id
                                           AND followed_tags.followable_type = 'ActsAsTaggableOn::Tag'
                                           AND followed_tags.follower_type = 'User'
                                           AND followed_tags.follower_id = :user_id
                                           AND followed_tags.explicit_points < 0"])

      relevancy_lever(:matching_positive_tags_intersection_count,
                      label: "Weight to give for number of the intersecting tags of the article and " \
                             "user positive follows.",
                      range: "[0..4]",
                      user_required: true,
                      select_fragment: "COUNT(followed_tags.id)",
                      joins_fragments: ["LEFT OUTER JOIN taggings
                                         ON taggings.taggable_id = articles.id
                                           AND taggable_type = 'Article'",
                                        "INNER JOIN tags
                                         ON taggings.tag_id = tags.id",
                                        "LEFT OUTER JOIN follows AS followed_tags
                                         ON tags.id = followed_tags.followable_id
                                           AND followed_tags.followable_type = 'ActsAsTaggableOn::Tag'
                                           AND followed_tags.follower_type = 'User'
                                           AND followed_tags.follower_id = :user_id
                                           AND followed_tags.explicit_points >= 0"])

      relevancy_lever(:matching_positive_tags_intersection_points,
                      label: "Weight to give for the sum points of the intersecting tags of the article and " \
                             "user positive follows.",
                      user_required: true,
                      range: "[0..10]",
                      select_fragment: "LEAST(10.0, SUM(followed_tags.points))::integer",
                      joins_fragments: ["LEFT OUTER JOIN taggings
                                         ON taggings.taggable_id = articles.id
                                           AND taggable_type = 'Article'",
                                        "INNER JOIN tags
                                         ON taggings.tag_id = tags.id",
                                        "LEFT OUTER JOIN follows AS followed_tags
                                         ON tags.id = followed_tags.followable_id
                                           AND followed_tags.followable_type = 'ActsAsTaggableOn::Tag'
                                           AND followed_tags.follower_type = 'User'
                                           AND followed_tags.follower_id = :user_id
                                           AND followed_tags.explicit_points >= 0"])

      relevancy_lever(:privileged_user_reaction,
                      label: "-1 when privileged user reactions down-vote, 0 when netural, and 1 when positive.",
                      user_required: false,
                      range: "[-1..1]",
                      select_fragment: "(CASE
                 WHEN articles.privileged_users_reaction_points_sum < :negative_reaction_threshold THEN -1
                 WHEN articles.privileged_users_reaction_points_sum > :positive_reaction_threshold THEN 1
                 ELSE 0 END)",
                      group_by_fragment: "articles.privileged_users_reaction_points_sum",
                      query_parameter_names: %i[negative_reaction_threshold positive_reaction_threshold])

      # Note the symmetry of the < and >=; the smaller value is always "exclusive" and the larger value is "inclusive"
      relevancy_lever(:privileged_user_reaction_granular,
                      label: "A more granular configuration for privileged user reactions (see select_fragment)",
                      user_required: false,
                      range: "[-2..2]",
                      select_fragment: "(CASE
                 --- Very negative
                 WHEN articles.privileged_users_reaction_points_sum < :very_negative_reaction_threshold THEN -2
                 --- Negative
                 WHEN articles.privileged_users_reaction_points_sum >= :very_negative_reaction_threshold
                      AND articles.privileged_users_reaction_points_sum < :negative_reaction_threshold THEN -1
                 --- Neutral
                 WHEN articles.privileged_users_reaction_points_sum >= :negative_reaction_threshold
                      AND articles.privileged_users_reaction_points_sum < :positive_reaction_threshold THEN 0
                 --- Positive
                 WHEN articles.privileged_users_reaction_points_sum >= :positive_reaction_threshold
                      AND articles.privileged_users_reaction_points_sum < :very_positive_reaction_threshold THEN 1
                 --- Very Positive
                 WHEN articles.privileged_users_reaction_points_sum >= :very_positive_reaction_threshold THEN 2
                 ELSE 0 END)",
                      group_by_fragment: "articles.privileged_users_reaction_points_sum",
                      query_parameter_names: %i[
                        very_negative_reaction_threshold
                        negative_reaction_threshold
                        very_positive_reaction_threshold
                        positive_reaction_threshold
                      ])

      relevancy_lever(:public_reactions,
                      label: "Weight to give for the number of unicorn, heart, reading list reactions for article.",
                      range: "[0..∞)",
                      user_required: false,
                      select_fragment: "articles.public_reactions_count",
                      group_by_fragment: "articles.public_reactions_count")

      relevancy_lever(:public_reactions_score,
                      label: "Weight to give based on article.score (see article.update_score for this calculation -
                      it's a sum of the scores of reactions on an article).",
                      range: "[0..∞)",
                      user_required: false,
                      select_fragment: "articles.score",
                      group_by_fragment: "articles.score")

      relevancy_lever(:language_match,
                      label: "Weight to give based on whether the language matches any of the user's languages",
                      range: "[0..1]", # 0 for no match, 1 for match
                      user_required: true,
                      select_fragment: "CASE
                                         WHEN COUNT(user_languages.language) = 0 THEN 0
                                         WHEN articles.language = ANY(array_agg(user_languages.language)) THEN 1
                                         ELSE 0
                                        END",
                      joins_fragments: ["LEFT OUTER JOIN user_languages
                                         ON user_languages.user_id = :user_id"],
                      group_by_fragment: "articles.language")

      relevancy_lever(:recommended_articles_match,
                      label: "Weight to give based on whether the article is in the first non-expired recommendations",
                      range: "[0..1]", # 0 for no match, 1 for match
                      user_required: true,
                      select_fragment: "CASE
                                         WHEN COUNT(first_matching_list.id) = 0 THEN 0
                                         WHEN articles.id = ANY(array_agg(first_matching_list.article_ids)
                                         FILTER (WHERE first_matching_list.article_ids IS NOT NULL)) THEN 1
                                         ELSE 0
                                        END",
                      joins_fragments: ["LEFT OUTER JOIN
                                          (SELECT * FROM recommended_articles_lists
                                           WHERE expires_at > CURRENT_TIMESTAMP
                                           AND placement_area = 0
                                           ORDER BY created_at ASC
                                           LIMIT 1) AS first_matching_list
                                        ON first_matching_list.user_id = :user_id"],
                      group_by_fragment: "articles.id")
    end

    private_constant :LEVER_CATALOG
    # rubocop:enable Metrics/BlockLength
  end
end
