//go:build e2e
// +build e2e

package performance

import (
	"bufio"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	mrand "math/rand"

	"github.com/tidbcloud/observability/test/ft/utils"
	"gopkg.in/yaml.v2"
	"k8s.io/apimachinery/pkg/util/rand"
)

type ParameterConfig struct {
	ConcurrentUsers     int           `yaml:"concurrentUsers"`
	TotalTenants        int           `yaml:"totalTenants"`
	ClustersPerTenant   int           `yaml:"clustersPerTenant"`
	EnableBigTenant     bool          `yaml:"enableBigTenant"`
	TestBizType         string        `yaml:"testBizType"`
	ParentClusterID     string        `yaml:"parentClusterID"`
	WriteBatchSize      int           `yaml:"writeBatchSize"`
	CtlreadTestDuration time.Duration `yaml:"ctlreadTestDuration"`
	RWInterval          time.Duration `yaml:"rwInterval"`

	TotalClusters     int           `yaml:"totalClusters"`
	MetricsPerWrite   int           `yaml:"metricsPerWrite"`
	WriteInterval     time.Duration `yaml:"writeInterval"`
	WriteTestDuration time.Duration `yaml:"writeTestDuration"`
	LabelNumber       int           `yaml:"labelNumber"`

	TotalDataTenants     int           `yaml:"totalDataTenants"`
	ReadMetricsCount     int           `yaml:"readMetricsCount"`
	ReadInterval         time.Duration `yaml:"readInterval"`
	DataReadTestDuration time.Duration `yaml:"dataReadTestDuration"`
}

// HTTP status code statistics
type StatusCodeStats struct {
	counts map[int]int
	mutex  sync.Mutex
}

// Request duration statistics
type DurationStats struct {
	durations []time.Duration
	mutex     sync.Mutex
}

// Operation result
type OperationResult struct {
	Operation     string
	StartTime     time.Time
	EndTime       time.Time
	Duration      time.Duration
	StatusCode    int
	Error         error
	RequestID     string
	ResourceID    string
	TenantID      string
	ProjectID     string
	ApplicationID string
}

// MetricDataPoint represents a single data point for a metric
type MetricDataPoint struct {
	Timestamp int64
	Value     float64
	Labels    map[string]string
}

// Metric represents a single metric with multiple data points
type Metric struct {
	Name       string
	DataPoints []MetricDataPoint
}

// WriteMetricsRequest represents a request to write metrics
type WriteMetricsRequest struct {
	ClusterID string
	Metrics   []Metric
}

// ReadMetricsRequest represents a request to read metrics
type ReadMetricsRequest struct {
	ClusterID   string
	MetricNames []string
	StartTime   int64
	EndTime     int64
	Step        int64 // Step in seconds
}

// Global statistics objects
var (
	createStatusStats = &StatusCodeStats{counts: make(map[int]int)}
	createTimeStats   = &DurationStats{}

	listStatusStats = &StatusCodeStats{counts: make(map[int]int)}
	listTimeStats   = &DurationStats{}

	detailStatusStats = &StatusCodeStats{counts: make(map[int]int)}
	detailTimeStats   = &DurationStats{}

	writeStatusStats = &StatusCodeStats{counts: make(map[int]int)}
	writeTimeStats   = &DurationStats{}

	readStatusStats = &StatusCodeStats{counts: make(map[int]int)}
	readTimeStats   = &DurationStats{}

	// save clusters' id in controlPlane phase
	createdClusters   = make([]string, 0, totalTenants*clustersPerTenant)
	createdClustersMu sync.Mutex
	fileMu            sync.Mutex

	// Atomic counters for tracking progress
	clusterCreationCounter uint64
	listQueryCounter       uint64
	detailQueryCounter     uint64
	writeCounter           uint64
	readCounter            uint64

	mu       sync.Mutex
	wrongMap = make(map[string]int)

	// config parameters
	concurrentUsers     int
	totalTenants        int
	clustersPerTenant   int
	enableBigTenant     bool
	testBizType         string
	parentClusterID     string
	writeBatchSize      int
	ctlreadTestDuration time.Duration
	rwInterval          time.Duration

	totalClusters     int
	metricsPerWrite   int
	writeInterval     time.Duration
	writeTestDuration time.Duration
	labelNumber       int

	totalDataTenants     int
	readMetricsCount     int
	readInterval         time.Duration
	dataReadTestDuration time.Duration
)

var perfClient = &http.Client{
	Timeout: 0,
	Transport: &http.Transport{
		MaxIdleConns:        10000, // 100
		MaxConnsPerHost:     10000, // 0
		MaxIdleConnsPerHost: 10000,
		IdleConnTimeout:     60 * time.Second,
		DisableKeepAlives:   false,
		//ForceAttemptHTTP2:   false, // disable http2
	},
}

func LoadParameterConfig(path string) {
	in, err := os.ReadFile(path)
	if err != nil {
		panic(fmt.Sprintf("Failed to read config file: %v", err))
	}

	type Wrapper struct {
		Parameter ParameterConfig `yaml:"parameter"`
	}

	var cfg Wrapper
	if err := yaml.Unmarshal(in, &cfg); err != nil {
		panic(fmt.Sprintf("Failed to unmarshal config: %v", err))
	}

	concurrentUsers = cfg.Parameter.ConcurrentUsers
	totalTenants = cfg.Parameter.TotalTenants
	clustersPerTenant = cfg.Parameter.ClustersPerTenant
	enableBigTenant = cfg.Parameter.EnableBigTenant
	testBizType = cfg.Parameter.TestBizType
	parentClusterID = cfg.Parameter.ParentClusterID
	writeBatchSize = cfg.Parameter.WriteBatchSize
	ctlreadTestDuration = cfg.Parameter.CtlreadTestDuration
	rwInterval = cfg.Parameter.RWInterval

	totalClusters = cfg.Parameter.TotalClusters
	metricsPerWrite = cfg.Parameter.MetricsPerWrite
	writeInterval = cfg.Parameter.WriteInterval
	writeTestDuration = cfg.Parameter.WriteTestDuration
	labelNumber = cfg.Parameter.LabelNumber

	totalDataTenants = cfg.Parameter.TotalDataTenants
	readMetricsCount = cfg.Parameter.ReadMetricsCount
	readInterval = cfg.Parameter.ReadInterval
	dataReadTestDuration = cfg.Parameter.DataReadTestDuration
}

// Record HTTP status code
func (s *StatusCodeStats) Record(statusCode int) {
	s.mutex.Lock()
	defer s.mutex.Unlock()
	s.counts[statusCode]++
}

// Get HTTP status code statistics
func (s *StatusCodeStats) GetStats() map[int]int {
	s.mutex.Lock()
	defer s.mutex.Unlock()

	result := make(map[int]int)
	for code, count := range s.counts {
		result[code] = count
	}
	return result
}

// Record request duration
func (s *DurationStats) Record(duration time.Duration) {
	s.mutex.Lock()
	defer s.mutex.Unlock()
	s.durations = append(s.durations, duration)
}

// Get request duration statistics
func (s *DurationStats) GetStats(testName string) (time.Duration, time.Duration, map[string]int) {
	s.mutex.Lock()
	defer s.mutex.Unlock()

	//fmt.Println(s.durations)
	if len(s.durations) == 0 {
		return 0, 0, nil
	}

	var (
		total        time.Duration
		maxTime      time.Duration
		count        int
		threshold    time.Duration
		distribution = make(map[string]int)
	)

	switch testName {
	case "TestWritePerformance":
		threshold = 80 * time.Second
	case "TestNormalReadPerformance", "TestColdStartReadPerformance":
		threshold = 30 * time.Second
	case "TestDataPlaneWritePerformance":
		threshold = 15 * time.Second
	case "TestDataPlaneReadPerformance":
		threshold = 30 * time.Second
	}
	for _, d := range s.durations {
		// Always contribute to distribution
		switch {
		case d < 100*time.Millisecond:
			distribution["<100ms"]++
		case d < 200*time.Millisecond:
			distribution["100-200ms"]++
		case d < 500*time.Millisecond:
			distribution["200-500ms"]++
		case d < time.Second:
			distribution["500ms-1s"]++
		case d < 2*time.Second:
			distribution["1-2s"]++
		case d < 5*time.Second:
			distribution["2-5s"]++
		case d < 10*time.Second:
			distribution["5-10s"]++
		default:
			distribution[">10s"]++
		}
		// Only include in average and max if within threshold
		if d <= threshold {
			total += d
			count++
			if d > maxTime {
				maxTime = d
			}
		}
	}
	var avg time.Duration
	if count > 0 {
		avg = total / time.Duration(count)
	}

	return avg, maxTime, distribution
}

func ensureLogDir() string {
	currentDir, err := os.Getwd()
	if err != nil {
		panic(fmt.Sprintf("Failed to get current directory: %v", err))
	}

	logDir := filepath.Join(currentDir, "log")
	if _, err := os.Stat(logDir); os.IsNotExist(err) {
		err := os.MkdirAll(logDir, 0755)
		if err != nil {
			panic(fmt.Sprintf("Failed to create log directory: %v", err))
		}
	}
	return logDir
}

func saveRandomPrefixToFile(filename, prefix string) error {
	fileMu.Lock()
	defer fileMu.Unlock()
	f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer f.Close()

	if _, err := f.WriteString(fmt.Sprintf("# RANDOM_PREFIX=%s\n", prefix)); err != nil {
		return err
	}
	return nil
}

func saveClusterIDToFile(filename, clusterID string) error {
	fileMu.Lock()
	defer fileMu.Unlock()
	f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer f.Close()

	if _, err := f.WriteString(clusterID + "\n"); err != nil {
		return err
	}
	return nil
}

func loadClusterIDs(filename string) ([]string, error) {
	var clusters []string
	f, err := os.Open(filename)
	if err != nil {
		return clusters, err
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		// Skip comment lines (prefix line)
		if !strings.HasPrefix(line, "#") && line != "" {
			clusters = append(clusters, line)
		}
	}
	if err := scanner.Err(); err != nil {
		return clusters, err
	}
	return clusters, nil
}

func loadRandomPrefix(filename string) (string, error) {
	f, err := os.Open(filename)
	if err != nil {
		return "", err
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "# RANDOM_PREFIX=") {
			return strings.TrimPrefix(line, "# RANDOM_PREFIX="), nil
		}
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return "", fmt.Errorf("random prefix not found in file")
}

func printPerformanceStats(t *testing.T, testName string, operation string, statusStats *StatusCodeStats, timeStats *DurationStats, totalRequests int, wrongMap map[string]int) {
	statusCodes := statusStats.GetStats()
	avgTime, maxTime, distribution := timeStats.GetStats(testName)
	// Print results
	t.Logf("\n------ %s Performance Statistics ------", operation)

	// Calculate success rate
	successCount := 0
	for code, count := range statusCodes {
		if code >= 200 && code < 300 {
			successCount += count
		}
	}
	successRate := float64(successCount) / float64(totalRequests) * 100

	// Print HTTP status code distribution
	t.Log("HTTP Status Code Distribution:")
	for code, count := range statusCodes {
		if code == 999 {
			t.Logf("  %d(timeout): %d (%.2f%%)", code, count, float64(count)/float64(totalRequests)*100)

		} else {
			t.Logf("  %d: %d (%.2f%%)", code, count, float64(count)/float64(totalRequests)*100)
		}
	}

	t.Logf("Request Success Rate: %.2f%%", successRate)
	t.Logf("Average Request Duration: %v", avgTime)
	t.Logf("Maximum Request Duration: %v", maxTime)

	// Print duration distribution
	t.Log("Request Duration Distribution:")
	for timeRange, count := range distribution {
		t.Logf("  %s: %d (%.2f%%)", timeRange, count, float64(count)/float64(totalRequests)*100)
	}

	// Write results
	logDir := ensureLogDir()
	logFilePath := filepath.Join(logDir, fmt.Sprintf("performance_results_%s.log", time.Now().Format("20060102_150405")))

	// mu := &sync.Mutex{}
	// mu.Lock()
	// defer mu.Unlock()

	file, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		t.Logf("Failed to open log file: %v", err)
		return
	}
	defer file.Close()

	var errorLines strings.Builder
	if len(wrongMap) > 0 {
		errorLines.WriteString("\n\nDetailed Errors:\n")
		for errStr, count := range wrongMap {
			errorLines.WriteString(fmt.Sprintf("  %s: %d\n", errStr, count))
		}
	}

	result := fmt.Sprintf(`
		[%s - %s]
		HTTP Status Code Distribution:
		%s

		Request Success Rate: %.2f%%
		Average Request Duration: %v
		Maximum Request Duration: %v

		Request Duration Distribution:
		%s

		--------------------------
		%s
		`,
		testName,
		operation,
		formatStatusCodes(statusCodes, totalRequests),
		successRate,
		avgTime,
		maxTime,
		formatDurationDistribution(distribution, totalRequests),
		errorLines.String(),
	)

	if _, err := file.WriteString(result); err != nil {
		t.Logf("Failed to write to log file: %v", err)
	}

	t.Logf("Performance statistics saved to: %s", logFilePath)
}

// Helper function: format status codes
func formatStatusCodes(statusCodes map[int]int, totalRequests int) string {
	var result strings.Builder
	for code, count := range statusCodes {
		result.WriteString(fmt.Sprintf("  %d: %d (%.2f%%)\n", code, count, float64(count)/float64(totalRequests)*100))
	}
	return result.String()
}

// Helper function: format duration distribution
func formatDurationDistribution(distribution map[string]int, totalRequests int) string {
	var result strings.Builder
	for timeRange, count := range distribution {
		result.WriteString(fmt.Sprintf("  %s: %d (%.2f%%)\n", timeRange, count, float64(count)/float64(totalRequests)*100))
	}
	return result.String()
}

// generateMetricDataPoints generates random data points for a metric
func generateMetricDataPoints(now time.Time, clusterID string, metricIndex int, labelNumber int) []MetricDataPoint {
	points := make([]MetricDataPoint, 1)

	labels := map[string]string{
		"cluster_id": clusterID,
		"metric_id":  fmt.Sprintf("metric_%d", metricIndex),
		"job":        "monitoring",
	}
	for i := 0; i < labelNumber; i++ {
		key := fmt.Sprintf("label_%d", i)
		value := fmt.Sprintf("value_%d", metricIndex+i)
		labels[key] = value
	}
	points[0] = MetricDataPoint{
		Timestamp: now.Unix(),
		Value:     mrand.Float64() * 100,
		Labels:    labels,
	}

	return points
}

// generateWriteRequest generates a write metrics request
func generateWriteRequest(clusterID string) WriteMetricsRequest {
	now := time.Now()

	// Create different metrics
	metrics := make([]Metric, metricsPerWrite)
	for i := 0; i < metricsPerWrite; i++ {
		metricName := fmt.Sprintf("test_metric_%d", i) // Use a unique name for each metric

		metrics[i] = Metric{
			Name:       metricName,
			DataPoints: generateMetricDataPoints(now, clusterID, i, labelNumber),
		}
	}

	return WriteMetricsRequest{
		ClusterID: clusterID, // no use
		Metrics:   metrics,
	}
}

// generateReadRequest generates a random read metrics request
func generateReadRequest(clusterID string) ReadMetricsRequest {
	now := time.Now()
	endTime := now.Unix()
	startTime := now.Add(-30 * time.Minute).Unix() // Query data from the last ? minutes

	// Select a random metric for each query
	metricIndex := rand.Intn(metricsPerWrite) // random index to query
	metricName := fmt.Sprintf("test_metric_%d", metricIndex)

	return ReadMetricsRequest{
		ClusterID:   clusterID,
		MetricNames: []string{metricName},
		StartTime:   startTime,
		EndTime:     endTime,
		Step:        20,
	}
}

// Initialize test environment
func setupTest() {
	utils.PreparConfig("./perf_config.yaml")
	LoadParameterConfig("./perf_config.yaml")
	utils.PrepareToken()
	utils.PrepareKubeClient()
	utils.SetRandomID(rand.String(6))
}

// TestMain for setting up and cleaning up the test environment
func TestMain(m *testing.M) {
	// Initialization and setup before testing
	fmt.Println("Setting up performance test environment")
	// Initialize test environment
	setupTest()
	// Run tests
	exitCode := m.Run()
	os.Exit(exitCode)
}
