package config

import (
	"os"
	"sort"

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/yaml"
)

type RootConfig struct {
	O11yConfig ObservabilityInfra `json:"o11yConfig"`
	AutoScaler AutoScaler         `json:"autoscaler"`

	// DataplaneAssumeRoleName is the role name that the controller will assume to query dataplane resources.
	// Used in AliCloud.
	DataplaneAssumeRoleName string `json:"dataplaneAssumeRoleName"`
	Mode                    string `json:"mode"`
}

const (
	ModeServerless = "serverless"
)

func (r *RootConfig) IsServerlessMode() bool {
	return r.Mode == ModeServerless
}

func (r *RootConfig) IsServerlessSharedPoolVector() bool {
	vectorCfg := r.O11yConfig.CustomerConfigsV2.NextGen.SharedPool.BuildConfig().Vector
	return r.IsServerlessMode() && vectorCfg.EnableServerless
}

// AutoScaler defines the scaling rule for the pod autoscaler
type AutoScaler struct {
	// Enabled indicates whether the autoscaling is enabled or not
	Enabled bool `json:"enabled"`
	// TargetUtilization is the target utilization percentage for the resource
	TargetUtilization int32 `json:"targetUtilization"`
	// FrozenSeconds is the duration in seconds to wait after a scaling action before considering further scaling actions
	FrozenSeconds int64 `json:"frozenSeconds"`
	// Hits is the number of samples that must meet the scaling criteria to trigger a scaling action
	Hits int32 `json:"hits"`
	// Samples is the number of samples to consider for scaling decisions
	Samples int `json:"samples"`
}

type ObservabilityInfra struct {
	Env            string `json:"env"`
	Vendor         string `json:"vendor"`
	Region         string `json:"region"`
	LogicRegion    string `json:"logicRegion"`
	IsByoc         bool   `json:"isByoc"`
	OpenAPIAddress string `json:"openAPIAddress"`
	// PeeringNLB is the NLB address used when the o11y VPC and dataplane VPC are
	// connected via VPC peering. When set, the controller uses this address as the
	// o11y backend endpoint and skips creating a PrivateLink interface endpoint.
	PeeringNLB          string `json:"peeringNLB,omitempty"`
	CreateO11yInfraPath string `json:"createO11yInfraPath"`
	DeleteO11yInfraPath string `json:"deleteO11yInfraPath"`
	GetO11yInfraPath    string `json:"getO11yInfraPath"`
	PauseO11yInfraPath  string `json:"pauseO11yInfraPath"`
	ResumeO11yInfraPath string `json:"resumeO11yInfraPath"`
	Auth0GetTokenURL    string `json:"auth0GetTokenURL"`
	Auth0ClientID       string `json:"auth0ClientID"`
	Auth0ClientSecret   string `json:"auth0ClientSecret"`
	Auth0Audience       string `json:"auth0Audience"`
	Version             int    `json:"version"`

	// o11y customer agent topology config
	CustomerConfigsV2 CustomerConfigsV2 `json:"customerConfigsV2"`
}

type CustomerConfigsV2 struct {
	Dedicated CustomerDedicated `json:"dedicated"`
	NextGen   CustomerNextGen   `json:"nextgen"`
}

type CustomerDedicated struct {
	Repos       map[string]string            `json:"repos"`
	Charts      map[string]CustomerHelmChart `json:"charts"`
	K8s         CustomerDedicatedK8s         `json:"k8s"`
	TiDBCluster CustomerDedicatedTiDBCluster `json:"tidbcluster"`
	Specs       CustomerDedicatedSpecs       `json:"specs"`
}

type CustomerDedicatedK8s struct {
	Default  CustomerAgents            `json:"default"`
	Specs    CustomerDedicatedSpecs    `json:"specs"`
	Tenants  map[string]CustomerAgents `json:"tenants,omitempty"`
	Projects map[string]CustomerAgents `json:"projects,omitempty"`
}

func (c *CustomerDedicatedK8s) BuildConfig() CustomerAgents {
	return c.Default
}

type CustomerHelmChart struct {
	Name    string         `json:"name"`
	Version string         `json:"version"`
	Values  map[string]any `json:"values"`
}

type CustomerDedicatedTiDBCluster struct {
	Default  CustomerAgents            `json:"default"`
	Tenants  map[string]CustomerAgents `json:"tenants,omitempty"`
	Projects map[string]CustomerAgents `json:"projects,omitempty"`
	Clusters map[string]CustomerAgents `json:"clusters,omitempty"`
}

func (c *CustomerDedicatedTiDBCluster) BuildConfig() CustomerAgents {
	return c.Default
}

type CustomerDedicatedSpecs struct {
	K8s         CustomerAgentSpecs `json:"k8s"`
	TiDBCluster CustomerAgentSpecs `json:"tidbcluster"`
}

type CustomerAgentSpec struct {
	Name         string                       `json:"name"`
	MaxNodes     uint32                       `json:"maxNodes"`
	InstanceType string                       `json:"instanceType"`
	Vector       *corev1.ResourceRequirements `json:"vector,omitempty"`
	VMAgent      *corev1.ResourceRequirements `json:"vmagent,omitempty"`
}

type CustomerNextGen struct {
	Repos      map[string]string            `json:"repos"`
	Charts     map[string]CustomerHelmChart `json:"charts"`
	K8s        CustomerNextGenK8s           `json:"k8s"`
	SharedPool CustomerNextGenSharedPool    `json:"sharedpool"`
	TiDBGroup  CustomerNextGenTiDBGroup     `json:"tidbgroup"`
}

type CustomerNextGenK8s struct {
	Default    CustomerAgents            `json:"default"`
	Specs      CustomerAgentSpecs        `json:"specs"`
	Kubernetes map[string]CustomerAgents `json:"kubernetes,omitempty"`
}

func (c *CustomerNextGenK8s) BuildConfig() CustomerAgents {
	return c.Default
}

type CustomerNextGenSharedPool struct {
	Default     CustomerAgents            `json:"default"`
	Specs       CustomerAgentSpecs        `json:"specs"`
	SharedPools map[string]CustomerAgents `json:"sharedpools,omitempty"`
}

func (c *CustomerNextGenSharedPool) BuildConfig() CustomerAgents {
	return c.Default
}

type CustomerNextGenTiDBGroup struct {
	Default    CustomerAgents            `json:"default"`
	Specs      CustomerAgentSpecs        `json:"specs"`
	TiDBGroups map[string]CustomerAgents `json:"tidbgroups,omitempty"`
}

func (c *CustomerNextGenTiDBGroup) BuildConfig() CustomerAgents {
	return c.Default
}

type CustomerAgents struct {
	Vector  *VectorConfig  `json:"vector,omitempty"`
	VMAgent *VMAgentConfig `json:"vmagent,omitempty"`
}

type VMAgentConfig struct {
	Image           string                         `json:"image,omitempty"`
	Args            map[string]string              `json:"args,omitempty"`
	Resources       *corev1.ResourceRequirements   `json:"resources,omitempty"`
	StatefulStorage *vmoperatorv1beta1.StorageSpec `json:"statefulStorage,omitempty"`
	Affinity        *corev1.Affinity               `json:"affinity,omitempty"`
	Tolerations     []corev1.Toleration            `json:"tolerations,omitempty"`
}

type VectorConfig struct {
	Chart              *CustomerHelmChart           `json:"chart,omitempty"`
	Image              string                       `json:"image,omitempty"`
	InitContainerImage string                       `json:"initContainerImage,omitempty"`
	PodHostNetwork     *bool                        `json:"podHostNetwork,omitempty"`
	DNSPolicy          string                       `json:"dnsPolicy,omitempty"`
	Resources          *corev1.ResourceRequirements `json:"resources,omitempty"`
	Replicas           uint32                       `json:"replicas,omitempty"`
	EnableServerless   bool                         `json:"enableServerless,omitempty"`
	EnableTiKVTopSQL   bool                         `json:"enableTiKVTopSQL,omitempty"`
	PDAddress          string                       `json:"pdAddress,omitempty"`
	ClusterSecretName  string                       `json:"clusterSecretName,omitempty"`
	ManagerServerURL   string                       `json:"managerServerURL,omitempty"`
	TiDBNamespace      string                       `json:"tidbNamespace,omitempty"`
}

type CustomerAgentSpecs []CustomerAgentSpec

func (specs CustomerAgentSpecs) Len() int {
	return len(specs)
}

func (specs CustomerAgentSpecs) Less(i, j int) bool {
	return specs[i].MaxNodes < specs[j].MaxNodes
}

func (specs CustomerAgentSpecs) Swap(i, j int) {
	specs[i], specs[j] = specs[j], specs[i]
}

// Index returns the index of the spec with the given name, or -1 if not found.
func (specs CustomerAgentSpecs) Index(name string) int {
	for i := 0; i < len(specs); i++ {
		if specs[i].Name == name {
			return i
		}
	}
	return -1
}

// Select returns the spec with the given name, or nil if not found.
func (specs CustomerAgentSpecs) Select(name string) *CustomerAgentSpec {
	if idx := specs.Index(name); idx >= 0 {
		return &specs[idx]
	}
	return nil
}

// SelectUp returns the next higher spec than currentSpec, but not exceeding maxSpec.
// If currentSpec is already at or above maxSpec, or if either spec is not found, returns nil.
func (specs CustomerAgentSpecs) SelectUp(currentSpec, maxSpec string) *CustomerAgentSpec {
	currIndex := specs.Index(currentSpec)
	if currIndex < 0 {
		return nil
	}
	maxIndex := specs.Index(maxSpec)
	if maxIndex < 0 {
		return nil
	}

	// If already at max spec, return current spec
	if currIndex == maxIndex {
		return &specs[currIndex]
	}

	if currIndex+1 > maxIndex {
		return nil
	}
	return &specs[currIndex+1]
}

// SelectDown returns the next lower spec than currentSpec, but not below minSpec.
// If currentSpec is already at or below minSpec, or if either spec is not found, returns nil.
func (specs CustomerAgentSpecs) SelectDown(currentSpec, minSpec string) *CustomerAgentSpec {
	currIndex := specs.Index(currentSpec)
	if currIndex < 0 {
		return nil
	}
	minIndex := specs.Index(minSpec)
	if minIndex < 0 {
		return nil
	}

	// If already at min spec, return current spec
	if currIndex == minIndex {
		return &specs[currIndex]
	}
	// If going down would go below min spec, return min spec
	if currIndex < minIndex {
		return &specs[minIndex]
	}

	if currIndex-1 < minIndex {
		return nil
	}
	return &specs[currIndex-1]
}

// LoadRootConfig loads the RootConfig from the given file path.
// It also sorts the specs in ascending order by MaxNodes.
func LoadRootConfig(path string) (RootConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return RootConfig{}, err
	}

	var config RootConfig
	err = yaml.Unmarshal(data, &config)
	if err != nil {
		return RootConfig{}, err
	}

	sort.Sort(config.O11yConfig.CustomerConfigsV2.Dedicated.Specs.K8s)
	sort.Sort(config.O11yConfig.CustomerConfigsV2.Dedicated.Specs.TiDBCluster)
	sort.Sort(config.O11yConfig.CustomerConfigsV2.NextGen.K8s.Specs)
	sort.Sort(config.O11yConfig.CustomerConfigsV2.NextGen.SharedPool.Specs)
	sort.Sort(config.O11yConfig.CustomerConfigsV2.NextGen.TiDBGroup.Specs)

	if config.AutoScaler.TargetUtilization <= 0 {
		config.AutoScaler.TargetUtilization = 80
	}
	if config.AutoScaler.FrozenSeconds <= 0 {
		config.AutoScaler.FrozenSeconds = 600 // default 10 minutes
	}
	if config.AutoScaler.Samples <= 0 {
		config.AutoScaler.Samples = 10
	}
	if config.AutoScaler.Hits <= 0 {
		config.AutoScaler.Hits = 8
	}

	return config, nil
}
