// This file discovers point-in-time cases and builds a deterministic evaluation catalog.
package evaluation

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	productionpolicy "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/policy"
)

// PointInTimeCasesDirectory contains independent single-decision cases.
const PointInTimeCasesDirectory = "point-in-time"

// Catalog is a deterministic collection of replayed evaluation cases.
type Catalog struct {
	Cases []*CaseBundle
}

// LoadCatalog loads manual cases and replays O11Y cases from same-ID archives.
func LoadCatalog(casesDir string) (*Catalog, error) {
	if casesDir == "" {
		return nil, fmt.Errorf("evaluation: cases directory is required")
	}

	paths, err := filepath.Glob(filepath.Join(casesDir, "*", PointInTimeCasesDirectory, "*.yaml"))
	if err != nil {
		return nil, fmt.Errorf("evaluation: scan cases: %w", err)
	}
	if len(paths) == 0 {
		return nil, fmt.Errorf("evaluation: no cases under %q", casesDir)
	}
	sort.Strings(paths)

	catalog := &Catalog{Cases: make([]*CaseBundle, 0, len(paths))}
	seen := make(map[string]struct{}, len(paths))
	for _, path := range paths {
		spec, err := loadCaseSpec(path)
		if err != nil {
			return nil, err
		}
		caseID := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
		if spec.ID != caseID {
			return nil, fmt.Errorf("evaluation: case file %q has id %q", path, spec.ID)
		}
		if component := filepath.Base(filepath.Dir(filepath.Dir(path))); spec.Component != component {
			return nil, fmt.Errorf("evaluation: case %q component %q, want directory %q", spec.ID, spec.Component, component)
		}
		if _, duplicate := seen[spec.ID]; duplicate {
			return nil, fmt.Errorf("evaluation: case id %q is duplicated", spec.ID)
		}
		seen[spec.ID] = struct{}{}

		bundle, err := loadCaseBundle(casesDir, path, spec)
		if err != nil {
			return nil, fmt.Errorf("evaluation: load case %q: %w", spec.ID, err)
		}
		catalog.Cases = append(catalog.Cases, bundle)
	}

	return catalog, nil
}

// EvaluateCatalog runs one production policy over every replayed case.
func EvaluateCatalog(ctx context.Context, catalog *Catalog, candidate productionpolicy.Policy) (*SuiteResult, error) {
	if catalog == nil {
		return nil, fmt.Errorf("evaluation: catalog is required")
	}

	return EvaluateSuite(ctx, catalog.Cases, candidate)
}

func loadCaseSpec(path string) (*CaseSpec, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("evaluation: open case %q: %w", path, err)
	}
	spec, decodeErr := DecodeCaseSpec(file)
	closeErr := file.Close()
	if decodeErr != nil {
		return nil, decodeErr
	}
	if closeErr != nil {
		return nil, fmt.Errorf("evaluation: close case %q: %w", path, closeErr)
	}

	return spec, nil
}

func loadCaseBundle(casesDir, casePath string, spec *CaseSpec) (*CaseBundle, error) {
	switch spec.Metrics.Type {
	case DataSourceManual:
		return BuildManualCaseBundle(spec)
	case DataSourceO11Y:
		profile, err := LoadMetricsProfile(casesDir, spec.Metrics.Profile)
		if err != nil {
			return nil, err
		}
		exportSpec, err := BuildO11YExportSpec(spec, profile)
		if err != nil {
			return nil, err
		}
		archivePath := strings.TrimSuffix(casePath, filepath.Ext(casePath)) + ".json"
		archiveFile, err := os.Open(archivePath)
		if err != nil {
			return nil, fmt.Errorf("evaluation: open O11Y archive %q: %w", archivePath, err)
		}
		archive, decodeErr := DecodeO11YArchive(archiveFile)
		closeErr := archiveFile.Close()
		if decodeErr != nil {
			return nil, decodeErr
		}
		if closeErr != nil {
			return nil, fmt.Errorf("evaluation: close O11Y archive %q: %w", archivePath, closeErr)
		}

		return ReplayO11YArchive(exportSpec, archive)
	default:
		return nil, fmt.Errorf("evaluation: case metrics type %q is unsupported", spec.Metrics.Type)
	}
}
