// This file implements the local CLI for replay, O11Y acquisition, and cache seeding.
package main

import (
	"context"
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/test/evaluation"
)

const (
	defaultTokenEnvironment     = "O11Y_AUTH0_TOKEN"
	defaultClinicKeyEnvironment = "CLINIC_API_KEY"
	defaultClinicEndpoint       = "https://clinic.pingcap.com"
	defaultO11YGlobalEndpoint   = "http://www.gs.us-west-2.aws.observability.tidbcloud.com"
	defaultRequestTimeout       = 30 * time.Second
	defaultCasesDir             = "test/evaluation/cases"
)

func main() {
	os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}

func run(args []string, stdout, stderr io.Writer) int {
	if len(args) == 0 {
		printUsage(stderr)
		return 2
	}

	switch args[0] {
	case "run":
		return runReplay(args[1:], stdout, stderr)
	case "fetch-o11y":
		return runFetchO11Y(args[1:], stdout, stderr)
	case "fetch-o11y-replay":
		return runFetchO11YReplay(args[1:], stdout, stderr)
	case "fetch-o11y-suite":
		return runFetchO11YSuite(args[1:], stdout, stderr)
	case "seed-cache":
		return runSeedCache(args[1:], stdout, stderr)
	default:
		fmt.Fprintf(stderr, "unknown command %q\n", args[0])
		printUsage(stderr)
		return 2
	}
}

func runReplay(args []string, stdout, stderr io.Writer) int {
	flags := flag.NewFlagSet("run", flag.ContinueOnError)
	flags.SetOutput(stderr)
	experimentPath := flags.String("experiment", "", "path to a replay experiment YAML")
	outputDir := flags.String("output", "", "directory for replay report.json and report.md")
	policyRevision := flags.String("policy-revision", "", "policy source revision (default current Git worktree)")
	cacheDir := flags.String("cache-dir", "", "directory for reusable raw O11Y responses (default <output>/cache)")
	tokenEnvironment := flags.String("token-env", defaultTokenEnvironment, "environment variable containing the O11Y bearer token")
	clinicKeyEnvironment := flags.String("clinic-key-env", defaultClinicKeyEnvironment, "environment variable containing the Clinic API key")
	clinicEndpoint := flags.String("clinic-endpoint", defaultClinicEndpoint, "Clinic API endpoint used when local replay data is missing")
	o11yGlobalEndpoint := flags.String("o11y-global-endpoint", defaultO11YGlobalEndpoint, "O11Y Global Server endpoint used for datasource discovery")
	timeout := flags.Duration("timeout", defaultRequestTimeout, "timeout for each automatic discovery or O11Y request")
	if err := flags.Parse(args); err != nil {
		return 2
	}
	if *experimentPath == "" || *outputDir == "" {
		fmt.Fprintln(stderr, "run requires --experiment and --output")
		return 2
	}
	if *timeout <= 0 {
		fmt.Fprintln(stderr, "run --timeout must be greater than zero")
		return 2
	}

	experiment, err := evaluation.LoadReplayExperiment(*experimentPath)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}
	resolvedPolicyRevision := *policyRevision
	if resolvedPolicyRevision == "" {
		resolvedPolicyRevision = workingTreeRevision()
	}
	resolvedCacheDir := *cacheDir
	if resolvedCacheDir == "" {
		resolvedCacheDir = filepath.Join(*outputDir, "cache")
	}
	offlineReport, err := evaluation.RunOfflineEvaluationWithOptions(context.Background(), experiment, resolvedPolicyRevision, evaluation.ReplayDatasetResolveOptions{
		Client:             &http.Client{Timeout: *timeout},
		ClinicEndpoint:     *clinicEndpoint,
		ClinicAPIKey:       os.Getenv(*clinicKeyEnvironment),
		O11YGlobalEndpoint: *o11yGlobalEndpoint,
		O11YBearerToken:    os.Getenv(*tokenEnvironment),
		CacheDir:           resolvedCacheDir,
	})
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}
	report := offlineReport.Replay
	jsonPath, markdownPath, err := evaluation.WriteReplayReport(*outputDir, report)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}
	offlinePath, err := evaluation.WriteOfflineEvaluationReport(*outputDir, offlineReport)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}

	fmt.Fprintf(stdout, "completed %d/%d scheduled replay evaluations; skipped %d; policy errors %d\n",
		report.Summary.EvaluatedPoints, report.Summary.ScheduledPoints, report.Summary.SkippedPoints, report.Summary.PolicyErrorPoints)
	fmt.Fprintf(stdout, "wrote %s, %s, and %s\n", jsonPath, markdownPath, offlinePath)
	return 0
}

func runFetchO11Y(args []string, stdout, stderr io.Writer) int {
	flags := flag.NewFlagSet("fetch-o11y", flag.ContinueOnError)
	flags.SetOutput(stderr)
	casePath := flags.String("case", "", "path to an O11Y YAML case")
	casesDir := flags.String("cases-dir", defaultCasesDir, "root directory containing component cases and metrics profiles")
	outputDir := flags.String("output", "", "root directory for component-grouped O11Y archives")
	caseOutput := flags.String("case-output", "", "optional path for the derived normalized case")
	cacheDir := flags.String("cache-dir", "", "directory for reusable raw O11Y responses (default <output>/cache)")
	tokenEnvironment := flags.String("token-env", defaultTokenEnvironment, "environment variable containing the O11Y bearer token")
	noAuth := flags.Bool("no-auth", false, "omit authorization for an authenticated local proxy")
	timeout := flags.Duration("timeout", defaultRequestTimeout, "timeout for each O11Y request")
	if err := flags.Parse(args); err != nil {
		return 2
	}
	if *casePath == "" || *outputDir == "" {
		fmt.Fprintln(stderr, "fetch-o11y requires --case and --output")
		return 2
	}
	if *timeout <= 0 {
		fmt.Fprintln(stderr, "fetch-o11y --timeout must be greater than zero")
		return 2
	}

	spec, err := loadO11YExportSpec(*casePath, *casesDir)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}

	bearerToken := ""
	if !*noAuth {
		bearerToken = os.Getenv(*tokenEnvironment)
	}
	resolvedCacheDir := *cacheDir
	if resolvedCacheDir == "" {
		resolvedCacheDir = filepath.Join(*outputDir, "cache")
	}
	archivePath := filepath.Join(*outputDir, spec.Case.Component, evaluation.PointInTimeCasesDirectory, spec.Case.ID+".json")

	_, archive, err := evaluation.FetchO11Y(context.Background(), spec, evaluation.O11YExportOptions{
		Client:             &http.Client{Timeout: *timeout},
		BearerToken:        bearerToken,
		RequireBearerToken: !*noAuth,
		ArchivePath:        archivePath,
		CasePath:           *caseOutput,
		CacheDir:           resolvedCacheDir,
	})
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}

	cacheHits := 0
	for i := range archive.Queries {
		if archive.Queries[i].CacheHit {
			cacheHits++
		}
	}
	fmt.Fprintf(stdout, "wrote %s; cache hits %d/%d\n", archivePath, cacheHits, len(archive.Queries))
	if *caseOutput != "" {
		fmt.Fprintf(stdout, "wrote derived case %s\n", *caseOutput)
	}
	return 0
}

func runFetchO11YReplay(args []string, stdout, stderr io.Writer) int {
	flags := flag.NewFlagSet("fetch-o11y-replay", flag.ContinueOnError)
	flags.SetOutput(stderr)
	datasetPath := flags.String("dataset", "", "path to a replay dataset YAML")
	casesDir := flags.String("cases-dir", defaultCasesDir, "root directory containing component cases and metrics profiles")
	outputDir := flags.String("output", "", "root directory for component-grouped replay archives")
	cacheDir := flags.String("cache-dir", "", "directory for reusable raw O11Y responses (default <output>/cache)")
	tokenEnvironment := flags.String("token-env", defaultTokenEnvironment, "environment variable containing the O11Y bearer token")
	noAuth := flags.Bool("no-auth", false, "omit authorization for an authenticated local proxy")
	timeout := flags.Duration("timeout", defaultRequestTimeout, "timeout for each O11Y request")
	if err := flags.Parse(args); err != nil {
		return 2
	}
	if *datasetPath == "" || *outputDir == "" {
		fmt.Fprintln(stderr, "fetch-o11y-replay requires --dataset and --output")
		return 2
	}
	if *timeout <= 0 {
		fmt.Fprintln(stderr, "fetch-o11y-replay --timeout must be greater than zero")
		return 2
	}

	spec, err := loadO11YDatasetExportSpec(*datasetPath, *casesDir)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}
	bearerToken := ""
	if !*noAuth {
		bearerToken = os.Getenv(*tokenEnvironment)
	}
	resolvedCacheDir := *cacheDir
	if resolvedCacheDir == "" {
		resolvedCacheDir = filepath.Join(*outputDir, "cache")
	}
	archivePath := filepath.Join(*outputDir, spec.Component, "replays", spec.ID+".json")
	archive, err := evaluation.FetchO11YDataset(context.Background(), spec, evaluation.O11YDatasetExportOptions{
		Client:             &http.Client{Timeout: *timeout},
		BearerToken:        bearerToken,
		RequireBearerToken: !*noAuth,
		ArchivePath:        archivePath,
		CacheDir:           resolvedCacheDir,
	})
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}

	cacheHits := 0
	for i := range archive.Queries {
		if archive.Queries[i].CacheHit {
			cacheHits++
		}
	}
	fmt.Fprintf(stdout, "wrote %s; cache hits %d/%d\n", archivePath, cacheHits, len(archive.Queries))
	return 0
}

func runFetchO11YSuite(args []string, stdout, stderr io.Writer) int {
	flags := flag.NewFlagSet("fetch-o11y-suite", flag.ContinueOnError)
	flags.SetOutput(stderr)
	casesDir := flags.String("cases-dir", defaultCasesDir, "root directory containing component cases and metrics profiles")
	outputDir := flags.String("output", "", "root directory for per-case outputs")
	cacheDir := flags.String("cache-dir", "", "directory for reusable raw O11Y responses (default <output>/cache)")
	tokenEnvironment := flags.String("token-env", defaultTokenEnvironment, "environment variable containing the O11Y bearer token")
	noAuth := flags.Bool("no-auth", false, "omit authorization for an authenticated local proxy")
	timeout := flags.Duration("timeout", defaultRequestTimeout, "timeout for each O11Y request")
	workers := flags.Int("workers", evaluation.DefaultO11YSuiteWorkers, "maximum concurrent cases")
	if err := flags.Parse(args); err != nil {
		return 2
	}
	if *outputDir == "" {
		fmt.Fprintln(stderr, "fetch-o11y-suite requires --output")
		return 2
	}
	if *timeout <= 0 || *workers <= 0 {
		fmt.Fprintln(stderr, "fetch-o11y-suite --timeout and --workers must be greater than zero")
		return 2
	}

	paths, err := filepath.Glob(filepath.Join(*casesDir, "*", evaluation.PointInTimeCasesDirectory, "*.yaml"))
	if err != nil {
		fmt.Fprintf(stderr, "glob cases: %v\n", err)
		return 1
	}
	if len(paths) == 0 {
		fmt.Fprintln(stderr, "fetch-o11y-suite found no cases")
		return 1
	}
	sort.Strings(paths)
	specs := make([]*evaluation.O11YExportSpec, 0, len(paths))
	results := make([]evaluation.O11YSuiteResult, 0, len(paths))
	for _, path := range paths {
		spec, loadErr := loadO11YExportSpec(path, *casesDir)
		if loadErr != nil {
			caseID := filepath.Base(path)
			caseID = caseID[:len(caseID)-len(filepath.Ext(caseID))]
			results = append(results, evaluation.O11YSuiteResult{
				CaseID: caseID,
				Error:  loadErr.Error(),
			})
			continue
		}
		specs = append(specs, spec)
	}
	loadFailureCount := len(results)

	bearerToken := ""
	if !*noAuth {
		bearerToken = os.Getenv(*tokenEnvironment)
	}
	resolvedCacheDir := *cacheDir
	if resolvedCacheDir == "" {
		resolvedCacheDir = filepath.Join(*outputDir, "cache")
	}
	var suiteErr error
	if len(specs) > 0 {
		fetched, fetchErr := evaluation.FetchO11YSuite(context.Background(), specs, evaluation.O11YSuiteOptions{
			Client:             &http.Client{Timeout: *timeout},
			BearerToken:        bearerToken,
			RequireBearerToken: !*noAuth,
			OutputDir:          *outputDir,
			CacheDir:           resolvedCacheDir,
			Workers:            *workers,
		})
		results = append(results, fetched...)
		suiteErr = fetchErr
	}
	sort.Slice(results, func(i, j int) bool {
		return results[i].CaseID < results[j].CaseID
	})
	for i := range results {
		if results[i].Error != "" {
			fmt.Fprintf(stderr, "%s failed: %s\n", results[i].CaseID, results[i].Error)
			continue
		}
		fmt.Fprintf(stdout, "%s wrote %s; cache hits %d/%d\n", results[i].CaseID, results[i].ArchivePath, results[i].CacheHits, results[i].QueryCount)
	}
	if suiteErr != nil || loadFailureCount > 0 {
		if suiteErr != nil {
			fmt.Fprintln(stderr, suiteErr)
		}
		if loadFailureCount > 0 {
			fmt.Fprintf(stderr, "failed to load %d/%d cases\n", loadFailureCount, len(paths))
		}
		return 1
	}
	return 0
}

func runSeedCache(args []string, stdout, stderr io.Writer) int {
	flags := flag.NewFlagSet("seed-cache", flag.ContinueOnError)
	flags.SetOutput(stderr)
	casesDir := flags.String("cases-dir", defaultCasesDir, "root directory containing component case directories")
	cacheDir := flags.String("cache-dir", "", "cache directory to populate")
	if err := flags.Parse(args); err != nil {
		return 2
	}
	if *cacheDir == "" {
		fmt.Fprintln(stderr, "seed-cache requires --cache-dir")
		return 2
	}

	seeded, err := evaluation.SeedO11YCache(*casesDir, *cacheDir)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 1
	}
	fmt.Fprintf(stdout, "seeded %d responses into %s\n", seeded, *cacheDir)
	return 0
}

func loadO11YExportSpec(casePath, casesDir string) (*evaluation.O11YExportSpec, error) {
	caseFile, err := os.Open(casePath)
	if err != nil {
		return nil, fmt.Errorf("open case: %w", err)
	}
	caseSpec, decodeErr := evaluation.DecodeCaseSpec(caseFile)
	closeErr := caseFile.Close()
	if decodeErr != nil {
		return nil, decodeErr
	}
	if closeErr != nil {
		return nil, fmt.Errorf("close case: %w", closeErr)
	}
	profile, err := evaluation.LoadMetricsProfile(casesDir, caseSpec.Metrics.Profile)
	if err != nil {
		return nil, err
	}
	return evaluation.BuildO11YExportSpec(caseSpec, profile)
}

func loadO11YDatasetExportSpec(datasetPath, casesDir string) (*evaluation.O11YDatasetExportSpec, error) {
	file, err := os.Open(datasetPath)
	if err != nil {
		return nil, fmt.Errorf("open replay dataset: %w", err)
	}
	datasetSpec, decodeErr := evaluation.DecodeReplayDatasetSpec(file)
	closeErr := file.Close()
	if decodeErr != nil {
		return nil, decodeErr
	}
	if closeErr != nil {
		return nil, fmt.Errorf("close replay dataset: %w", closeErr)
	}
	profile, err := evaluation.LoadMetricsProfile(casesDir, datasetSpec.Profile)
	if err != nil {
		return nil, err
	}
	return evaluation.BuildO11YDatasetExportSpec(datasetSpec, profile)
}

func workingTreeRevision() string {
	revisionOutput, err := exec.Command("git", "rev-parse", "HEAD").Output()
	if err != nil {
		return "unknown"
	}
	revision := strings.TrimSpace(string(revisionOutput))
	if revision == "" {
		return "unknown"
	}

	statusOutput, err := exec.Command("git", "status", "--porcelain").Output()
	if err == nil && len(statusOutput) > 0 {
		return revision + "-dirty"
	}
	return revision
}

func printUsage(w io.Writer) {
	fmt.Fprintln(w, "usage:")
	fmt.Fprintln(w, "  autoscaling-eval run --experiment <path> --output <dir> [--cache-dir <dir>]")
	fmt.Fprintln(w, "  autoscaling-eval fetch-o11y --case <path> --output <dir> [--case-output <path>] [--cases-dir <dir>] [--cache-dir <dir>]")
	fmt.Fprintln(w, "  autoscaling-eval fetch-o11y-replay --dataset <path> --output <dir> [--cases-dir <dir>] [--cache-dir <dir>]")
	fmt.Fprintln(w, "  autoscaling-eval fetch-o11y-suite --output <dir> [--cases-dir <dir>] [--cache-dir <dir>] [--workers 4]")
	fmt.Fprintln(w, "  autoscaling-eval seed-cache --cache-dir <dir> [--cases-dir <dir>]")
}
