# frozen_string_literal: true

module GraphQL
  module Metrics
    class Tracer
      # NOTE: These constants come from the graphql ruby gem.
      GRAPHQL_GEM_LEX_KEY = 'lex'
      GRAPHQL_GEM_EXECUTE_MULTIPLEX_KEY = 'execute_multiplex'
      GRAPHQL_GEM_PARSING_KEY = 'parse'
      GRAPHQL_GEM_VALIDATION_KEY = 'validate'
      GRAPHQL_GEM_ANALYZE_KEY = 'analyze_query'
      GRAPHQL_GEM_TRACING_FIELD_KEYS = [
        GRAPHQL_GEM_TRACING_FIELD_KEY = 'execute_field',
        GRAPHQL_GEM_TRACING_LAZY_FIELD_KEY = 'execute_field_lazy'
      ]

      def trace(key, data, &block)
        # NOTE: Context doesn't exist yet during lexing, parsing.
        possible_context = data[:query]&.context

        skip_tracing = possible_context&.fetch(GraphQL::Metrics::SKIP_GRAPHQL_METRICS_ANALYSIS, false)
        return yield if skip_tracing

        # NOTE: Not all tracing events are handled here, but those that are are handled in this case statement in
        # chronological order.
        case key
        when GRAPHQL_GEM_LEX_KEY
          return setup_tracing(&block)
        when GRAPHQL_GEM_EXECUTE_MULTIPLEX_KEY
          return setup_tracing(&block)
        when GRAPHQL_GEM_PARSING_KEY
          return capture_parsing_time(&block)
        when GRAPHQL_GEM_VALIDATION_KEY
          context = possible_context
          return capture_validation_time(context, &block)
        when GRAPHQL_GEM_ANALYZE_KEY
          context = possible_context
          return capture_analysis_time(context, &block)

        when *GRAPHQL_GEM_TRACING_FIELD_KEYS
          return yield if data[:query].context[SKIP_FIELD_AND_ARGUMENT_METRICS]
          return yield unless GraphQL::Metrics.timings_capture_enabled?(data[:query].context)

          context_key = case key
          when GRAPHQL_GEM_TRACING_FIELD_KEY
            GraphQL::Metrics::INLINE_FIELD_TIMINGS
          when GRAPHQL_GEM_TRACING_LAZY_FIELD_KEY
            GraphQL::Metrics::LAZY_FIELD_TIMINGS
          end

          trace_field(context_key, data, &block)
        else
          return yield
        end
      end

      private

      PreContext = Struct.new(
        :query_start_time,
        :query_start_time_monotonic,
        :parsing_start_time_offset,
        :parsing_duration
      ) do
        def reset_parsing_timings
          self[:parsing_start_time_offset] = nil
          self[:parsing_duration] = nil
        end
      end

      def pre_context
        # NOTE: This is used to store timings from lexing, parsing, validation, before we have a context to store
        # values in. Uses thread-safe Concurrent::ThreadLocalVar to store a set of values per thread.
        @pre_context ||= Concurrent::ThreadLocalVar.new(PreContext.new)
        @pre_context.value
      end

      def setup_tracing
        return yield if pre_context.query_start_time

        pre_context.query_start_time = GraphQL::Metrics.current_time
        pre_context.query_start_time_monotonic = GraphQL::Metrics.current_time_monotonic

        yield
      end

      def capture_parsing_time
        timed_result = GraphQL::Metrics.time { yield }

        pre_context.parsing_start_time_offset = timed_result.start_time
        pre_context.parsing_duration = timed_result.duration

        timed_result.result
      end

      def capture_validation_time(context)
        if pre_context.parsing_duration.nil?
          pre_context.parsing_start_time_offset = 0.0
          pre_context.parsing_duration = 0.0
        end

        timed_result = GraphQL::Metrics.time(pre_context.query_start_time_monotonic) { yield }

        ns = context.namespace(CONTEXT_NAMESPACE)

        ns[QUERY_START_TIME] = pre_context.query_start_time
        ns[QUERY_START_TIME_MONOTONIC] = pre_context.query_start_time_monotonic
        ns[PARSING_START_TIME_OFFSET] = pre_context.parsing_start_time_offset
        ns[PARSING_DURATION] = pre_context.parsing_duration
        ns[VALIDATION_START_TIME_OFFSET] = timed_result.time_since_offset
        ns[VALIDATION_DURATION] = timed_result.duration

        pre_context.reset_parsing_timings

        timed_result.result
      end

      def capture_analysis_time(context)
        ns = context.namespace(CONTEXT_NAMESPACE)

        timed_result = GraphQL::Metrics.time(ns[QUERY_START_TIME_MONOTONIC]) { yield }

        ns[ANALYSIS_START_TIME_OFFSET] = timed_result.time_since_offset
        ns[ANALYSIS_DURATION] = timed_result.duration

        timed_result.result
      end

      def trace_field(context_key, data)
        ns = data[:query].context.namespace(CONTEXT_NAMESPACE)
        query_start_time_monotonic = ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]

        timed_result = GraphQL::Metrics.time(query_start_time_monotonic) { yield }

        path_excluding_numeric_indicies = data[:path].select { |p| p.is_a?(String) }
        ns[context_key][path_excluding_numeric_indicies] ||= []
        ns[context_key][path_excluding_numeric_indicies] << {
          start_time_offset: timed_result.time_since_offset, duration: timed_result.duration
        }

        timed_result.result
      end
    end
  end
end