/*
Copyright 2021 The Kubernetes Authors.

Licensed 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.
*/

package benchmark

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"time"

	"github.com/sirupsen/logrus"
)

// LifecycleBenchmarkDatapoint encodes a single benchmark for a lifecycle operation.
// Contains a slice of int64s which represent the duration in nanoseconds of the
// operations which comprise a lifecycle being benchmarked.
// (e.g. the individual CRUD operations which are cycled through during the benchmark).
type LifecycleBenchmarkDatapoint struct {
	// int64 index of the sample.
	SampleIndex int `json:"sampleIndex"`

	// int64 nanosecond timestamp of the start of the result.
	StartTime int64 `json:"startTime"`

	// int64 nanosecond timestamp of the start of the result.
	EndTime int64 `json:"endTime"`

	// Slice of int64s representing the durations of individual operations.
	// The operation durations should be in the order they were executed in.
	// Note that the sum of OperationsDurationsNs need not be exactly equal to the duration
	// determined by subtracting StartTime from EndTime, as there may be additional steps
	// (e.g. timer setup) performed between the individual operations.
	OperationsDurationsNs []int64 `json:"operationsDurationsNs"`

	// String mapping for adding arbitrary meta-info for the lifecycle result:
	MetaInfo map[string]string `json:"metaInfo"`
}

// LifecycleBenchmarksResultsSet houses results for benchmarks involving resource lifecycles
// which include multiple benchmarked iterations of the cycle.
type LifecycleBenchmarksResultsSet struct {
	// Slice of string operation names which represent one cycle.
	OperationsNames []string `json:"operationsNames"`

	// The maximum number of lifecycles which were benchmarked in parallel.
	// Anything <= 1 should be considered sequential.
	NumParallel int `json:"numParallel"`

	// List of datapoints for each lifecycle benchmark.
	Datapoints []LifecycleBenchmarkDatapoint `json:"datapoints"`
}

// LifecycleBenchmarksResultsManager tracks lifecycle benchmark results through channels.
type LifecycleBenchmarksResultsManager struct {
	// The LifecycleBenchmarksResultsSet where results are added.
	resultsSet LifecycleBenchmarksResultsSet

	// Channel for sending results to the manager.
	resultsChannel chan *LifecycleBenchmarkDatapoint

	// Channel to indicate when the results consumer goroutine has ended.
	resultsOverChannel chan bool

	// Flag to indicate whether the results consumer goroutine is running.
	resultsConsumerRunning bool

	// The maximum timeout in seconds to wait between individual results being received.
	resultsChannelTimeoutSeconds int
}

// NewLifecycleBenchmarksResultsManager instantiates a new LifecycleBenchmarksResultsManager and its internal channels/structures.
func NewLifecycleBenchmarksResultsManager(initialResultsSet LifecycleBenchmarksResultsSet, resultsChannelTimeoutSeconds int) *LifecycleBenchmarksResultsManager {
	lbrm := LifecycleBenchmarksResultsManager{
		resultsSet:                   initialResultsSet,
		resultsChannelTimeoutSeconds: resultsChannelTimeoutSeconds,
		resultsChannel:               make(chan *LifecycleBenchmarkDatapoint),
		resultsOverChannel:           make(chan bool),
	}
	if lbrm.resultsSet.Datapoints == nil {
		lbrm.resultsSet.Datapoints = make([]LifecycleBenchmarkDatapoint, 0)
	}

	return &lbrm
}

// StartResultsConsumer starts the results consumer goroutine and returns the channel to write results to.
// A nil value must be sent after all other results were sent to indicate the end of the result
// stream.
func (lbrm *LifecycleBenchmarksResultsManager) StartResultsConsumer() chan *LifecycleBenchmarkDatapoint {
	if !lbrm.resultsConsumerRunning {
		lbrm.resultsConsumerRunning = true
		go lbrm.awaitResult()
	}

	return lbrm.resultsChannel
}

// AwaitAllResults waits for the result consumer goroutine and returns all the results registered insofar.
func (lbrm *LifecycleBenchmarksResultsManager) AwaitAllResults(timeoutSeconds int) error {
	if !lbrm.resultsConsumerRunning {
		return nil
	}

	timeout := time.After(time.Duration(timeoutSeconds) * time.Second)
	select {
	case <-lbrm.resultsOverChannel:
		lbrm.resultsConsumerRunning = false

		return nil
	case <-timeout:
		logrus.Warnf("Failed to await all results. Results registered so far were: %+v", lbrm.resultsSet)

		return fmt.Errorf("benchmark results waiting timed out after %d seconds", timeoutSeconds)
	}
}

// WriteResultsFile saves the results gathered so far as JSON under the given filepath.
func (lbrm *LifecycleBenchmarksResultsManager) WriteResultsFile(filepath string) error {
	if lbrm.resultsConsumerRunning {
		return errors.New("results consumer is still running and expecting results")
	}

	data, err := json.MarshalIndent(lbrm.resultsSet, "", " ")
	if err == nil {
		err = os.WriteFile(filepath, data, 0o600)
		if err != nil {
			return fmt.Errorf("failed to write benchmarks results to file: %v", filepath)
		}
	} else {
		return fmt.Errorf("failed to serialize benchmark data: %w", err)
	}

	return nil
}

// Function which continuously consumes results from the resultsChannel until receiving a nil.
func (lbrm *LifecycleBenchmarksResultsManager) awaitResult() {
	numOperations := len(lbrm.resultsSet.OperationsNames)

	for {
		var res *LifecycleBenchmarkDatapoint

		timeout := time.After(time.Duration(lbrm.resultsChannelTimeoutSeconds) * time.Second)

		select {
		case res = <-lbrm.resultsChannel:
			// Receiving nil indicates results are over:
			if res == nil {
				logrus.Info("Results ended")

				lbrm.resultsConsumerRunning = false
				lbrm.resultsOverChannel <- true

				return
			}

			// Warn if an improper number of results was received:
			if len(res.OperationsDurationsNs) != numOperations {
				logrus.Warnf("Received improper number of datapoints for operations %+v: %+v", lbrm.resultsSet.OperationsNames, res.OperationsDurationsNs)
			}

			// Register the result:
			lbrm.resultsSet.Datapoints = append(lbrm.resultsSet.Datapoints, *res)

		case <-timeout:
			err := fmt.Errorf("timed out after waiting %d seconds for new results", lbrm.resultsChannelTimeoutSeconds)
			logrus.Error(err.Error())
			panic(err)
		}
	}
}
