// This file resolves replay datasets locally and acquires missing archives from O11Y.
package evaluation

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"sigs.k8s.io/yaml"
)

const (
	ReplayDatasetsDirectory   = "replays"
	defaultClinicEndpoint     = "https://clinic.pingcap.com"
	defaultO11YGlobalEndpoint = "http://www.gs.us-west-2.aws.observability.tidbcloud.com"
	replayDatasetTimestamp    = "20060102T150405Z"
	replayDatasetIDHashLength = 12
	defaultReferenceMaxGap    = 120
)

type ReplayDatasetResolveOptions struct {
	Client             *http.Client
	ClinicEndpoint     string
	ClinicAPIKey       string
	O11YGlobalEndpoint string
	O11YBearerToken    string
	CacheDir           string
}

func ResolveReplayDataset(ctx context.Context, experiment *ReplayExperiment, options ReplayDatasetResolveOptions) (*ReplayDataset, bool, error) {
	if experiment == nil {
		return nil, false, fmt.Errorf("evaluation: replay experiment is required")
	}
	if err := validateReplayDatasetSelector(experiment); err != nil {
		return nil, false, err
	}

	profile, err := LoadMetricsProfile(experiment.CasesDir, experiment.Spec.MetricsProfile)
	if err != nil {
		return nil, false, err
	}
	paths, err := matchingReplayDatasetPaths(experiment)
	if err != nil {
		return nil, false, err
	}
	var spec *ReplayDatasetSpec
	var specPath string
	switch len(paths) {
	case 1:
		dataset, err := LoadReplayDataset(paths[0], experiment.CasesDir)
		if err == nil {
			selected, err := selectReplayDataset(experiment, dataset)
			return selected, false, err
		}
		if !errors.Is(err, os.ErrNotExist) {
			return nil, false, err
		}
		specPath = paths[0]
		spec, err = loadReplayDatasetSpec(specPath)
		if err != nil {
			return nil, false, err
		}
	case 0:
		spec, err = DiscoverReplayDatasetSpec(ctx, experiment, profile, options)
		if err != nil {
			return nil, false, fmt.Errorf("evaluation: local replay dataset not found and automatic O11Y discovery failed: %w", err)
		}
		replaysDir := filepath.Join(experiment.CasesDir, spec.Component, ReplayDatasetsDirectory)
		if err := os.MkdirAll(replaysDir, 0o755); err != nil {
			return nil, false, fmt.Errorf("evaluation: create replay dataset directory: %w", err)
		}
		specPath = filepath.Join(replaysDir, spec.ID+".yaml")
	default:
		return nil, false, fmt.Errorf("evaluation: replay target %q window %s to %s matches multiple local datasets: %s",
			experiment.Spec.Target.ClusterID,
			experiment.Spec.Window.Start.Format(timeLayout),
			experiment.Spec.Window.End.Format(timeLayout),
			strings.Join(paths, ", "))
	}

	exportSpec, err := BuildO11YDatasetExportSpec(spec, profile)
	if err != nil {
		return nil, false, err
	}
	archivePath := strings.TrimSuffix(specPath, filepath.Ext(specPath)) + ".json"
	archive, err := FetchO11YDataset(ctx, exportSpec, O11YDatasetExportOptions{
		Client:             options.Client,
		BearerToken:        options.O11YBearerToken,
		RequireBearerToken: true,
		ArchivePath:        archivePath,
		CacheDir:           options.CacheDir,
	})
	if err != nil {
		return nil, false, fmt.Errorf("evaluation: automatically fetch replay dataset: %w", err)
	}
	if _, err := os.Stat(specPath); errors.Is(err, os.ErrNotExist) {
		if err := writeReplayDatasetSpec(specPath, spec); err != nil {
			return nil, false, err
		}
	} else if err != nil {
		return nil, false, err
	}

	data, err := os.ReadFile(archivePath)
	if err != nil {
		return nil, false, fmt.Errorf("evaluation: read fetched replay dataset archive: %w", err)
	}
	dataset, err := ReplayO11YDatasetArchive(exportSpec, archive, digestBytes(data))
	if err != nil {
		return nil, false, err
	}
	selected, err := selectReplayDataset(experiment, dataset)
	return selected, true, err
}

func loadReplayDatasetSpec(path string) (*ReplayDatasetSpec, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("evaluation: open replay dataset spec %q: %w", path, err)
	}
	spec, decodeErr := DecodeReplayDatasetSpec(file)
	closeErr := file.Close()
	if decodeErr != nil {
		return nil, decodeErr
	}
	if closeErr != nil {
		return nil, fmt.Errorf("evaluation: close replay dataset spec %q: %w", path, closeErr)
	}
	return spec, nil
}

func matchingReplayDatasetPaths(experiment *ReplayExperiment) ([]string, error) {
	pattern := filepath.Join(experiment.CasesDir, experiment.Spec.Component, ReplayDatasetsDirectory, "*.yaml")
	paths, err := filepath.Glob(pattern)
	if err != nil {
		return nil, fmt.Errorf("evaluation: list replay datasets: %w", err)
	}
	sort.Strings(paths)

	matches := make([]string, 0, 1)
	for _, path := range paths {
		spec, err := loadReplayDatasetSpec(path)
		if err != nil {
			return nil, err
		}
		if replayDatasetSpecMatchesExperiment(spec, &experiment.Spec) {
			matches = append(matches, path)
		}
	}
	return matches, nil
}

func replayDatasetSpecMatchesExperiment(dataset *ReplayDatasetSpec, experiment *ReplayExperimentSpec) bool {
	return dataset.Component == experiment.Component &&
		dataset.Profile == experiment.MetricsProfile &&
		dataset.Target.ClusterID == experiment.Target.ClusterID &&
		dataset.Window.StepSeconds == experiment.DataStepSeconds &&
		!experiment.Window.Start.Before(dataset.Window.Start) &&
		!experiment.Window.End.After(dataset.Window.End)
}

func validateReplayDatasetSelector(experiment *ReplayExperiment) error {
	spec := &experiment.Spec
	if experiment.CasesDir == "" || spec.Component == "" || spec.MetricsProfile == "" || spec.Target.ClusterID == "" {
		return fmt.Errorf("evaluation: replay cases directory, component, metrics profile, and cluster ID are required")
	}
	if spec.Window.Start.IsZero() || spec.Window.End.IsZero() || !spec.Window.Start.Before(spec.Window.End) {
		return fmt.Errorf("evaluation: replay window start must be before end")
	}
	if spec.DataStepSeconds < 1 {
		return fmt.Errorf("evaluation: replay data step must be greater than zero")
	}
	return nil
}

func writeReplayDatasetSpec(path string, spec *ReplayDatasetSpec) error {
	data, err := yaml.Marshal(spec)
	if err != nil {
		return fmt.Errorf("evaluation: encode replay dataset spec: %w", err)
	}
	if err := os.WriteFile(path, data, 0o644); err != nil {
		return fmt.Errorf("evaluation: write replay dataset spec: %w", err)
	}
	return nil
}

func newReplayDatasetID(component, clusterID, profile string, window O11YQueryWindow) string {
	identity := strings.Join([]string{component, clusterID, profile, fmt.Sprint(window.StepSeconds)}, "\n")
	hash := digestBytes([]byte(identity))
	return fmt.Sprintf("%s-continuous-%s-%s-%s",
		component,
		window.Start.UTC().Format(replayDatasetTimestamp),
		window.End.UTC().Format(replayDatasetTimestamp),
		hash[:replayDatasetIDHashLength])
}
