package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"strings"
	"sync/atomic"
	"syscall"
	"time"

	"golang.org/x/sync/errgroup"
)

type LoadTestConfig struct {
	Concurrency          int
	EncryptionContexts   int
	TestType             string
	ReportUpdateDuration time.Duration
}

type LoadTestReport struct {
	NumWorkers            int
	NumEncryptionContexts int
	TimeStart             time.Time
	TotalEncryptCalls     uint64
	TotalDecryptCalls     uint64
	TotalBytesEncrypted   uint64
	TotalBytesDecrypted   uint64
}

func (r *LoadTestReport) Report() string {
	elapsed := time.Since(r.TimeStart)
	avgEncryptedPerCall := r.TotalBytesEncrypted / r.TotalEncryptCalls
	avgDecryptedPerCall := r.TotalBytesDecrypted / r.TotalDecryptCalls
	avgEncryptedPerSecond := float64(r.TotalBytesEncrypted) / elapsed.Seconds()
	avgDecryptedPerSecond := float64(r.TotalBytesDecrypted) / elapsed.Seconds()
	return fmt.Sprintf("\n--------------------------------------\nStats:\n\tLoad Test Duration: %v\n\tNumber of Workers: %v\n\tNumber of Encryption Contexts: %v\n\tTotal Bytes Encrypted: %v\n\tTotal Bytes Decrypted: %v\n\tTotal Encrypt Calls: %v\n\tTotal Decrypt Calls: %v\n\tAverage Bytes Encrypted Per Call: %v\n\tAverage Bytes Decrypted Per Call: %v\n\tAverage Bytes Encrypted per Second: %.3f\n\tAverage Bytes Decrypted per Second: %.3f",
		elapsed, r.NumWorkers, r.NumEncryptionContexts, r.TotalBytesEncrypted, r.TotalBytesDecrypted, r.TotalEncryptCalls, r.TotalDecryptCalls, avgEncryptedPerCall, avgDecryptedPerCall, avgEncryptedPerSecond, avgDecryptedPerSecond)
}

func main() {
	var (
		numWorkers                  = flag.Int("n", 1, "number of workers to encrypt and decrypt concurrently with, must be > 0")
		numEncryptionContexts       = flag.Int("c", 1, "number of encryption contexts to encrypt/decrypt with, must be > 0")
		reportUpdateDurationSeconds = flag.Int("d", 300, "duration in seconds to send report updates")
		testType                    = flag.String("t", "EncryptDecrypt", "test type to run: one of [encrypt, decrypt, encryptdecrypt]")
	)

	flag.Parse()

	if *numWorkers < 0 {
		flag.Usage()
		os.Exit(1)
	}

	if *reportUpdateDurationSeconds < 0 {
		flag.Usage()
		os.Exit(1)
	}

	test := strings.ToLower(*testType)
	if test != "encrypt" && test != "decrypt" && test != "encryptdecrypt" {
		flag.Usage()
		os.Exit(1)
	}

	fmt.Printf("Running load test with %d workers, %d encryption contexts. You will be updated every %d seconds of the load test progress.\n\n\n", *numWorkers, *numEncryptionContexts, *reportUpdateDurationSeconds)

	testConfig := &LoadTestConfig{
		Concurrency:          *numWorkers,
		EncryptionContexts:   *numEncryptionContexts,
		TestType:             test,
		ReportUpdateDuration: time.Duration(*reportUpdateDurationSeconds) * time.Second,
	}

	report := loadtest(testConfig)

	fmt.Printf("\n\n\n*********************************FINAL LOAD TEST RESULTS*********************************%s\n\n\n", report.Report())
}

func loadtest(config *LoadTestConfig) *LoadTestReport {
	var latestBytesEncrypted, latestBytesDecrypted, latestEncryptCalls, latestDecryptCalls uint64
	report := &LoadTestReport{
		NumWorkers:            config.Concurrency,
		NumEncryptionContexts: config.EncryptionContexts,
	}
	ticker := time.NewTicker(config.ReportUpdateDuration)
	ole := NewOLETester(config)
	ctx, cancel := context.WithCancel(context.Background())
	g, ctx := errgroup.WithContext(ctx)

	// signal routine
	g.Go(func() error {
		signalC := make(chan os.Signal, 1)
		signal.Notify(signalC, os.Interrupt, syscall.SIGTERM)

		select {
		case s := <-signalC:
			fmt.Printf("received signal: %v\n", s)
			cancel()
		case <-ctx.Done():
			fmt.Printf("shutting down signal capturing")
			return ctx.Err()
		}

		return nil
	})

	report.TimeStart = time.Now()
	for i := 1; i <= config.Concurrency; i++ {
		i := i // avoid concurrency bug
		g.Go(func() error {
			for {
				select {
				case <-ctx.Done():
					fmt.Printf("shutting down worker #%d\n", i)
					return ctx.Err()
				case <-ticker.C:
					diffBytesEncrypted := report.TotalBytesEncrypted - latestBytesEncrypted
					diffBytesDecrypted := report.TotalBytesDecrypted - latestBytesDecrypted
					diffEncryptCalls := report.TotalEncryptCalls - latestEncryptCalls
					diffDecryptCalls := report.TotalDecryptCalls - latestDecryptCalls

					var diffAvgEncrypt, diffAvgDecrypt string
					if diffEncryptCalls == 0 {
						diffAvgEncrypt = "N/A"
					} else {
						diffAvgEncrypt = fmt.Sprintf("%v", diffBytesEncrypted/diffEncryptCalls)
					}
					if diffDecryptCalls == 0 {
						diffAvgDecrypt = "N/A"
					} else {
						diffAvgDecrypt = fmt.Sprintf("%v", diffBytesDecrypted/diffDecryptCalls)
					}

					update := fmt.Sprintf("\n\tTotal Bytes Encrypted Since Last Update: %v\n\tTotal Bytes Decrypted Since Last Update: %v\n\tTotal Encrypt Calls Since Last Update: %d\n\tTotal Decrypt Calls Since Last Update: %d\n\tAverage Bytes Encrypted Since Last Update: %v\n\tAverage Bytes Decrypted Since Last Update: %v\n",
						diffBytesEncrypted, diffBytesDecrypted, diffEncryptCalls, diffDecryptCalls, diffAvgEncrypt, diffAvgDecrypt)
					fmt.Printf("%s%s", report.Report(), update)
					latestBytesEncrypted = report.TotalBytesEncrypted
					latestBytesDecrypted = report.TotalBytesDecrypted
					latestEncryptCalls = report.TotalEncryptCalls
					latestDecryptCalls = report.TotalDecryptCalls
				default:
					singleStats := ole.E2E(config.TestType)
					atomic.AddUint64(&report.TotalBytesEncrypted, singleStats.BytesEncrypted)
					atomic.AddUint64(&report.TotalBytesDecrypted, singleStats.BytesDecrypted)
					atomic.AddUint64(&report.TotalEncryptCalls, singleStats.EncryptCalls)
					atomic.AddUint64(&report.TotalDecryptCalls, singleStats.DecryptCalls)
				}

			}
		})
	}

	if err := g.Wait(); err != nil {
		fmt.Println("load test finished without error")
	} else {
		fmt.Printf("error received: %v\n", err)
	}

	return report
}
