# frozen_string_literal: true

rails_version = Rails.respond_to?(:version) ? Rails.version : "unknown"
Appsignal.internal_logger.debug("Loading Rails (#{rails_version}) integration")

require "appsignal/utils/rails_helper"
require "appsignal/rack/rails_instrumentation"

module Appsignal
  module Integrations
    # @!visibility private
    class Railtie < ::Rails::Railtie
      config.appsignal = ActiveSupport::OrderedOptions.new
      config.appsignal.start_at = :on_load

      # Run after the Rails framework is loaded
      initializer "appsignal.configure_rails_initialization" do |app|
        Appsignal::Integrations::Railtie.on_load(app)
      end

      # Run after the Rails app's initializers are run
      config.after_initialize do |app|
        Appsignal::Integrations::Railtie.after_initialize(app)
      end

      console do
        Appsignal::Probes.stop
      end

      def self.on_load(app)
        load_default_config

        Appsignal::Integrations::Railtie.start if app.config.appsignal.start_at == :on_load

        # If AppSignal is supposed to start after initialization, we need to add the
        # instrumentation middleware now, even if AppSignal may not actually start
        # later, because the middleware stack will be frozen after initialization.
        #
        # Otherwise, only add the instrumentation middleware if AppSignal did actually
        # start before this point, preventing the middleware from being added when
        # AppSignal is not active.
        return unless app.config.appsignal.start_at == :after_initialize || Appsignal.started?

        add_instrumentation_middleware(app)
      end

      def self.after_initialize(app)
        Appsignal::Integrations::Railtie.start if app.config.appsignal.start_at == :after_initialize
      end

      def self.load_default_config
        Appsignal::Config.add_loader_defaults(
          :rails,
          :root_path => Rails.root,
          :env => Rails.env,
          :name => Appsignal::Utils::RailsHelper.detected_rails_app_name,
          :log_path => Rails.root.join("log"),
          :ignore_actions => ["Rails::HealthController#show"]
        )
      end

      def self.start
        Appsignal.start
        return unless Appsignal.started?

        initialize_error_reporter
      end

      def self.add_instrumentation_middleware(app)
        app.middleware.insert(
          0,
          ::Appsignal::Rack::EventMiddleware
        )
        app.middleware.insert_after(
          ActionDispatch::DebugExceptions,
          Appsignal::Rack::RailsInstrumentation
        )
      end

      def self.initialize_error_reporter
        return unless Appsignal.config[:enable_rails_error_reporter]
        return unless Rails.respond_to?(:error)

        Rails.error.subscribe(Appsignal::Integrations::RailsErrorReporterSubscriber)
      end
    end

    # Report errors reported by the Rails error reporter using {Appsignal.report_error}.
    #
    # @!visibility private
    class RailsErrorReporterSubscriber
      class << self
        def report(error, handled:, severity:, context: {}, source: nil) # rubocop:disable Lint/UnusedMethodArgument
          return if ignored_error?(error)

          is_rails_runner = source == "application.runner.railties"
          namespace, action_name, tags, custom_data = context_for(context.dup)

          Appsignal.report_error(error) do |transaction|
            if namespace
              transaction.set_namespace(namespace)
            elsif is_rails_runner
              transaction.set_namespace("runner")
            end
            transaction.set_action(action_name) if action_name
            transaction.add_custom_data(custom_data) if custom_data

            tags[:reported_by] = :rails_error_reporter
            tags[:severity] = severity
            tags[:source] = source.to_s if source
            transaction.add_tags(tags)
          end
        end

        private

        def ignored_error?(error)
          # We don't need to alert about Sidekiq job internal errors.
          defined?(Sidekiq::JobRetry::Handled) && error.is_a?(Sidekiq::JobRetry::Handled)
        end

        def context_for(context)
          tags = {}

          appsignal_context = context.delete(:appsignal)

          # Use the user override set in the context
          if appsignal_context
            context_namespace = appsignal_context[:namespace]
            namespace = context_namespace if context_namespace

            context_action_name = appsignal_context[:action]
            action_name = context_action_name if context_action_name

            context_custom_data = appsignal_context[:custom_data]
            custom_data = context_custom_data if context_custom_data
          end
          tags.merge!(context)

          [namespace, action_name, tags, custom_data]
        end
      end
    end
  end
end
