// This file loads metric-query profiles referenced by point-in-time and replay data.
package evaluation

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"sigs.k8s.io/yaml"
)

const (
	MetricsProfileVersionV1Alpha1      = "v1alpha1"
	maxMetricsProfileBytes             = 1 << 20
	metricsProfileClusterIDPlaceholder = "${cluster_id}"
)

// MetricsProfileSource contains reusable query definitions for one O11Y source.
type MetricsProfileSource struct {
	Type    O11YMetricSource `json:"type"`
	Queries []O11YQuerySpec  `json:"queries"`
}

// MetricsProfile is an immutable, versioned set of component metrics.
type MetricsProfile struct {
	Version   string                 `json:"version"`
	ID        string                 `json:"id"`
	Component string                 `json:"component"`
	Sources   []MetricsProfileSource `json:"sources"`
}

// DecodeMetricsProfile strictly decodes one shared metrics profile.
func DecodeMetricsProfile(r io.Reader) (*MetricsProfile, error) {
	data, err := io.ReadAll(io.LimitReader(r, maxMetricsProfileBytes+1))
	if err != nil {
		return nil, fmt.Errorf("evaluation: read metrics profile: %w", err)
	}
	if len(data) > maxMetricsProfileBytes {
		return nil, fmt.Errorf("evaluation: metrics profile exceeds %d bytes", maxMetricsProfileBytes)
	}

	var profile MetricsProfile
	if err := yaml.UnmarshalStrict(data, &profile); err != nil {
		return nil, fmt.Errorf("evaluation: decode metrics profile: %w", err)
	}
	if err := validateMetricsProfile(&profile); err != nil {
		return nil, err
	}
	return &profile, nil
}

// LoadMetricsProfile resolves a logical profile reference under one cases root.
func LoadMetricsProfile(rootDir, reference string) (*MetricsProfile, error) {
	if rootDir == "" {
		return nil, fmt.Errorf("evaluation: cases root is required")
	}
	if !isMetricsProfileReference(reference) {
		return nil, fmt.Errorf("evaluation: metrics profile reference %q is invalid", reference)
	}

	parts := strings.Split(reference, "/")
	path := filepath.Join(rootDir, parts[0], "metrics", parts[1]+".yaml")
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("evaluation: open metrics profile %q: %w", reference, err)
	}
	profile, decodeErr := DecodeMetricsProfile(file)
	closeErr := file.Close()
	if decodeErr != nil {
		return nil, decodeErr
	}
	if closeErr != nil {
		return nil, fmt.Errorf("evaluation: close metrics profile %q: %w", reference, closeErr)
	}
	if profile.ID != reference {
		return nil, fmt.Errorf("evaluation: metrics profile id %q, want reference %q", profile.ID, reference)
	}
	return profile, nil
}

func validateMetricsProfile(profile *MetricsProfile) error {
	if profile == nil {
		return fmt.Errorf("evaluation: metrics profile is required")
	}
	if profile.Version != MetricsProfileVersionV1Alpha1 {
		return fmt.Errorf("evaluation: metrics profile version %q is unsupported", profile.Version)
	}
	if !isMetricsProfileReference(profile.ID) {
		return fmt.Errorf("evaluation: metrics profile id %q is invalid", profile.ID)
	}
	if profile.Component == "" {
		return fmt.Errorf("evaluation: metrics profile component is required")
	}
	if len(profile.Sources) == 0 {
		return fmt.Errorf("evaluation: metrics profile sources are required")
	}

	queryNames := make(map[string]struct{})
	sourceTypes := make(map[O11YMetricSource]struct{}, len(profile.Sources))
	for i := range profile.Sources {
		source := &profile.Sources[i]
		switch source.Type {
		case O11YMetricSourceApplication, O11YMetricSourceInfrastructure:
		default:
			return fmt.Errorf("evaluation: metrics profile sources[%d].type %q is unsupported", i, source.Type)
		}
		if _, duplicate := sourceTypes[source.Type]; duplicate {
			return fmt.Errorf("evaluation: metrics profile source %q is duplicated", source.Type)
		}
		sourceTypes[source.Type] = struct{}{}
		if err := validateO11YQueries(i, source.Queries, queryNames); err != nil {
			return err
		}
		for j := range source.Queries {
			promQL := source.Queries[j].PromQL
			if strings.Contains(strings.ReplaceAll(promQL, metricsProfileClusterIDPlaceholder, ""), "${") {
				return fmt.Errorf("evaluation: metrics profile query %q contains an unsupported placeholder", source.Queries[j].Name)
			}
			if !strings.Contains(promQL, metricsProfileClusterIDPlaceholder) {
				return fmt.Errorf("evaluation: metrics profile query %q must contain %s", source.Queries[j].Name, metricsProfileClusterIDPlaceholder)
			}
		}
	}
	return nil
}

func isMetricsProfileReference(reference string) bool {
	parts := strings.Split(reference, "/")
	return len(parts) == 2 && isSafeQueryName(parts[0]) && isSafeQueryName(parts[1])
}
