# encoding: utf-8

require 'ting_yun/agent/database'
require 'ting_yun/agent/transaction/transaction_state'
require 'ting_yun/support/coerce'
require 'ting_yun/metrics/stats'
require 'ting_yun/support/helper'
module TingYun
  module Agent
    module Collector
      # This class contains the logic of recording slow SQL traces, which may
      # represent multiple aggregated SQL queries.
      #
      # A slow SQL trace consists of a collection of SQL instrumented SQL queries
      # that all normalize to the same text. For example, the following two
      # queries would be aggregated together into a single slow SQL trace:
      #
      #   SELECT * FROM table WHERE id=42
      #   SELECT * FROM table WHERE id=1234
      #
      # Each slow SQL trace keeps track of the number of times the same normalized
      # query was seen, the min, max, and total time spent executing those
      # queries, and an example backtrace from one of the aggregated queries.
      #
      class SqlSampler

        MAX_SAMPLES = 10

        attr_reader :sql_traces

        def initialize
          @sql_traces = {}
          @samples_lock = Mutex.new
        end

        def enabled?
          Agent.config[:'nbs.action_tracer.enabled'] &&
              Agent.config[:'nbs.action_tracer.slow_sql'] &&
                  TingYun::Agent::Database.should_record_sql?('nbs.action_tracer.record_sql')
        end

        def on_start_transaction(state, uri=nil)
          return unless enabled?

          state.sql_sampler_transaction_data = TransactionSqlData.new

          if Agent.config[:'nbs.action_tracer.slow_sql'] && state.sql_sampler_transaction_data
            state.sql_sampler_transaction_data.set_transaction_info(uri)
          end
        end

        # duration{:type => sec}
        def notice_sql(sql, metric_name, config, duration, state=nil, explainer=nil) #THREAD_LOCAL_ACCESS sometimes
          start_time = Time.now.to_f
          state ||= TingYun::Agent::TransactionState.tl_get
          data = state.sql_sampler_transaction_data
          return unless data

          if state.sql_recorded? && !metric_name.nil?
            if duration*1000 > TingYun::Agent.config[:'nbs.action_tracer.slow_sql_threshold']
              if duration*1000 > TingYun::Agent.config[:'nbs.action_tracer.stack_trace_threshold']
                backtrace = (caller.reject! { |t| t.include?('tingyun_rpm') })
                backtrace = backtrace.first(40) if backtrace.length > 40
                backtrace = backtrace.join("\n")
              else
                backtrace = ''
              end
              statement = TingYun::Agent::Database::Statement.new(sql, config, explainer)
              data.sql_data << SlowSql.new(statement, metric_name, duration, start_time, backtrace)
            end
          end
        end


        def on_finishing_transaction(state, name)
          return unless enabled?

          transaction_sql_data = state.sql_sampler_transaction_data
          return unless transaction_sql_data

          transaction_sql_data.set_transaction_name(name)

          save_slow_sql(transaction_sql_data)
        end

        def save_slow_sql(data)
          if data.sql_data.size > 0
            @samples_lock.synchronize do
              ::TingYun::Agent.logger.debug "Examining #{data.sql_data.size} slow transaction sql statement(s)"
              save data
            end
          end
        end

        def save (transaction_sql_data)
          action_metric_name = transaction_sql_data.action_metric_name
          uri                = transaction_sql_data.uri

          transaction_sql_data.sql_data.each do |sql_item|
            normalized_sql = sql_item.normalize
            sql_trace = @sql_traces[normalized_sql]
            if sql_trace
              sql_trace.aggregate(sql_item, action_metric_name, uri)
            else
              if has_room?
                sql_trace = SqlTrace.new(normalized_sql, sql_item, action_metric_name, uri)
              elsif should_add_trace?(sql_item)
                remove_shortest_trace
                sql_trace = SqlTrace.new(normalized_sql, sql_item, action_metric_name, uri)
              end

              if sql_trace
                @sql_traces[normalized_sql] = sql_trace
              end

            end
          end
        end

        # this should always be called under the @samples_lock
        def has_room?
          @sql_traces.size < MAX_SAMPLES
        end

        # this should always be called under the @samples_lock
        def should_add_trace?(sql_item)
          @sql_traces.any? do |(_, existing_trace)|
            existing_trace.max_call_time < sql_item.duration
          end
        end

        # this should always be called under the @samples_lock
        def remove_shortest_trace
          shortest_key, _ = @sql_traces.min_by { |(_, trace)| trace.max_call_time }
          @sql_traces.delete(shortest_key)
        end


        def harvest!
          return [] unless enabled?

          slowest = []
          @samples_lock.synchronize do
            slowest = @sql_traces.values
            @sql_traces = {}
          end
          slowest.each {|trace| trace.prepare_to_send }
          slowest
        end

        def reset!
          @samples_lock.synchronize do
            @sql_traces = {}
          end
        end

        def merge!
          @samples_lock.synchronize do
            sql_traces.each do |trace|
              existing_trace = @sql_traces[trace.sql]
              if existing_trace
                existing_trace.aggregate_trace(trace)
              else
                @sql_traces[trace.sql] = trace
              end
            end
          end
        end


      end

      class TransactionSqlData
        attr_reader :action_metric_name
        attr_reader :uri
        attr_reader :sql_data

        def initialize
          @sql_data = []
        end

        def set_transaction_info(uri)
          @uri = uri
        end

        def set_transaction_name(name)
          @action_metric_name = name
        end
      end

      class SlowSql
        attr_reader :statement
        attr_reader :metric_name
        attr_reader :duration
        attr_reader :backtrace
        attr_reader :start_time


        def initialize(statement, metric_name, duration, t0,  backtrace=nil)
          @start_time = t0
          @statement = statement
          @metric_name = metric_name
          @duration = duration
          @backtrace = backtrace
        end

        def sql
          statement.sql
        end

        def obfuscate
          TingYun::Agent::Database.obfuscate_sql(statement)
        end


        def normalize
          TingYun::Agent::Database::Obfuscator.instance.default_sql_obfuscator(statement)
        end

        def explain
          if statement.config && statement.explainer
            TingYun::Agent::Database.explain_sql(statement.sql, statement.config, statement.explainer)
          end
        end

        # We can't serialize the explainer, so clear it before we transmit
        def prepare_to_send
          statement.explainer = nil
        end
      end



      class SqlTrace < TingYun::Metrics::Stats

        attr_reader :action_metric_name
        attr_reader :uri
        attr_reader :sql
        attr_reader :slow_sql
        attr_reader :params

        def initialize(normalized_query, slow_sql, action_name, uri)
          super()
          @params = {}

          @action_metric_name = action_name
          @slow_sql = slow_sql
          @sql = normalized_query
          @uri = uri
          @params[:stacktrace] = slow_sql.backtrace
          record_data_point(float(slow_sql.duration))
        end

        def aggregate(slow_sql, action_name, uri)
          if slow_sql.duration > max_call_time
            @action_metric_name = action_name
            @slow_sql = slow_sql
            @uri = uri
            @params[:stacktrace] = slow_sql.backtrace
          end
          record_data_point(float(slow_sql.duration))
        end

        def aggregate_trace(trace)
          aggregate(trace.slow_sql, trace.path, trace.url)
        end

        def prepare_to_send
          @sql = @slow_sql.sql unless need_to_obfuscate?
          @params[:explainPlan] = @slow_sql.explain if need_to_explain?
        end

        def need_to_obfuscate?
          Agent.config[:'nbs.action_tracer.record_sql'].to_s == 'obfuscated'
        end

        def need_to_explain?
          Agent.config[:'nbs.action_tracer.explain_enabled'] &&  @slow_sql.duration * 1000 > TingYun::Agent.config[:'nbs.action_tracer.explain_threshold']
        end


        include TingYun::Support::Coerce

        def to_collector_array(encoder)
          [
              @slow_sql.start_time,
              string(@action_metric_name),
              string(@slow_sql.metric_name),
              string(@uri),
              string(@sql),
              int(@call_count),
              TingYun::Helper.time_to_millis(@total_call_time),
              TingYun::Helper.time_to_millis(@max_call_time),
              TingYun::Helper.time_to_millis(@min_call_time),
              encoder.encode(@params)
          ]
        end
      end
    end
  end
end