# Copyright (c) 2018 SolarWinds, LLC.
# All rights reserved.
#

AO_TRACING_ENABLED = 1
AO_TRACING_DISABLED = 0
AO_TRACING_UNSET = -1

AO_TRACING_DECISIONS_OK = 0

OBOE_SETTINGS_UNSET = -1

module AppOpticsAPM
  ##
  # This module helps with setting up the transaction filters and applying them
  #
  class TransactionSettings

    attr_accessor :do_sample, :do_metrics
    attr_reader   :auth_msg, :do_propagate, :status_msg, :type, :source, :rate, :xtrace
                  #, :status

    def initialize(url = '', xtrace = '', options = nil)
      @do_metrics = false
      @do_sample = false
      @do_propagate = true
      @xtrace = xtrace || ''
      tracing_mode = AO_TRACING_ENABLED

      if AppOpticsAPM::Context.isValid
        @do_sample = AppOpticsAPM.tracing?
        return
      end

      if url && asset?(url)
        @do_propagate = false
        return
      end

      if tracing_mode_disabled? && !tracing_enabled?(url) ||
        tracing_disabled?(url)

        tracing_mode = AO_TRACING_DISABLED
      end

      args = [@xtrace]
      args << tracing_mode
      args << (AppOpticsAPM::Config[:sample_rate] || OBOE_SETTINGS_UNSET)

      if options && (options.options || options.signature)
        args << (options.trigger_trace ? 1 : 0)
        args << (trigger_tracing_mode_disabled? ? 0 : 1)
        args << options.options
        args << options.signature
        args << options.timestamp
      end

      metrics, sample, @rate, @source, @bucket_rate, @bucket_cap, @type, @auth, @status_msg, @auth_msg, @status =
        AppOpticsAPM::Context.getDecisions(*args)

      if @status > AO_TRACING_DECISIONS_OK
        AppOpticsAPM.logger.warn "[appoptics-apm/sample] Problem getting the sampling decisions: #{@status_msg} code: #{@status}"
      end

      @do_metrics = metrics > 0
      @do_sample = sample > 0
    end

    def to_s
      "do_propagate: #{do_propagate}, do_sample: #{do_sample}, do_metrics: #{do_metrics} rate: #{rate}, source: #{source}"
    end

    def add_kvs(kvs)
      kvs[:SampleRate] = @rate
      kvs[:SampleSource] = @source
    end

    def triggered_trace?
      @type == 1
    end

    def auth_ok?
      # @auth is undefined if initialize is called with an existing context
      !@auth || @auth < 1
    end

    private

    ##
    # check the config setting for :tracing_mode
    def tracing_mode_disabled?
      AppOpticsAPM::Config[:tracing_mode] &&
        [:disabled, :never].include?(AppOpticsAPM::Config[:tracing_mode])
    end

    ##
    # tracing_enabled?
    #
    # Given a path, this method determines whether it matches any of the
    # regexps to exclude it from metrics and traces
    #
    def tracing_enabled?(url)
      return false unless AppOpticsAPM::Config[:url_enabled_regexps].is_a? Array
      # once we only support Ruby >= 2.4.0 use `match?` instead of `=~`
      return AppOpticsAPM::Config[:url_enabled_regexps].any? { |regex| regex =~ url }
    rescue => e
      AppOpticsAPM.logger.warn "[AppOpticsAPM/filter] Could not apply :enabled filter to path. #{e.inspect}"
      true
    end

    ##
    # tracing_disabled?
    #
    # Given a path, this method determines whether it matches any of the
    # regexps to exclude it from metrics and traces
    #
    def tracing_disabled?(url)
      return false unless AppOpticsAPM::Config[:url_disabled_regexps].is_a? Array
      # once we only support Ruby >= 2.4.0 use `match?` instead of `=~`
      return AppOpticsAPM::Config[:url_disabled_regexps].any? { |regex| regex =~ url }
    rescue => e
      AppOpticsAPM.logger.warn "[AppOpticsAPM/filter] Could not apply :disabled filter to path. #{e.inspect}"
      false
    end

    def trigger_tracing_mode_disabled?
      AppOpticsAPM::Config[:trigger_tracing_mode] &&
        AppOpticsAPM::Config[:trigger_tracing_mode] == :disabled
    end

    ##
    # asset?
    #
    # Given a path, this method determines whether it is a static asset
    #
    def asset?(path)
      return false unless AppOpticsAPM::Config[:dnt_compiled]
      # once we only support Ruby >= 2.4.0 use `match?` instead of `=~`
      return AppOpticsAPM::Config[:dnt_compiled] =~ path
    rescue => e
      AppOpticsAPM.logger.warn "[AppOpticsAPM/filter] Could not apply do-not-trace filter to path. #{e.inspect}"
      false
    end

    public

    class << self

      def asset?(path)
        return false unless AppOpticsAPM::Config[:dnt_compiled]
        # once we only support Ruby >= 2.4.0 use `match?` instead of `=~`
        return AppOpticsAPM::Config[:dnt_compiled] =~ path
      rescue => e
        AppOpticsAPM.logger.warn "[AppOpticsAPM/filter] Could not apply do-not-trace filter to path. #{e.inspect}"
        false
      end


      def compile_url_settings(settings)
        if !settings.is_a?(Array) || settings.empty?
          reset_url_regexps
          return
        end

        # `tracing: disabled` is the default
        disabled = settings.select { |v| !v.has_key?(:tracing) || v[:tracing] == :disabled }
        enabled = settings.select { |v| v[:tracing] == :enabled }

        AppOpticsAPM::Config[:url_enabled_regexps] = compile_regexp(enabled)
        AppOpticsAPM::Config[:url_disabled_regexps] = compile_regexp(disabled)
      end

      def compile_regexp(settings)
        regexp_regexp     = compile_url_settings_regexp(settings)
        extensions_regexp = compile_url_settings_extensions(settings)

        regexps = [regexp_regexp, extensions_regexp].flatten.compact

        regexps.empty? ? nil : regexps
      end

      def compile_url_settings_regexp(value)
        regexps = value.select do |v|
          v.key?(:regexp) &&
            !(v[:regexp].is_a?(String) && v[:regexp].empty?) &&
            !(v[:regexp].is_a?(Regexp) && v[:regexp].inspect == '//')
        end

        regexps.map! do |v|
          begin
            v[:regexp].is_a?(String) ? Regexp.new(v[:regexp], v[:opts]) : Regexp.new(v[:regexp])
          rescue
            AppOpticsAPM.logger.warn "[appoptics_apm/config] Problem compiling transaction_settings item #{v}, will ignore."
            nil
          end
        end
        regexps.keep_if { |v| !v.nil?}
        regexps.empty? ? nil : regexps
      end

      def compile_url_settings_extensions(value)
        extensions = value.select do |v|
          v.key?(:extensions) &&
            v[:extensions].is_a?(Array) &&
            !v[:extensions].empty?
        end
        extensions = extensions.map { |v| v[:extensions] }.flatten
        extensions.keep_if { |v| v.is_a?(String)}

        extensions.empty? ? nil : Regexp.new("(#{Regexp.union(extensions).source})(\\?.+){0,1}$")
      end

      def reset_url_regexps
        AppOpticsAPM::Config[:url_enabled_regexps] = nil
        AppOpticsAPM::Config[:url_disabled_regexps] = nil
      end
    end
  end
end