# frozen_string_literal: true

# name: discourse-ai
# about: Enables integration between AI modules and features in Discourse
# meta_topic_id: 259214
# version: 0.0.1
# authors: Discourse
# url: https://github.com/discourse/discourse/tree/main/plugins/discourse-ai

require "tokenizers"
require "tiktoken_ruby"
require "discourse_ai/tokenizers"
require "ed25519"
require "smarter_json"
require "json_completer"

enabled_site_setting :discourse_ai_enabled

register_asset "stylesheets/common/streaming.scss"
register_asset "stylesheets/common/ai-blinking-animation.scss"
register_asset "stylesheets/common/ai-user-settings.scss"
register_asset "stylesheets/common/ai-features.scss"
register_asset "stylesheets/common/ai-payload-viewer.scss"
register_asset "stylesheets/common/ai-decoded-transcript.scss"

register_asset "stylesheets/admin/ai-features-editor.scss", :admin
register_asset "stylesheets/admin/ai-logs.scss", :admin

register_asset "stylesheets/modules/translation/admin/translations.scss", :admin

register_asset "stylesheets/modules/ai-helper/common/ai-helper.scss"
register_asset "stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss", :desktop
register_asset "stylesheets/modules/ai-helper/mobile/ai-helper.scss", :mobile

register_asset "stylesheets/modules/summarization/common/ai-summary.scss"
register_asset "stylesheets/modules/summarization/desktop/ai-summary.scss", :desktop

register_asset "stylesheets/modules/summarization/common/ai-gists.scss"

register_asset "stylesheets/modules/admin-dashboard/common/admin-dashboard-highlight.scss"
register_asset "stylesheets/modules/ai-bot/common/bot-replies.scss"
register_asset "stylesheets/modules/ai-bot/common/ai-agent.scss"
register_asset "stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss"
register_asset "stylesheets/modules/ai-bot/mobile/ai-agent.scss", :mobile

register_asset "stylesheets/modules/ai-bot-conversations/common.scss"
register_asset "stylesheets/modules/ai-bot-conversations/docked-composer.scss"

register_asset "stylesheets/modules/embeddings/common/semantic-related-topics.scss"
register_asset "stylesheets/modules/embeddings/common/semantic-search.scss"

register_asset "stylesheets/modules/sentiment/common/dashboard.scss"

register_asset "stylesheets/modules/llms/common/ai-llms-editor.scss"
register_asset "stylesheets/modules/llms/common/ai-secret-selector.scss"
register_asset "stylesheets/modules/embeddings/common/ai-embedding-editor.scss"

register_asset "stylesheets/modules/llms/common/usage.scss"
register_asset "stylesheets/modules/llms/common/spam.scss"
register_asset "stylesheets/modules/llms/common/ai-llm-quotas.scss"
register_asset "stylesheets/modules/llms/common/ai-credit-bar.scss"

register_asset "stylesheets/modules/ai-bot/common/ai-tools.scss"

register_asset "stylesheets/modules/ai-bot/common/ai-artifact.scss"
register_asset "stylesheets/modules/ai-bot/common/ai-tool-approval.scss"

module ::DiscourseAi
  PLUGIN_NAME = "discourse-ai"

  def self.public_asset_path(name)
    File.expand_path(File.join(__dir__, "public", name))
  end
end

Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: DiscourseAi)

require_relative "lib/engine"
require_relative "lib/configuration/module"
require_relative "lib/mcp/oauth_token_store"
require_relative "lib/mcp/oauth_discovery"
require_relative "lib/mcp/oauth_client_registration"
require_relative "lib/mcp/oauth_flow"

# Other plugins can register features through this register.
DiscoursePluginRegistry.define_filtered_register(:external_ai_features)

DiscourseAi::Configuration::Module::NAMES.each do |module_name|
  register_site_setting_area("ai-features/#{module_name}")
end

after_initialize do
  register_admin_dashboard_section(
    id: "ask_ai",
    enabled: -> { SiteSetting.ai_ask_ai_enabled },
  ) do |start_date:, end_date:, current_user:|
    DiscourseAi::AdminDashboard::AskAi.build(start_date:, end_date:, current_user:)
  end

  register_modifier(:site_setting_result) do |setting_result|
    if setting_result[:setting] == :ai_discover_enabled && !SiteSetting.ai_discover_enabled
      setting_result[:disabled] = true
    end
    setting_result
  end

  if defined?(Rack::MiniProfiler)
    Rack::MiniProfiler.config.skip_paths << "/discourse-ai/ai-bot/artifacts"
  end

  # Avoid a mini_sql warning ("no type cast defined") by registering a halfvec text decoder.
  if !GlobalSetting.skip_db?
    if halfvec_oid = DB.query_single("SELECT oid FROM pg_type WHERE typname = 'halfvec'").first
      DB.type_map.add_coder(PG::TextDecoder::String.new(oid: halfvec_oid))
    end
  end

  # do not autoload this cause we may have no namespace
  require_relative "discourse_automation/llm_triage"
  require_relative "discourse_automation/llm_report"
  require_relative "discourse_automation/ai_tool_action"
  require_relative "discourse_automation/llm_agent_triage"
  require_relative "discourse_automation/llm_tagger"

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

  add_admin_route("discourse_ai.title", "discourse-ai", { use_new_show_route: true })

  register_seedfu_fixtures(Rails.root.join("plugins/discourse-ai/db/fixtures/agents"))

  [
    DiscourseAi::Embeddings::EntryPoint.new,
    DiscourseAi::Sentiment::EntryPoint.new,
    DiscourseAi::AiHelper::EntryPoint.new,
    DiscourseAi::Summarization::EntryPoint.new,
    DiscourseAi::AiBot::EntryPoint.new,
    DiscourseAi::AiModeration::EntryPoint.new,
    DiscourseAi::Translation::EntryPoint.new,
    DiscourseAi::Discover::EntryPoint.new,
    DiscourseAi::Discoveries::EntryPoint.new,
  ].each { |a_module| a_module.inject_into(self) }

  register_problem_check ProblemCheck::AiLlmStatus
  register_problem_check ProblemCheck::AiLlmVisionDelegation
  register_problem_check ProblemCheck::AiImageCaptionAgent

  register_reviewable_type ReviewableAiChatMessage
  register_reviewable_type ReviewableAiPost
  register_reviewable_type ReviewableAiToolAction
  add_permitted_reviewable_param :reviewable_ai_tool_action, :post_id

  on(:reviewable_transitioned_to) do |new_status, reviewable|
    ModelAccuracy.adjust_model_accuracy(new_status, reviewable)
    if DiscourseAi::AiModeration::SpamScanner.enabled?
      DiscourseAi::AiModeration::SpamMetric.update(new_status, reviewable)
    end
  end

  on(:user_destroyed) do |user|
    DiscourseAi::AiApiAuditLogCleaner.delete_for_user(user.id)
    DiscourseAi::Discoveries.clear_recent_asks(user_id: user.id)
    AiAgent.detach_user!(user.id)
  end

  register_user_destroyer_on_content_deletion_callback(
    Proc.new { |user| DiscourseAi::AiApiAuditLogCleaner.delete_for_user_content(user) },
  )

  on(:post_destroyed) do |post|
    if !Post.with_deleted.exists?(post.id)
      DiscourseAi::AiApiAuditLogCleaner.delete_for_post(post.id)
      DiscourseAi::PostImageCaptions.delete_for_post(post.id)
    end
  end

  on(:topic_destroyed) do |topic|
    if !Topic.with_deleted.exists?(topic.id)
      DiscourseAi::AiApiAuditLogCleaner.delete_for_topic(topic.id)
    end
  end

  if Rails.env.test?
    require_relative "spec/support/embeddings_generation_stubs"
    require_relative "spec/support/fake_external_agent"
  end

  reloadable_patch do |plugin|
    Guardian.prepend DiscourseAi::GuardianExtensions
    Topic.prepend DiscourseAi::TopicExtensions
    Post.prepend DiscourseAi::PostExtensions
  end

  # AI bots reply via `skip_guardian: true`, so the reachability warning is misleading.
  register_modifier(:composer_mention_user_reason) do |reason, user|
    DiscourseAi::AiBot::EntryPoint.all_bot_ids.include?(user.id) ? nil : reason
  end

  register_modifier(:post_should_secure_uploads?) do |_, _, topic|
    if topic.private_message? && SharedAiConversation.exists?(target: topic)
      false
    else
      # revert to default behavior
      # even though this can be shortened this is the clearest way to express it
      nil
    end
  end

  on(:post_process_cooked) do |doc, post|
    DiscourseAi::PostImageCaptions.process_cooked(
      doc,
      post,
      locale: DiscourseAi::PostImageCaptions.original_locale(post),
    )
  end

  on(:post_process_localized_cooked) do |doc, post, post_localization|
    DiscourseAi::PostImageCaptions.process_cooked(doc, post, locale: post_localization.locale)
  end

  register_modifier(:post_search_index_text) do |text, post_id, cooked, locale|
    DiscourseAi::PostImageCaptions.append_to_search_text(text, post_id, cooked, locale: locale)
  end

  on(:reduce_cooked) do |doc, _post|
    DiscourseAi::PostImageCaptions.remove_existing_caption_metadata(doc)
  end

  add_api_key_scope(:ai, { update_agents: { actions: %w[discourse_ai/admin/ai_agents#update] } })

  add_api_key_scope(
    :ai,
    {
      manage_artifacts: {
        actions: %w[
          discourse_ai/admin/ai_artifacts#index
          discourse_ai/admin/ai_artifacts#show
          discourse_ai/admin/ai_artifacts#create
          discourse_ai/admin/ai_artifacts#update
          discourse_ai/admin/ai_artifacts#destroy
        ],
      },
    },
  )

  plugin_icons = %w[
    chart-column
    spell-check
    language
    images
    far-copy
    robot
    info
    bars-staggered
    far-circle-question
    face-smile
    face-meh
    face-angry
    circle-info
    discourse-ai
  ]
  plugin_icons.each { |icon| register_svg_icon(icon) }

  add_model_callback(DiscourseAutomation::Automation, :after_save) do
    DiscourseAi::Configuration::Feature.feature_cache.flush!
  end

  add_model_callback(AiAgent, :after_commit, on: %i[create update]) do
    if saved_change_to_user_id?
      DiscourseAi::AiBot::UserFlair.sync_user_ids!(saved_change_to_user_id)
    end
  end

  add_model_callback(LlmModel, :after_commit, on: %i[create update]) do
    if saved_change_to_user_id?
      DiscourseAi::AiBot::UserFlair.sync_user_ids!(saved_change_to_user_id)
    end
  end

  add_custom_reviewable_filter(
    [
      :ai_triage_automation_id,
      Proc.new do |results, value|
        context = "#{DiscourseAi::Automation::TRIAGE_AUTOMATION_SCORE_CONTEXT_PREFIX}%"
        if value != :all
          automation_id = value.is_a?(Integer) ? value : value.to_s[/\A\d+\z/]&.to_i
          next results if !automation_id || automation_id <= 0

          context = DiscourseAi::Automation.triage_automation_score_context(automation_id)
        end

        results.where(<<~SQL, context:)
            EXISTS (
              SELECT 1
              FROM reviewable_scores
              WHERE reviewable_scores.reviewable_id = reviewables.id
              AND reviewable_scores.context LIKE :context
            )
          SQL
      end,
    ],
    type_filter: {
      id: "discourse_ai:triage",
      value: :all,
    },
    reason_filters: -> do
      DiscourseAutomation::Automation
        .where(script: %w[llm_triage llm_agent_triage])
        .order(:name)
        .pluck(:id, :name)
        .map { |id, name| { id: "ai_triage_automation:#{id}", name:, value: id } }
    end,
  )
end
