# SPDX-License-Identifier: Apache-2.0
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
#
# Modifications Copyright OpenSearch Contributors. See
# GitHub history for details.
#
# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

module OpenSearch
  module Benchmarking

    # Class encapsulating formatting and indexing the results from a benchmarking run.
    #
    # @since 7.0.0
    class Results

      attr_reader :raw_results

      # String constant for unit of time elapsed.
      #
      # @since 7.0.0
      MILLISECONDS = 'milliseconds'.freeze

      # Create a Results object.
      #
      # @example Create a results object.
      #   Benchmarking::Results.new(task, [...], options)
      #
      # @param [ OpenSearch::Benchmarking ] task The task that executed the benchmarking run.
      # @param [ Array<Fixnum> ] results An array of the results.
      # @param [ Hash ] options The options.
      #
      # @since 7.0.0
      def initialize(task, results, options = {})
        @task = task
        @raw_results = results.map { |r| r * 1000 }
        @options = options
      end

      # Index the results document into opensearch.
      #
      # @example Index the results.
      #   results.index!(client)
      #
      # @param [ OpenSearch::Client ] client The client to use to index the results.
      #
      # @return [ Hash ] The results document.
      #
      # @since 7.0.0
      def index!(client)
        create_index!(client)
        client.index(index: index_name, body: results_doc)
        results_doc
      end

      # The document recording the benchmarking run results, to index into the results cluster.
      #
      # @example Get the results document.
      #   results.results_doc
      #
      # @return [ Hash ] The results document.
      #
      # @since 7.0.0
      def results_doc
        @results_doc ||= begin
          { '@timestamp' => Time.now.iso8601,
            event: event_doc,
            agent: agent_doc,
            server: server_doc }
        end
      end

      private

      attr_reader :options

      DEFAULT_INDEX_NAME = 'benchmarking_results'.freeze

      DEFAULT_METRICS = ['median'].freeze

      CLIENT_NAME = 'opensearch-ruby-client'.freeze

      COMPLEXITIES = { OpenSearch::Benchmarking::Simple => :simple,
                       OpenSearch::Benchmarking::Complex => :complex }.freeze

      def action_iterations
        options[:action_iterations]
      end

      def index_name
        options[:index_name] || DEFAULT_INDEX_NAME
      end

      def create_index!(client)
        unless client.indices.exists?(index: index_name)
          client.indices.create(index: index_name)
        end
      end

      def event_doc
        { description: description,
          category: category,
          action: action,
          duration: duration,
          statistics: statistics_doc,
          repetitions: repetitions_doc }.tap do |doc|
            doc.merge!(dataset: dataset,
                       dataset_details: dataset_details) if dataset
        end
      end

      def description
        @task.description
      end

      def category
        COMPLEXITIES[@task.class]
      end

      def action
        @options[:operation]
      end


      def dataset
        @options[:dataset]
      end

      def dataset_details
        { size: @options[:dataset_size],
          num_documents: @options[:dataset_n_documents] }
      end

      def duration
        @options[:duration] * 1000
      end

      def statistics_doc
        { unit: MILLISECONDS,
          mean: mean,
          median: median,
          max: max,
          min: min,
          standard_deviation: standard_deviation
        }
      end

      def median
        raw_results.sort![raw_results.size / 2 - 1]
      end

      def mean
        raw_results.inject { |sum, el| sum + el }.to_f / raw_results.size
      end

      def max
        raw_results.max
      end

      def min
        raw_results.min
      end

      def standard_deviation
        return 0 if raw_results.size < 2
        Math.sqrt(sample_variance)
      end

      def sample_variance
        m = mean
        sum = raw_results.inject(0) { |sum, i| sum +(i-m)**2 }
        sum/(raw_results.length - 1).to_f
      end

      def repetitions_doc
        { warmup: @task.warmup_repetitions,
          measured: @task.measured_repetitions,
          iterations: action_iterations }
      end

      def agent_doc
        { version: OpenSearch::VERSION,
          name: CLIENT_NAME,
          git: git_doc,
          language: language_doc,
          os: client_os_doc,
          adapter: adapter }
      end

      def adapter
        @task.client_adapter
      end

      def git_doc
        sha = `git rev-parse HEAD`
        branch = /\* (.+)/.match(`git branch`)[1]
        commit_message = `git log -1 --pretty=%B`
        repository = 'opensearch-ruby'

        { branch: branch,
          sha: sha.chomp,
          commit_message: commit_message.gsub(/\n/, ''),
          repository: repository.chomp }
      end

      def language_doc
        version = [
            RUBY_VERSION,
            RUBY_PLATFORM,
            RbConfig::CONFIG['build']
        ].compact.join(', ')
        { runtime_version: version }
      end

      def client_os_doc
        { platform: platform,
          type: type,
          architecture: architecture }
      end

      def type
        (RbConfig::CONFIG && RbConfig::CONFIG['host_os']) ?
            RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase : 'unknown'
      end

      def architecture
        RbConfig::CONFIG['target_cpu']
      end

      def platform
        [
            @platform,
            RUBY_VERSION,
            RUBY_PLATFORM,
            RbConfig::CONFIG['build']
        ].compact.join(', ')
      end

      def server_doc
        { version: @task.server_version,
          nodes_info: @task.nodes_info }
      end
    end
  end
end
