# frozen_string_literal: true

# name: discourse-topic-voting
# about: Adds the ability to vote on topics in a specified category.
# meta_topic_id: 40121
# version: 0.5
# author: Joe Buhlig joebuhlig.com, Sam Saffron
# url: https://github.com/discourse/discourse/tree/main/plugins/discourse-topic-voting

register_asset "stylesheets/common/topic-voting.scss"

register_svg_icon "check-to-slot"
register_svg_icon "vote-up"
register_svg_icon "vote-up-filled"

enabled_site_setting :topic_voting_enabled

Discourse.top_menu_items.push(:votes)
Discourse.anonymous_top_menu_items.push(:votes)
Discourse.filters.push(:votes)
Discourse.anonymous_filters.push(:votes)

module ::DiscourseTopicVoting
  PLUGIN_NAME = "discourse-topic-voting"
  ENABLE_TOPIC_VOTING_SETTING = "enable_topic_voting"
  VOTER_PREVIEW_LIMIT = 104
  BADGE_NAMES = %w[Daydreamer Brainstormer Innovator Visionary].freeze
end

require_relative "lib/discourse_topic_voting/badge_queries"
require_relative "lib/discourse_topic_voting/engine"
require_relative "lib/discourse_topic_voting/topic_votes_filter"

after_initialize do
  SeedFu.fixture_paths << Rails.root.join("plugins/discourse-topic-voting/db/fixtures").to_s

  if respond_to?(:register_discourse_workflows_node)
    register_discourse_workflows_node { DiscourseWorkflows::Nodes::TopicReceivedVote::V1 }
  end

  reloadable_patch do
    register_category_type(DiscourseTopicVoting::Categories::Types::Ideas)
    Category.prepend(DiscourseTopicVoting::CategoryExtension)
    ListController.prepend(DiscourseTopicVoting::ListControllerExtension)
    Topic.prepend(DiscourseTopicVoting::TopicExtension)
    TopicQuery.prepend(DiscourseTopicVoting::TopicQueryExtension)
    User.prepend(DiscourseTopicVoting::UserExtension)
    WebHook.prepend(DiscourseTopicVoting::WebHookExtension)
  end

  add_to_serializer(:post, :can_vote, include_condition: -> { object.post_number == 1 }) do
    object.topic&.can_vote?
  end

  add_to_serializer(:topic_view, :can_vote) { object.topic.can_vote? }
  add_to_serializer(:topic_view, :vote_count) { object.topic.vote_count }
  add_to_serializer(:topic_view, :user_voted) do
    scope.user ? object.topic.user_voted?(scope.user) : false
  end

  TopicQuery.results_filter_callbacks << ->(_type, result, user, options) do
    return result unless SiteSetting.topic_voting_enabled

    result = result.preload(:topic_vote_count)

    if user
      result =
        result.select(
          "topics.*, COALESCE((SELECT 1 FROM topic_voting_votes WHERE user_id = #{user.id} AND topic_id = topics.id), 0) AS current_user_voted",
        )

      if options[:state] == "my_votes"
        result =
          result.joins(
            "INNER JOIN topic_voting_votes ON topic_voting_votes.topic_id = topics.id AND topic_voting_votes.user_id = #{user.id} AND topic_voting_votes.archive = FALSE",
          )
      end
    end

    if options[:order] == "votes"
      sort_dir = (options[:ascending] == "true") ? "ASC" : "DESC"
      result =
        result.joins(
          "LEFT JOIN topic_voting_topic_vote_count ON topic_voting_topic_vote_count.topic_id = topics.id",
        ).reorder(
          "COALESCE(topic_voting_topic_vote_count.votes_count,'0')::integer #{sort_dir}, topics.bumped_at DESC",
        )
    end

    result
  end

  register_category_custom_field_type(DiscourseTopicVoting::ENABLE_TOPIC_VOTING_SETTING, :boolean)
  register_preloaded_category_custom_fields DiscourseTopicVoting::ENABLE_TOPIC_VOTING_SETTING

  add_to_serializer(:topic_list_item, :vote_count, include_condition: -> { object.can_vote? }) do
    object.vote_count
  end
  add_to_serializer(:topic_list_item, :can_vote, include_condition: -> { object.regular? }) do
    object.can_vote?
  end
  add_to_serializer(:topic_list_item, :user_voted, include_condition: -> { object.can_vote? }) do
    object.user_voted?(scope.user) if scope.user
  end
  add_to_serializer(
    :basic_category,
    :can_vote,
    include_condition: -> { Category.can_vote?(object.id) },
  ) { true }

  register_search_advanced_filter(/^min_vote_count:(\d+)$/) do |posts, match|
    posts.where(
      "(SELECT votes_count FROM topic_voting_topic_vote_count WHERE topic_voting_topic_vote_count.topic_id = posts.topic_id) >= ?",
      match.to_i,
    )
  end

  register_search_advanced_order(:votes) do |posts|
    posts.reorder(
      "COALESCE((SELECT dvtvc.votes_count FROM topic_voting_topic_vote_count dvtvc WHERE dvtvc.topic_id = topics.id), 0) DESC",
    )
  end

  register_modifier(:badge_granter_suppress_notification) do |suppress, badge, granted_at, _|
    next true if DiscourseTopicVoting::BADGE_NAMES.include?(badge.name) && granted_at < 2.weeks.ago
    suppress
  end

  register_modifier(:topics_filter_options) do |results, _guardian|
    results.concat(
      [
        {
          name: "votes-min:",
          description: I18n.t("topic_voting.filter.description.topic_votes_min"),
          type: "number",
        },
        {
          name: "votes-max:",
          description: I18n.t("topic_voting.filter.description.topic_votes_max"),
          type: "number",
        },
        {
          name: "order:votes",
          description: I18n.t("topic_voting.filter.description.order_topic_votes"),
        },
        {
          name: "order:votes-asc",
          description: I18n.t("topic_voting.filter.description.order_topic_votes_asc"),
        },
      ],
    )

    results
  end

  add_to_serializer(:current_user, :votes_exceeded) { object.reached_voting_limit? }
  add_to_serializer(:current_user, :votes_count) { object.vote_count }
  add_to_serializer(:current_user, :votes_left) { object.votes_left }
  add_to_serializer(:current_user, :vote_limit) { object.vote_limit }

  topic_votes_value_from = ->(values) do
    value = values&.last
    return if value.blank? || value !~ /\A\d+\z/

    value.to_i
  end

  add_filter_custom_filter("votes-min") do |scope, filter_values, _guardian|
    value = topic_votes_value_from.call(filter_values)
    next scope if value.nil?

    DiscourseTopicVoting::TopicVotesFilter.apply(scope, min_votes: value)
  end

  add_filter_custom_filter("votes-max") do |scope, filter_values, _guardian|
    value = topic_votes_value_from.call(filter_values)
    next scope if value.nil?

    DiscourseTopicVoting::TopicVotesFilter.apply(scope, max_votes: value)
  end

  add_filter_custom_filter("order:votes") do |scope, order_direction, _guardian|
    DiscourseTopicVoting::TopicVotesFilter.apply(scope, order_direction:)
  end

  on(:topic_status_updated) do |topic, status, enabled|
    next if topic.trashed?
    next if %w[closed autoclosed archived].exclude?(status)

    if enabled
      Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteRelease, topic_id: topic.id)
    else
      is_closing_unarchived = %w[closed autoclosed].include?(status) && !topic.archived
      is_archiving_open = status == "archived" && !topic.closed

      if is_closing_unarchived || is_archiving_open
        Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteReclaim, topic_id: topic.id)
      end
    end
  end

  on(:topic_trashed) do |topic|
    if !topic.closed && !topic.archived
      Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteRelease, topic_id: topic.id, trashed: true)
    end
  end

  on(:topic_recovered) do |topic|
    if !topic.closed && !topic.archived
      Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteReclaim, topic_id: topic.id)
    end
  end

  on(:post_edited) do |post, _, revisor|
    if SiteSetting.topic_voting_enabled && revisor.topic_diff.has_key?("category_id") &&
         DiscourseTopicVoting::Vote.exists?(topic_id: post.topic_id) && !post.topic.closed &&
         !post.topic.archived && !post.topic.trashed?
      new_category_id = post.reload.topic.category_id
      if Category.can_vote?(new_category_id)
        Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteReclaim, topic_id: post.topic_id)
      else
        Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteRelease, topic_id: post.topic_id)
      end
    end
  end

  on(:topic_merged) { |orig, dest| DiscourseTopicVoting::TopicMerger.merge(orig, dest) }

  on(:merging_users) do |source_user, target_user|
    DiscourseTopicVoting::UserMerger.merge(source_user, target_user)
  end

  Discourse::Application.routes.prepend do
    get "c/*category_slug_path_with_id/l/votes.rss" => "list#votes_feed", :format => :rss
  end

  Discourse::Application.routes.append do
    mount DiscourseTopicVoting::Engine, at: "/voting"

    get "topics/voted-by/:username" => "list#voted_by",
        :as => "voted_by",
        :constraints => {
          username: RouteFormat.username,
        }
  end

  register_anonymous_action("vote_topic") do |user, params|
    DiscourseTopicVoting::Votes::Cast.call(
      params: {
        topic_id: params["topic_id"],
      },
      guardian: user.guardian,
    )
  end
end

after_initialize do
  require_relative "lib/discourse_topic_voting/mcp_tools"
  register_mcp_tool(
    "discourse_topic_voting_vote_set",
    title: "Set topic vote",
    description: "Casts or removes the authenticated user's vote on a visible votable topic.",
    implementation: DiscourseTopicVoting::McpTools::SetVote,
    input_schema: {
      type: "object",
      properties: {
        topic_id: {
          type: "integer",
          minimum: 1,
        },
        voted: {
          type: "boolean",
        },
      },
      required: %w[topic_id voted],
      additionalProperties: false,
    },
    required_scopes: %w[discourse-topic-voting:write],
    annotations: {
      readOnlyHint: false,
      destructiveHint: false,
    },
    risk: :write,
    availability: -> { SiteSetting.topic_voting_enabled },
  )
end
