// Package config owns the capacity-planner runtime configuration file.
package config

import (
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/alecthomas/units"
	commonmodel "github.com/prometheus/common/model"
	"github.com/prometheus/prometheus/promql/parser"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/util/jsonpath"
	"sigs.k8s.io/yaml"
)

const (
	APIVersion = "capacity-planner.tidbcloud.com/v1alpha1"
	Kind       = "CapacityPlannerConfiguration"
)

// Config is the complete capacity-planner runtime configuration. Tier
// templates intentionally remain in their existing independent YAML file.
type Config struct {
	APIVersion string           `json:"apiVersion"`
	Kind       string           `json:"kind"`
	Controller ControllerConfig `json:"controller"`
	Server     ServerConfig     `json:"server"`
	Metrics    MetricsConfig    `json:"metrics"`
	Storage    StorageConfig    `json:"storage"`
	Query      QueryConfig      `json:"query"`
	Records    RecordsConfig    `json:"records"`
	Scrapers   ScrapersConfig   `json:"scrapers"`
	Rules      RulesConfig      `json:"rules"`
	Logging    LoggingConfig    `json:"logging"`
}

type ControllerConfig struct {
	LeaderElection bool            `json:"leaderElection"`
	SyncPeriod     metav1.Duration `json:"syncPeriod"`
}

type ServerConfig struct {
	Health HealthServerConfig `json:"health"`
}

type HealthServerConfig struct {
	BindAddress string `json:"bindAddress"`
}

// MetricsConfig configures only the capacity-planner's own metrics endpoint.
type MetricsConfig struct {
	BindAddress string `json:"bindAddress"`
}

type StorageConfig struct {
	Path      string          `json:"path"`
	Retention metav1.Duration `json:"retention"`
}

type QueryConfig struct {
	BindAddress string          `json:"bindAddress"`
	Timeout     metav1.Duration `json:"timeout"`
	MaxSamples  int             `json:"maxSamples"`
	MaxPoints   int             `json:"maxPoints"`
}

type RecordsConfig struct {
	Dir string `json:"dir"`
}

type ScrapersConfig struct {
	Interval      metav1.Duration        `json:"interval"`
	MetricsServer MetricsServerConfig    `json:"metricsServer"`
	PodWatcher    PodWatcherConfig       `json:"podWatcher"`
	PodMetrics    DirectPodMetricsConfig `json:"podMetrics"`
}

type MetricsServerConfig struct {
	MaxAge    metav1.Duration   `json:"maxAge"`
	Resources map[string]string `json:"resources"`
}

type PodWatcherConfig struct {
	Metrics []PodWatcherMetric `json:"metrics"`
}

type PodWatcherMetric struct {
	Name       string `json:"name"`
	JSONPath   string `json:"jsonPath"`
	MatchValue string `json:"matchValue"`
}

type DirectPodMetricsConfig struct {
	Timeout     metav1.Duration  `json:"timeout"`
	MaxBodySize units.Base2Bytes `json:"maxBodySize"`
	Concurrency int              `json:"concurrency"`
	Metrics     []string         `json:"metrics"`
}

type RulesConfig struct {
	Interval metav1.Duration  `json:"interval"`
	Engine   RuleEngineConfig `json:"engine"`
	Records  []RecordingRule  `json:"records"`
}

type RuleEngineConfig struct {
	Timeout       metav1.Duration `json:"timeout"`
	MaxSamples    int             `json:"maxSamples"`
	LookbackDelta metav1.Duration `json:"lookbackDelta"`
}

type RecordingRule struct {
	Record string            `json:"record"`
	Expr   string            `json:"expr"`
	Labels map[string]string `json:"labels,omitempty"`
}

type LoggingConfig struct {
	Development     bool   `json:"development"`
	Encoder         string `json:"encoder"`
	Level           string `json:"level"`
	StacktraceLevel string `json:"stacktraceLevel"`
	TimeEncoding    string `json:"timeEncoding"`
}

// Default returns the safe standalone defaults used when no config file is
// supplied. The Helm chart always renders an explicit production config.
func Default() *Config {
	return &Config{
		APIVersion: APIVersion,
		Kind:       Kind,
		Controller: ControllerConfig{
			LeaderElection: false,
			SyncPeriod:     metav1.Duration{Duration: 30 * time.Second},
		},
		Server:  ServerConfig{Health: HealthServerConfig{BindAddress: ":8081"}},
		Metrics: MetricsConfig{BindAddress: "0"},
		Storage: StorageConfig{
			Path:      "/tmp/capacity-planner-tsdb",
			Retention: metav1.Duration{Duration: time.Hour},
		},
		Query: QueryConfig{
			BindAddress: "0",
			Timeout:     metav1.Duration{Duration: 10 * time.Second},
			MaxSamples:  50000,
			MaxPoints:   11000,
		},
		Records: RecordsConfig{Dir: "/tmp/capacity-planner-records"},
		Scrapers: ScrapersConfig{
			Interval: metav1.Duration{Duration: 15 * time.Second},
			MetricsServer: MetricsServerConfig{
				MaxAge: metav1.Duration{Duration: 2 * time.Minute},
				Resources: map[string]string{
					"cpu":    "cpu",
					"memory": "memory",
				},
			},
			PodWatcher: PodWatcherConfig{Metrics: []PodWatcherMetric{
				{Name: "pod_status_ready", JSONPath: `.status.conditions[?(@.type=="Ready")].status`, MatchValue: "True"},
				{Name: "pod_status_running", JSONPath: ".status.phase", MatchValue: "Running"},
			}},
			PodMetrics: DirectPodMetricsConfig{
				Timeout:     metav1.Duration{Duration: 10 * time.Second},
				MaxBodySize: units.Base2Bytes(32 * units.MiB),
				Concurrency: 16,
			},
		},
		Rules: RulesConfig{
			Interval: metav1.Duration{Duration: 15 * time.Second},
			Engine: RuleEngineConfig{
				Timeout:       metav1.Duration{Duration: 10 * time.Second},
				MaxSamples:    50000,
				LookbackDelta: metav1.Duration{Duration: 5 * time.Minute},
			},
		},
		Logging: LoggingConfig{
			// Defaults match the production deployment. Local debugging can
			// re-enable development and console output in the config file.
			Development:     false,
			Encoder:         "json",
			Level:           "info",
			StacktraceLevel: "error",
			TimeEncoding:    "rfc3339",
		},
	}
}

// LoadFile loads and validates a runtime configuration file.
func LoadFile(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read runtime config %q: %w", path, err)
	}
	configured, err := Parse(data)
	if err != nil {
		return nil, fmt.Errorf("load runtime config %q: %w", path, err)
	}
	return configured, nil
}

// Parse parses a strict YAML runtime configuration over the standalone
// defaults and validates the result.
func Parse(data []byte) (*Config, error) {
	configured := Default()
	if err := yaml.UnmarshalStrict(data, configured); err != nil {
		return nil, fmt.Errorf("parse YAML: %w", err)
	}
	if err := configured.Validate(); err != nil {
		return nil, err
	}
	return configured, nil
}

// Validate validates all user-controlled runtime configuration.
func (c *Config) Validate() error {
	if c.APIVersion != APIVersion {
		return fmt.Errorf("apiVersion must be %q", APIVersion)
	}
	if c.Kind != Kind {
		return fmt.Errorf("kind must be %q", Kind)
	}
	if c.Controller.SyncPeriod.Duration <= 0 {
		return fmt.Errorf("controller.syncPeriod must be greater than zero")
	}
	if strings.TrimSpace(c.Server.Health.BindAddress) == "" {
		return fmt.Errorf("server.health.bindAddress is required")
	}
	if strings.TrimSpace(c.Metrics.BindAddress) == "" {
		return fmt.Errorf("metrics.bindAddress is required")
	}
	if strings.TrimSpace(c.Storage.Path) == "" {
		return fmt.Errorf("storage.path is required")
	}
	if c.Storage.Retention.Duration <= 0 {
		return fmt.Errorf("storage.retention must be greater than zero")
	}
	if strings.TrimSpace(c.Query.BindAddress) == "" {
		return fmt.Errorf("query.bindAddress is required")
	}
	if c.Query.Timeout.Duration <= 0 {
		return fmt.Errorf("query.timeout must be greater than zero")
	}
	if c.Query.MaxSamples <= 0 {
		return fmt.Errorf("query.maxSamples must be greater than zero")
	}
	if c.Query.MaxPoints <= 0 {
		return fmt.Errorf("query.maxPoints must be greater than zero")
	}
	if c.Scrapers.Interval.Duration <= 0 {
		return fmt.Errorf("scrapers.interval must be greater than zero")
	}
	if c.Scrapers.MetricsServer.MaxAge.Duration <= 0 {
		return fmt.Errorf("scrapers.metricsServer.maxAge must be greater than zero")
	}
	if len(c.Scrapers.MetricsServer.Resources) == 0 {
		return fmt.Errorf("scrapers.metricsServer.resources are required")
	}
	for name, resource := range c.Scrapers.MetricsServer.Resources {
		if !commonmodel.LegacyValidation.IsValidMetricName(name) {
			return fmt.Errorf("scrapers.metricsServer.resources: invalid metric name %q", name)
		}
		if strings.TrimSpace(resource) == "" {
			return fmt.Errorf("scrapers.metricsServer.resources.%s is required", name)
		}
	}
	if err := validatePodWatcherMetrics(c.Scrapers.PodWatcher.Metrics); err != nil {
		return err
	}
	if c.Scrapers.PodMetrics.Timeout.Duration <= 0 {
		return fmt.Errorf("scrapers.podMetrics.timeout must be greater than zero")
	}
	if c.Scrapers.PodMetrics.MaxBodySize <= 0 || int64(c.Scrapers.PodMetrics.MaxBodySize) > 1024*1024*1024 {
		return fmt.Errorf("scrapers.podMetrics.maxBodySize must be between 1B and 1GiB")
	}
	if c.Scrapers.PodMetrics.Concurrency <= 0 {
		return fmt.Errorf("scrapers.podMetrics.concurrency must be greater than zero")
	}
	if err := validateMetricNames("scrapers.podMetrics.metrics", c.Scrapers.PodMetrics.Metrics); err != nil {
		return err
	}
	if c.Rules.Interval.Duration <= 0 {
		return fmt.Errorf("rules.interval must be greater than zero")
	}
	if c.Rules.Engine.Timeout.Duration <= 0 {
		return fmt.Errorf("rules.engine.timeout must be greater than zero")
	}
	if c.Rules.Engine.MaxSamples <= 0 {
		return fmt.Errorf("rules.engine.maxSamples must be greater than zero")
	}
	if c.Rules.Engine.LookbackDelta.Duration <= 0 {
		return fmt.Errorf("rules.engine.lookbackDelta must be greater than zero")
	}
	if err := validateRecordingRules(c.Rules.Records); err != nil {
		return err
	}
	if c.Logging.Encoder != "json" && c.Logging.Encoder != "console" {
		return fmt.Errorf("logging.encoder must be json or console")
	}
	if !validLogLevel(c.Logging.Level) {
		return fmt.Errorf("logging.level is invalid: %q", c.Logging.Level)
	}
	if !validLogLevel(c.Logging.StacktraceLevel) {
		return fmt.Errorf("logging.stacktraceLevel is invalid: %q", c.Logging.StacktraceLevel)
	}
	if !validTimeEncoding(c.Logging.TimeEncoding) {
		return fmt.Errorf("logging.timeEncoding is invalid: %q", c.Logging.TimeEncoding)
	}
	return nil
}

func validatePodWatcherMetrics(metrics []PodWatcherMetric) error {
	if len(metrics) == 0 {
		return fmt.Errorf("scrapers.podWatcher.metrics are required")
	}
	names := make(map[string]struct{}, len(metrics))
	for i := range metrics {
		configured := &metrics[i]
		if !commonmodel.LegacyValidation.IsValidMetricName(configured.Name) {
			return fmt.Errorf("scrapers.podWatcher.metrics[%d].name: invalid metric name %q", i, configured.Name)
		}
		if _, duplicate := names[configured.Name]; duplicate {
			return fmt.Errorf("scrapers.podWatcher.metrics: duplicate metric name %q", configured.Name)
		}
		names[configured.Name] = struct{}{}
		expression := strings.TrimSpace(configured.JSONPath)
		if expression == "" {
			return fmt.Errorf("scrapers.podWatcher.metrics[%d].jsonPath is required", i)
		}
		if strings.HasPrefix(expression, "{") || strings.HasSuffix(expression, "}") {
			return fmt.Errorf("scrapers.podWatcher.metrics[%d].jsonPath must omit outer braces", i)
		}
		if err := jsonpath.New(configured.Name).AllowMissingKeys(true).Parse("{" + expression + "}"); err != nil {
			return fmt.Errorf("scrapers.podWatcher.metrics[%d].jsonPath is invalid: %w", i, err)
		}
	}
	return nil
}

func validateMetricNames(path string, metrics []string) error {
	names := make(map[string]struct{}, len(metrics))
	for i, name := range metrics {
		if !commonmodel.LegacyValidation.IsValidMetricName(name) {
			return fmt.Errorf("%s[%d]: invalid metric name %q", path, i, name)
		}
		if _, duplicate := names[name]; duplicate {
			return fmt.Errorf("%s: duplicate metric name %q", path, name)
		}
		names[name] = struct{}{}
	}
	return nil
}

func validateRecordingRules(records []RecordingRule) error {
	promQLParser := parser.NewParser(parser.Options{})
	for i := range records {
		configured := &records[i]
		if !commonmodel.LegacyValidation.IsValidMetricName(configured.Record) {
			return fmt.Errorf("rules.records[%d].record: invalid metric name %q", i, configured.Record)
		}
		if strings.TrimSpace(configured.Expr) == "" {
			return fmt.Errorf("rules.records[%d].expr is required", i)
		}
		if _, err := promQLParser.ParseExpr(configured.Expr); err != nil {
			return fmt.Errorf("rules.records[%d].expr is invalid PromQL: %w", i, err)
		}
		for name := range configured.Labels {
			if !commonmodel.LegacyValidation.IsValidLabelName(name) {
				return fmt.Errorf("rules.records[%d].labels: invalid label name %q", i, name)
			}
			if name == string(commonmodel.MetricNameLabel) {
				return fmt.Errorf("rules.records[%d].labels: metric name cannot be overridden", i)
			}
		}
	}
	return nil
}

func validLogLevel(value string) bool {
	switch value {
	case "debug", "info", "warn", "error", "dpanic", "panic", "fatal":
		return true
	default:
		return false
	}
}

func validTimeEncoding(value string) bool {
	switch value {
	case "rfc3339nano", "rfc3339", "iso8601", "millis", "nanos", "epoch":
		return true
	default:
		return false
	}
}
