// Package main provides load testing for StemeDB.
//
// This tool benchmarks StemeDB performance with three scenarios:
//   - Baseline: 10K assertions to establish latency baseline
//   - Sustained: 1K writes/sec for configurable duration
//   - Concurrent: 100 concurrent readers with background writes
//
// Usage:
//
//	go run ./cmd/load-test --api-url http://localhost:18180
//	go run ./cmd/load-test --scenario sustained --duration 1h
//	go run ./cmd/load-test --scenario concurrent --readers 100
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"

	"github.com/orchard9/stemedb-go/steme"
)

const (
	defaultAPIURL      = "http://127.0.0.1:18180"
	defaultKeysFile    = "demo/keys/agents.json"
	defaultOutputDir   = "uat/production-readiness/results"
	defaultDuration    = 5 * time.Minute
	defaultTargetRPS   = 1000
	defaultReaderCount = 100
)

// DemoAgent represents a demo agent from agents.json.
type DemoAgent struct {
	Seed        string `json:"seed"`
	PublicKey   string `json:"public_key"`
	Tier        int    `json:"tier"`
	Description string `json:"description"`
}

func main() {
	// Parse flags
	apiURL := flag.String("api-url", "", "StemeDB API URL (default: $STEMEDB_API_URL or http://127.0.0.1:18180)")
	keysFile := flag.String("keys-file", "", "Path to agents.json (default: demo/keys/agents.json)")
	scenario := flag.String("scenario", "all", "Test scenario: baseline|sustained|concurrent|all")
	duration := flag.Duration("duration", defaultDuration, "Duration for sustained write test")
	targetRPS := flag.Int("target-rps", defaultTargetRPS, "Target requests per second for sustained test")
	readers := flag.Int("readers", defaultReaderCount, "Number of concurrent readers")
	outputDir := flag.String("output", "", "Output directory for reports (default: uat/production-readiness/results)")
	verbose := flag.Bool("verbose", false, "Enable verbose output")
	flag.Parse()

	// Resolve API URL
	url := *apiURL
	if url == "" {
		url = os.Getenv("STEMEDB_API_URL")
	}
	if url == "" {
		url = defaultAPIURL
	}

	// Resolve keys file
	keysPath := *keysFile
	if keysPath == "" {
		keysPath = findKeysFile()
	}

	// Resolve output directory
	outDir := *outputDir
	if outDir == "" {
		outDir = findOutputDir()
	}

	// Check if meter is disabled
	meterDisabled := os.Getenv("STEMEDB_METER_ENABLED") == "false"

	fmt.Println("=== StemeDB Load Tester ===")
	fmt.Println()
	fmt.Printf("API URL:     %s\n", url)
	fmt.Printf("Keys File:   %s\n", keysPath)
	fmt.Printf("Scenario:    %s\n", *scenario)
	fmt.Printf("Output Dir:  %s\n", outDir)
	if *scenario == "sustained" || *scenario == "all" {
		fmt.Printf("Duration:    %v\n", *duration)
		fmt.Printf("Target RPS:  %d\n", *targetRPS)
		if !meterDisabled {
			fmt.Println("WARNING: Meter is enabled. Set STEMEDB_METER_ENABLED=false for accurate sustained test results.")
		}
	}
	if *scenario == "concurrent" || *scenario == "all" {
		fmt.Printf("Readers:     %d\n", *readers)
	}
	fmt.Println()

	// Load agents
	clients, signers, err := loadAgents(keysPath, url)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to load agents: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("Loaded %d demo agents\n\n", len(clients))

	// Verify server is reachable
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Handle interrupt
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigCh
		fmt.Println("\nInterrupted. Stopping tests...")
		cancel()
	}()

	health, err := clients[0].Health(ctx)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Server not reachable at %s: %v\n", url, err)
		fmt.Fprintln(os.Stderr, "Start the server with: STEMEDB_METER_ENABLED=false cargo run --release --bin stemedb-api")
		os.Exit(1)
	}
	fmt.Printf("Server Status: %s (v%s, %d assertions)\n\n", health.Status, health.Version, health.AssertionsCount)

	// Prepare scenario config
	cfg := ScenarioConfig{
		Clients:   clients,
		Signers:   signers,
		Duration:  *duration,
		TargetRPS: *targetRPS,
		Readers:   *readers,
		Verbose:   *verbose,
	}

	// Run scenarios
	results := TestResults{
		APIURL:        url,
		Duration:      *duration,
		TargetRPS:     *targetRPS,
		ReaderCount:   *readers,
		MeterDisabled: meterDisabled,
	}

	switch *scenario {
	case "baseline":
		results.BaselineMetrics, err = RunBaselineLatency(ctx, cfg)
	case "sustained":
		results.SustainedMetrics, err = RunSustainedWrites(ctx, cfg)
	case "concurrent":
		results.ConcurrentMetrics, err = RunConcurrentReaders(ctx, cfg)
	case "all":
		results.BaselineMetrics, err = RunBaselineLatency(ctx, cfg)
		if err == nil {
			results.SustainedMetrics, err = RunSustainedWrites(ctx, cfg)
		}
		if err == nil {
			results.ConcurrentMetrics, err = RunConcurrentReaders(ctx, cfg)
		}
	default:
		fmt.Fprintf(os.Stderr, "Unknown scenario: %s\n", *scenario)
		os.Exit(1)
	}

	if err != nil && err != context.Canceled {
		fmt.Fprintf(os.Stderr, "Test failed: %v\n", err)
		os.Exit(1)
	}

	// Generate report
	fmt.Println("=== Generating Report ===")
	reportPath, err := GenerateReport(results, outDir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to generate report: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("Report saved to: %s\n", reportPath)

	// Print summary
	fmt.Println()
	fmt.Println("=== Summary ===")
	printSummary(results)
}

// loadAgents loads demo agents and creates clients.
func loadAgents(path, baseURL string) ([]*steme.Client, []*steme.Signer, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to read %s: %w", path, err)
	}

	var agents map[string]DemoAgent
	if err := json.Unmarshal(data, &agents); err != nil {
		return nil, nil, fmt.Errorf("failed to parse %s: %w", path, err)
	}

	var clients []*steme.Client
	var signers []*steme.Signer

	for name, agent := range agents {
		signer, err := steme.NewSignerFromHex(agent.Seed)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to create signer for %s: %w", name, err)
		}
		signers = append(signers, signer)
		clients = append(clients, steme.NewClient(baseURL, signer))
	}

	return clients, signers, nil
}

// findKeysFile locates the agents.json file.
func findKeysFile() string {
	// Try current directory
	if _, err := os.Stat(defaultKeysFile); err == nil {
		return defaultKeysFile
	}

	// Try one level up (if running from cmd/load-test)
	upPath := filepath.Join("..", "..", defaultKeysFile)
	if _, err := os.Stat(upPath); err == nil {
		return upPath
	}

	return defaultKeysFile
}

// findOutputDir locates the output directory.
func findOutputDir() string {
	// Try current directory
	if _, err := os.Stat("uat/production-readiness"); err == nil {
		return defaultOutputDir
	}

	// Try one level up
	upPath := filepath.Join("..", "..", defaultOutputDir)
	if _, err := os.Stat(filepath.Dir(upPath)); err == nil {
		return upPath
	}

	return defaultOutputDir
}

func printSummary(results TestResults) {
	allPass := true

	if results.BaselineMetrics != nil {
		m := results.BaselineMetrics
		pass := m.WriteP99() <= Targets.BaselineP99 && m.WriteErrorRate() == 0
		status := "PASS"
		if !pass {
			status = "FAIL"
			allPass = false
		}
		fmt.Printf("Baseline:   %s (p99: %v, errors: %d)\n", status, m.WriteP99(), m.WriteErrors())
	}

	if results.SustainedMetrics != nil {
		m := results.SustainedMetrics
		pass := m.WriteP99() <= Targets.SustainedP99 && m.WriteErrorRate() <= Targets.SustainedErrRate
		status := "PASS"
		if !pass {
			status = "FAIL"
			allPass = false
		}
		fmt.Printf("Sustained:  %s (p99: %v, %.0f/sec, errors: %d)\n",
			status, m.WriteP99(), m.WriteThroughput(), m.WriteErrors())
	}

	if results.ConcurrentMetrics != nil {
		m := results.ConcurrentMetrics
		pass := m.DegradationPct() <= Targets.DegradationPct
		status := "PASS"
		if !pass {
			status = "FAIL"
			allPass = false
		}
		fmt.Printf("Concurrent: %s (degradation: %.1f%%, readers: %d)\n",
			status, m.DegradationPct(), m.ReaderCount())
	}

	fmt.Println()
	if allPass {
		fmt.Println("Overall: PASS - System ready for production load")
	} else {
		fmt.Println("Overall: FAIL - See report for recommendations")
	}
}
