package config

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

	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/helm"
	"helm.sh/helm/v3/pkg/chart"
	"helm.sh/helm/v3/pkg/chart/loader"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	"sigs.k8s.io/yaml"
)

// CRDVersionError is returned when there is an error loading CRDs from a specific version.
type CRDVersionError struct {
	Version string
	Reason  string
}

func (e *CRDVersionError) Error() string {
	return fmt.Sprintf("CRD version error for version '%s': %s", e.Version, e.Reason)
}

type TemplatesConfig struct {
	Addons    []AddonTemplateConfig
	Infra     []InfraTemplateConfig
	Dedicated []DedicatedTemplateConfig
	DP        []DPTemplateConfig
}

func (c *TemplatesConfig) GetDefaultAddonTemplateId() string {
	for _, addon := range c.Addons {
		if addon.Default {
			return addon.TemplateId
		}
	}
	return ""
}

func (c *TemplatesConfig) GetAddonTemplate(templateId string) (*AddonTemplateConfig, bool) {
	for _, addon := range c.Addons {
		if addon.TemplateId == templateId {
			return &addon, true
		}
	}
	return nil, false
}

// GetDefaultInfraTemplateIdWithVersion return the first default template found with supported version.
func (c *TemplatesConfig) GetDefaultInfraTemplateIdWithVersion(version string) string {
	return getDefaultTemplateIdWithVersion(version, c.Infra)
}

func (c *TemplatesConfig) GetDefaultDedicatedTemplateIdWithVersion(version string) string {
	return getDefaultTemplateIdWithVersion(version, c.Dedicated)
}

func (c *TemplatesConfig) GetDefaultDPTemplateIdWithVersion(version string) string {
	return getDefaultTemplateIdWithVersion(version, c.DP)
}

func (c *TemplatesConfig) GetInfraTemplateWithVersion(templateId string, version string) (*InfraTemplateConfig, bool) {
	return getTemplateWithVersion(c.Infra, templateId, version)
}

func (c *TemplatesConfig) GetDedicatedTemplateWithVersion(templateId string, version string) (*DedicatedTemplateConfig, bool) {
	return getTemplateWithVersion(c.Dedicated, templateId, version)
}

func (c *TemplatesConfig) GetDPTemplateWithVersion(templateId string, version string) (*DPTemplateConfig, bool) {
	return getTemplateWithVersion(c.DP, templateId, version)
}

func LoadTemplatesConfig(addonPath, infraPath, dedicatedPath, dpPath string) (TemplatesConfig, error) {
	tmpl := TemplatesConfig{}

	addonData, err := os.ReadFile(addonPath)
	if err != nil {
		return tmpl, err
	}
	var addonConfig []AddonTemplateConfig
	if err = yaml.Unmarshal(addonData, &addonConfig); err != nil {
		return tmpl, err
	}

	infraData, err := os.ReadFile(infraPath)
	if err != nil {
		return tmpl, err
	}
	var infraConfig []InfraTemplateConfig
	if err = yaml.Unmarshal(infraData, &infraConfig); err != nil {
		return tmpl, err
	}

	dedicatedData, err := os.ReadFile(dedicatedPath)
	if err != nil {
		return tmpl, err
	}
	var dedicatedConfig []DedicatedTemplateConfig
	if err = yaml.Unmarshal(dedicatedData, &dedicatedConfig); err != nil {
		return tmpl, err
	}

	dpData, err := os.ReadFile(dpPath)
	if err != nil {
		return tmpl, err
	}
	var dpConfig []DPTemplateConfig
	if err = yaml.Unmarshal(dpData, &dpConfig); err != nil {
		return tmpl, err
	}

	tmpl.Addons = addonConfig
	tmpl.Infra = infraConfig
	tmpl.Dedicated = dedicatedConfig
	tmpl.DP = dpConfig

	return tmpl, nil
}

type AddonTemplateConfig struct {
	TemplateId string `json:"templateId"`

	Default bool `json:"default"`

	// EKS contains entries of k8sVersion -> addonConfigs
	EKS map[string][]EKSAddonConfig `json:"eks"`
}

type EKSAddonConfig struct {
	AddonVersion string `json:"addonVersion"`

	AddonName string `json:"addonName"`
	// The set of configuration values for the add-on that's created. The values
	// that you provide should follow the schema in DescribeAddonConfiguration
	// (https://docs.aws.amazon.com/eks/latest/APIReference/API_DescribeAddonConfiguration.html)
	// or via 'aws eks describe-addon-configuration --addon-name=<> --addon-version=<>'.
	Values *string `json:"values,omitempty"`
}

type ComponentTemplateConfig interface {
	GetSupportedVersions() []string
	GetTemplateId() string
	IsDefault() bool
}

// getDefaultTemplateIdWithVersion return the first default template found with supported version.
func getDefaultTemplateIdWithVersion[T ComponentTemplateConfig](version string, templates []T) string {
	for _, t := range templates {
		if t.IsDefault() && slices.Contains(t.GetSupportedVersions(), version) {
			return t.GetTemplateId()
		}
	}
	return ""
}

func getTemplateWithVersion[T ComponentTemplateConfig](templates []T, templateId string, version string) (*T, bool) {
	for _, t := range templates {
		if t.GetTemplateId() == templateId && slices.Contains(t.GetSupportedVersions(), version) {
			return &t, true
		}
	}
	return nil, false
}

type TemplateConfigMeta struct {
	TemplateId        string   `json:"templateId"`
	Default           bool     `json:"default"`
	SupportedVersions []string `json:"supportedVersions"`
}

func (c TemplateConfigMeta) GetTemplateId() string {
	return c.TemplateId
}

func (c TemplateConfigMeta) IsDefault() bool {
	return c.Default
}

func (c TemplateConfigMeta) GetSupportedVersions() []string {
	return c.SupportedVersions
}

type InfraTemplateConfig struct {
	TemplateConfigMeta        `json:",inline"`
	KubeStateMetrics          InfraComponentConfig `json:"kubeStateMetrics"`
	NodeExporter              InfraComponentConfig `json:"nodeExporter"`
	EventExporter             InfraComponentConfig `json:"eventExporter"`
	ClusterAutoscaler         InfraComponentConfig `json:"clusterAutoscaler"`
	CertManager               InfraComponentConfig `json:"certManager"`
	PCAIssuer                 InfraComponentConfig `json:"pcaIssuer"`
	AWSLoadBalancerController InfraComponentConfig `json:"awsLoadBalancerController"`
	NodeLocalDNS              InfraComponentConfig `json:"nodeLocalDNS"`
}

type DedicatedTemplateConfig struct {
	TemplateConfigMeta `json:",inline"`
	TiDBOperator       InfraComponentConfig `json:"tidbOperator"`
	TiDBOperatorV2     InfraComponentConfig `json:"tidbOperatorV2"`

	ClusterProxy   InfraComponentConfig `json:"clusterProxy"`
	ClusterGateway InfraComponentConfig `json:"clusterGateway"`
	SLIReporter    InfraComponentConfig `json:"sliReporter"`
}

type DPTemplateConfig struct {
	TemplateConfigMeta `json:",inline"`
	TiFlowOperator     InfraComponentConfig `json:"tiflowOperator"`
	DataflowAgent      InfraComponentConfig `json:"dataflowAgent"`
}

// InfraComponentConfig is a configuration for a helm chart.
// It implements the helm.Package interface.
type InfraComponentConfig struct {
	config.HelmConfig `json:",inline"`
	ReleaseName       string `json:"releaseName"`
	Namespace         string `json:"namespace"`
	// HelmValues contains the configuration overlay values of the helm chart.
	// access it via ToHelmValues() or
	// DeepCopy() first if you need to modify the values.
	HelmValues map[string]interface{} `json:"helmValues"`
	// CRDVersion specifies the version of CRDs to load for this component.
	// If specified, the CRDs will be loaded from the `ChartPath/CRDVersion/`
	// This field is optional and when omitted, the system will use the default
	// CRD loading behavior for backward compatibility.
	CRDVersion string `json:"crdVersion,omitempty"`
}

func (c InfraComponentConfig) GetNamespace() string {
	return c.Namespace
}

// GetName returns the releaseName of the helm release.
// Note: the releaseName may be different from the chart name and the underlying resource Name.
func (c InfraComponentConfig) GetName() string {
	return c.ReleaseName
}

func (c InfraComponentConfig) GetCRDs() ([]*apiextensionsv1.CustomResourceDefinition, error) {
	var result []*apiextensionsv1.CustomResourceDefinition

	// If CRDVersion is specified, load CRDs from the versioned directory.
	if c.CRDVersion != "" {
		versionedCRDDir := filepath.Join(c.ChartPath, c.CRDVersion)
		files, err := os.ReadDir(versionedCRDDir)
		if err != nil {
			if os.IsNotExist(err) {
				return nil, &CRDVersionError{Version: c.CRDVersion, Reason: fmt.Sprintf("directory '%s' not found", versionedCRDDir)}
			}
			return nil, fmt.Errorf("failed to read versioned CRD directory '%s': %w", versionedCRDDir, err)
		}

		var foundYAML bool
		for _, file := range files {
			if !file.IsDir() && (strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml")) {
				foundYAML = true
				crdPath := filepath.Join(versionedCRDDir, file.Name())
				crd, err := helm.LoadCRD(crdPath)
				if err != nil {
					return nil, fmt.Errorf("failed to load CRDs from versioned file '%s': %w", crdPath, err)
				}
				result = append(result, crd...)
			}
		}
		if !foundYAML {
			return nil, &CRDVersionError{Version: c.CRDVersion, Reason: fmt.Sprintf("no YAML file found in directory '%s'", versionedCRDDir)}
		}
		return result, nil
	}

	// Fallback to the original behavior for backward compatibility.
	for _, path := range c.CRDPaths {
		crd, err := helm.LoadCRD(path)
		if err != nil {
			return nil, err
		}
		result = append(result, crd...)
	}
	return result, nil
}

func (c InfraComponentConfig) ToHelmChart() (result *chart.Chart, err error) {
	result, err = loader.Load(c.ChartPath)
	if err != nil {
		return nil, fmt.Errorf("failed to load helm chart %q: %w", c.ChartPath, err)
	}
	return
}

func (c InfraComponentConfig) ToHelmValues() map[string]interface{} {
	return deepCopyValues(c.HelmValues)
}

// deepCopyMap performs a deep copy of a map[string]interface{}.
func deepCopyValues(m map[string]interface{}) map[string]interface{} {
	valsCopy := make(map[string]interface{})
	for k, v := range m {
		// Check if the value is a map[string]interface{} and recursively deep copy it.
		if innerMap, ok := v.(map[string]interface{}); ok {
			valsCopy[k] = deepCopyValues(innerMap)
		} else {
			// If the value is not a map, copy it directly.
			valsCopy[k] = v
		}
	}
	return valsCopy
}

func (c InfraComponentConfig) SetHelmValues(values []byte) error {
	return yaml.Unmarshal(values, &c.HelmValues)
}

// DeepCopy copies the helm values of the InfraComponentConfig.
func (c InfraComponentConfig) DeepCopy() *InfraComponentConfig {
	out := c
	paths := make([]string, len(c.CRDPaths))
	copy(paths, c.CRDPaths)
	out.CRDPaths = paths
	valsCopy := deepCopyValues(c.HelmValues)
	out.HelmValues = valsCopy
	// CRDVersion is a string, so it's copied by value automatically
	out.CRDVersion = c.CRDVersion
	return &out
}

var _ helm.Package = InfraComponentConfig{}
