package linkerd2

import (
	"errors"
	"fmt"

	"github.com/imdario/mergo"
	"github.com/linkerd/linkerd2/charts"
	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
	"github.com/linkerd/linkerd2/pkg/k8s"
	"github.com/linkerd/linkerd2/pkg/version"
	"helm.sh/helm/v3/pkg/chart/loader"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/yaml"
)

var (
	// HelmChartDirCrds is the directory name for the linkerd-crds chart
	HelmChartDirCrds = "linkerd-crds"

	// HelmChartDirCP is the directory name for the linkerd-control-plane chart
	HelmChartDirCP = "linkerd-control-plane"
)

type (
	// Values contains the top-level elements in the Helm charts
	Values struct {
		ControllerImage              string                 `json:"controllerImage"`
		ControllerReplicas           uint                   `json:"controllerReplicas"`
		ControllerUID                int64                  `json:"controllerUID"`
		ControllerGID                int64                  `json:"controllerGID"`
		EnableH2Upgrade              bool                   `json:"enableH2Upgrade"`
		EnablePodAntiAffinity        bool                   `json:"enablePodAntiAffinity"`
		NodeAffinity                 map[string]interface{} `json:"nodeAffinity"`
		EnablePodDisruptionBudget    bool                   `json:"enablePodDisruptionBudget"`
		Controller                   map[string]interface{} `json:"controller"`
		WebhookFailurePolicy         string                 `json:"webhookFailurePolicy"`
		DeploymentStrategy           map[string]interface{} `json:"deploymentStrategy,omitempty"`
		DisableHeartBeat             bool                   `json:"disableHeartBeat"`
		HeartbeatSchedule            string                 `json:"heartbeatSchedule"`
		Configs                      ConfigJSONs            `json:"configs"`
		ClusterDomain                string                 `json:"clusterDomain"`
		ClusterNetworks              string                 `json:"clusterNetworks"`
		ImagePullPolicy              string                 `json:"imagePullPolicy"`
		CliVersion                   string                 `json:"cliVersion"`
		ControllerLogLevel           string                 `json:"controllerLogLevel"`
		ControllerLogFormat          string                 `json:"controllerLogFormat"`
		ProxyContainerName           string                 `json:"proxyContainerName"`
		HighAvailability             bool                   `json:"highAvailability"`
		CNIEnabled                   bool                   `json:"cniEnabled"`
		EnableEndpointSlices         bool                   `json:"enableEndpointSlices"`
		DisableIPv6                  bool                   `json:"disableIPv6"`
		ControlPlaneTracing          bool                   `json:"controlPlaneTracing"`
		ControlPlaneTracingNamespace string                 `json:"controlPlaneTracingNamespace"`
		IdentityTrustAnchorsPEM      string                 `json:"identityTrustAnchorsPEM"`
		IdentityTrustDomain          string                 `json:"identityTrustDomain"`
		PrometheusURL                string                 `json:"prometheusUrl"`
		ImagePullSecrets             []map[string]string    `json:"imagePullSecrets"`
		LinkerdVersion               string                 `json:"linkerdVersion"`
		RevisionHistoryLimit         uint                   `json:"revisionHistoryLimit"`

		DestinationController *DestinationController `json:"destinationController"`
		Heartbeat             map[string]interface{} `json:"heartbeat"`
		SPValidator           map[string]interface{} `json:"spValidator"`

		PodAnnotations    map[string]string `json:"podAnnotations"`
		PodLabels         map[string]string `json:"podLabels"`
		PriorityClassName string            `json:"priorityClassName"`

		PodMonitor       *PodMonitor       `json:"podMonitor"`
		PolicyController *PolicyController `json:"policyController"`
		Proxy            *Proxy            `json:"proxy"`
		ProxyInit        *ProxyInit        `json:"proxyInit"`
		NetworkValidator *NetworkValidator `json:"networkValidator"`
		Identity         *Identity         `json:"identity"`
		DebugContainer   *DebugContainer   `json:"debugContainer"`
		ProxyInjector    *ProxyInjector    `json:"proxyInjector"`
		ProfileValidator *Webhook          `json:"profileValidator"`
		PolicyValidator  *Webhook          `json:"policyValidator"`
		NodeSelector     map[string]string `json:"nodeSelector"`
		Tolerations      []interface{}     `json:"tolerations"`

		DestinationResources   *Resources `json:"destinationResources"`
		HeartbeatResources     *Resources `json:"heartbeatResources"`
		IdentityResources      *Resources `json:"identityResources"`
		ProxyInjectorResources *Resources `json:"proxyInjectorResources"`

		DestinationProxyResources   *Resources `json:"destinationProxyResources"`
		IdentityProxyResources      *Resources `json:"identityProxyResources"`
		ProxyInjectorProxyResources *Resources `json:"proxyInjectorProxyResources"`
		Egress                      *Egress    `json:"egress"`
	}

	// Resources represents the computational resources setup for a given container
	Egress struct {
		GlobalEgressNetworkNamespace string `json:"globalEgressNetworkNamespace"`
	}

	DestinationController struct {
		MeshedHttp2ClientProtobuf map[string]interface{} `json:"meshedHttp2ClientProtobuf"`
		PodAnnotations            map[string]string      `json:"podAnnotations"`
	}

	// PodDisruptionBudget contains the fields to set the PDB
	PodDisruptionBudget struct {
		MaxUnavailable int `json:"maxUnavailable"`
	}

	// ConfigJSONs is the JSON encoding of the Linkerd configuration
	ConfigJSONs struct {
		Global  string `json:"global"`
		Proxy   string `json:"proxy"`
		Install string `json:"install"`
	}

	// Proxy contains the fields to set the proxy sidecar container
	Proxy struct {
		Capabilities                         *Capabilities    `json:"capabilities"`
		EnableExternalProfiles               bool             `json:"enableExternalProfiles"`
		Image                                *Image           `json:"image"`
		EnableShutdownEndpoint               bool             `json:"enableShutdownEndpoint"`
		LogLevel                             string           `json:"logLevel"`
		LogFormat                            string           `json:"logFormat"`
		LogHTTPHeaders                       string           `json:"logHTTPHeaders"`
		SAMountPath                          *VolumeMountPath `json:"saMountPath"`
		Ports                                *Ports           `json:"ports"`
		Resources                            *Resources       `json:"resources"`
		UID                                  int64            `json:"uid"`
		GID                                  int64            `json:"gid"`
		WaitBeforeExitSeconds                uint64           `json:"waitBeforeExitSeconds"`
		IsGateway                            bool             `json:"isGateway"`
		IsIngress                            bool             `json:"isIngress"`
		RequireIdentityOnInboundPorts        string           `json:"requireIdentityOnInboundPorts"`
		OutboundConnectTimeout               string           `json:"outboundConnectTimeout"`
		InboundConnectTimeout                string           `json:"inboundConnectTimeout"`
		OutboundDiscoveryCacheUnusedTimeout  string           `json:"outboundDiscoveryCacheUnusedTimeout"`
		InboundDiscoveryCacheUnusedTimeout   string           `json:"inboundDiscoveryCacheUnusedTimeout"`
		DisableOutboundProtocolDetectTimeout bool             `json:"disableOutboundProtocolDetectTimeout"`
		DisableInboundProtocolDetectTimeout  bool             `json:"disableInboundProtocolDetectTimeout"`
		PodInboundPorts                      string           `json:"podInboundPorts"`
		OpaquePorts                          string           `json:"opaquePorts"`
		Await                                bool             `json:"await"`
		DefaultInboundPolicy                 string           `json:"defaultInboundPolicy"`
		OutboundTransportMode                string           `json:"outboundTransportMode"`
		AccessLog                            string           `json:"accessLog"`
		ShutdownGracePeriod                  string           `json:"shutdownGracePeriod"`
		NativeSidecar                        bool             `json:"nativeSidecar"`
		StartupProbe                         *StartupProbe    `json:"startupProbe"`
		ReadinessProbe                       *Probe           `json:"readinessProbe"`
		LivenessProbe                        *Probe           `json:"livenessProbe"`
		Control                              *ProxyControl    `json:"control"`
		Metrics                              *ProxyMetrics    `json:"metrics"`
		Tracing                              *ProxyTracing    `json:"tracing"`

		AdditionalEnv   []corev1.EnvVar `json:"additionalEnv"`
		ExperimentalEnv []corev1.EnvVar `json:"experimentalEnv"`

		Inbound  ProxyParams `json:"inbound,omitempty"`
		Outbound ProxyParams `json:"outbound,omitempty"`

		// Deprecated: Use Runtime.Workers.Minimum.
		Cores int64 `json:"cores,omitempty"`

		Runtime ProxyRuntime `json:"runtime,omitempty"`
	}

	ProxyParams      = map[string]ProxyScopeParams
	ProxyScopeParams = map[string]ProxyProtoParams
	ProxyProtoParams = map[string]interface{}

	ProxyControl struct {
		Streams *ProxyControlStreams `json:"streams"`
	}

	ProxyControlStreams struct {
		InitialTimeout string `json:"initialTimeout"`
		IdleTimeout    string `json:"idleTimeout"`
		Lifetime       string `json:"lifetime"`
	}

	ProxyMetrics struct {
		HostnameLabels bool `json:"hostnameLabels"`
	}

	ProxyTracing struct {
		Enable           bool                   `json:"enable"`
		Protocol         string                 `json:"protocol"`
		TraceServiceName string                 `json:"traceServiceName"`
		Collector        *ProxyTracingCollector `json:"collector"`
	}

	ProxyTracingCollector struct {
		Endpoint     string                         `json:"endpoint"`
		MeshIdentity *ProxyTracingCollectorIdentity `json:"meshIdentity"`
	}

	ProxyTracingCollectorIdentity struct {
		ServiceAccountName string `json:"serviceAccountName"`
	}

	ProxyRuntime struct {
		Workers ProxyRuntimeWorkers `json:"workers,omitempty"`
	}

	ProxyRuntimeWorkers struct {
		Maximum int64 `json:"maximum,omitempty"`
		Minimum int64 `json:"minimum,omitempty"`

		MaximumCPURatio float64 `json:"maximumCPURatio,omitempty"`
	}

	// ProxyInit contains the fields to set the proxy-init container
	ProxyInit struct {
		Capabilities        *Capabilities    `json:"capabilities"`
		IgnoreInboundPorts  string           `json:"ignoreInboundPorts"`
		IgnoreOutboundPorts string           `json:"ignoreOutboundPorts"`
		KubeAPIServerPorts  string           `json:"kubeAPIServerPorts"`
		SkipSubnets         string           `json:"skipSubnets"`
		LogLevel            string           `json:"logLevel"`
		LogFormat           string           `json:"logFormat"`
		Image               *Image           `json:"image"`
		SAMountPath         *VolumeMountPath `json:"saMountPath"`
		XTMountPath         *VolumeMountPath `json:"xtMountPath"`
		/* DEPRECATED: should be removed after stable-2.16.0, left in for bc */
		Resources            *Resources `json:"resources"`
		CloseWaitTimeoutSecs int64      `json:"closeWaitTimeoutSecs"`
		Privileged           bool       `json:"privileged"`
		RunAsRoot            bool       `json:"runAsRoot"`
		RunAsUser            int64      `json:"runAsUser"`
		RunAsGroup           int64      `json:"runAsGroup"`
		IptablesMode         string     `json:"iptablesMode"`
	}

	NetworkValidator struct {
		LogLevel              string `json:"logLevel"`
		LogFormat             string `json:"logFormat"`
		ConnectAddr           string `json:"connectAddr"`
		ListenAddr            string `json:"listenAddr"`
		Timeout               string `json:"timeout"`
		EnableSecurityContext bool   `json:"enableSecurityContext"`
	}

	// DebugContainer contains the fields to set the debugging sidecar
	DebugContainer struct {
		Image *Image `json:"image"`
	}

	// PodMonitor contains the fields to configure the Prometheus Operator `PodMonitor`
	PodMonitor struct {
		Enabled        bool                  `json:"enabled"`
		ScrapeInterval string                `json:"scrapeInterval"`
		ScrapeTimeout  string                `json:"scrapeTimeout"`
		Controller     *PodMonitorController `json:"controller"`
		ServiceMirror  *PodMonitorComponent  `json:"serviceMirror"`
		Proxy          *PodMonitorComponent  `json:"proxy"`
	}

	// PodMonitorController contains the fields to configure the Prometheus Operator `PodMonitor` for the control-plane
	PodMonitorController struct {
		Enabled           bool   `json:"enabled"`
		NamespaceSelector string `json:"namespaceSelector"`
	}

	// PodMonitorComponent contains the fields to configure the Prometheus Operator `PodMonitor` for other components
	PodMonitorComponent struct {
		Enabled bool `json:"enabled"`
	}

	// PolicyController contains the fields to configure the policy controller container
	PolicyController struct {
		Image         *Image     `json:"image"`
		Resources     *Resources `json:"resources"`
		LogLevel      string     `json:"logLevel"`
		ProbeNetworks []string   `json:"probeNetworks"`
	}

	// Image contains the details to define a container image
	Image struct {
		Name       string `json:"name"`
		PullPolicy string `json:"pullPolicy"`
		Version    string `json:"version"`
	}

	// Ports contains all the port-related setups
	Ports struct {
		Admin    int32 `json:"admin"`
		Control  int32 `json:"control"`
		Inbound  int32 `json:"inbound"`
		Outbound int32 `json:"outbound"`
	}

	Probe struct {
		InitialDelaySeconds uint `json:"initialDelaySeconds"`
		TimeoutSeconds      uint `json:"timeoutSeconds"`
	}

	// Constraints wraps the Limit and Request settings for computational resources
	Constraints struct {
		Limit   string `json:"limit"`
		Request string `json:"request"`
	}

	// Capabilities contains the SecurityContext capabilities to add/drop into the injected
	// containers
	Capabilities struct {
		Add  []string `json:"add"`
		Drop []string `json:"drop"`
	}

	// VolumeMountPath contains the details for volume mounts
	VolumeMountPath struct {
		Name      string `json:"name"`
		MountPath string `json:"mountPath"`
		ReadOnly  bool   `json:"readOnly"`
	}

	// Resources represents the computational resources setup for a given container
	Resources struct {
		CPU              Constraints `json:"cpu"`
		Memory           Constraints `json:"memory"`
		EphemeralStorage Constraints `json:"ephemeral-storage"`
	}

	// StartupProbe represents the initContainer startup probe parameters for the proxy
	StartupProbe struct {
		InitialDelaySeconds uint `json:"initialDelaySeconds"`
		PeriodSeconds       uint `json:"periodSeconds"`
		FailureThreshold    uint `json:"failureThreshold"`
	}

	// Identity contains the fields to set the identity variables in the proxy
	// sidecar container
	Identity struct {
		ExternalCA                    bool              `json:"externalCA"`
		ServiceAccountTokenProjection bool              `json:"serviceAccountTokenProjection"`
		Issuer                        *Issuer           `json:"issuer"`
		KubeAPI                       *KubeAPI          `json:"kubeAPI"`
		PodAnnotations                map[string]string `json:"podAnnotations"`

		AdditionalEnv   []corev1.EnvVar `json:"additionalEnv"`
		ExperimentalEnv []corev1.EnvVar `json:"experimentalEnv"`
	}

	// Issuer has the Helm variables of the identity issuer
	Issuer struct {
		Scheme             string     `json:"scheme"`
		ClockSkewAllowance string     `json:"clockSkewAllowance"`
		IssuanceLifetime   string     `json:"issuanceLifetime"`
		TLS                *IssuerTLS `json:"tls"`
	}

	// KubeAPI contains the kube-apiserver client config
	KubeAPI struct {
		ClientQPS   float32 `json:"clientQPS"`
		ClientBurst int     `json:"clientBurst"`
	}

	// ProxyInjector configures the proxy-injector webhook
	ProxyInjector struct {
		Webhook
		PodAnnotations  map[string]string `json:"podAnnotations"`
		AdditionalEnv   []corev1.EnvVar   `json:"additionalEnv"`
		ExperimentalEnv []corev1.EnvVar   `json:"experimentalEnv"`
	}

	// Webhook Helm variables for a webhook
	Webhook struct {
		*TLS
		NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector"`
	}

	// TLS has a pair of PEM-encoded key and certificate variables used in the
	// Helm templates
	TLS struct {
		ExternalSecret     bool   `json:"externalSecret"`
		KeyPEM             string `json:"keyPEM"`
		CrtPEM             string `json:"crtPEM"`
		CaBundle           string `json:"caBundle"`
		InjectCaFrom       string `json:"injectCaFrom"`
		InjectCaFromSecret string `json:"injectCaFromSecret"`
	}

	// IssuerTLS is a stripped down version of TLS that lacks the integral caBundle.
	// It is tracked separately in the field 'IdentityTrustAnchorsPEM'
	IssuerTLS struct {
		KeyPEM string `json:"keyPEM"`
		CrtPEM string `json:"crtPEM"`
	}
)

// NewValues returns a new instance of the Values type.
func NewValues() (*Values, error) {
	v, err := readDefaults(HelmChartDirCrds + "/values.yaml")
	if err != nil {
		return nil, err
	}
	vCP, err := readDefaults(HelmChartDirCP + "/values.yaml")
	if err != nil {
		return nil, err
	}
	*v, err = v.Merge(*vCP)
	if err != nil {
		return nil, err
	}

	v.DebugContainer.Image.Version = version.Version
	v.CliVersion = k8s.CreatedByAnnotationValue()
	v.ProfileValidator.TLS = &TLS{}
	v.ProxyInjector.TLS = &TLS{}
	v.ProxyContainerName = k8s.ProxyContainerName

	return v, nil
}

// ValuesFromConfigMap converts the data in linkerd-config into
// a Values struct
func ValuesFromConfigMap(cm *corev1.ConfigMap) (*Values, error) {
	raw, ok := cm.Data["values"]
	if !ok {
		return nil, errors.New("Linkerd values not found in ConfigMap")
	}
	v := &Values{}
	err := yaml.Unmarshal([]byte(raw), &v)
	return v, err
}

// MergeHAValues retrieves the default HA values and merges them into the received values
func MergeHAValues(values *Values) error {
	haValues, err := readDefaults(HelmChartDirCP + "/values-ha.yaml")
	if err != nil {
		return err
	}
	*values, err = values.Merge(*haValues)
	return err
}

// readDefaults read all the default variables from filename.
func readDefaults(filename string) (*Values, error) {
	valuesFile := &loader.BufferedFile{Name: filename}
	if err := chartspkg.ReadFile(charts.Templates, "", valuesFile); err != nil {
		return nil, err
	}

	var values Values
	err := yaml.Unmarshal(chartspkg.InsertVersion(valuesFile.Data), &values)

	return &values, err
}

// Merge merges the non-empty properties of src into v.
// A new Values instance is returned. Neither src nor v are mutated after
// calling Merge.
func (v Values) Merge(src Values) (Values, error) {
	// By default, mergo.Merge doesn't overwrite any existing non-empty values
	// in its first argument. So in HA mode, we are merging values.yaml into
	// values-ha.yaml, instead of the other way round (like Helm). This ensures
	// that all the HA values take precedence.
	if err := mergo.Merge(&src, v); err != nil {
		return Values{}, err
	}

	return src, nil
}

// ToMap converts the Values intro a map[string]interface{}
func (v *Values) ToMap() (map[string]interface{}, error) {
	var valuesMap map[string]interface{}
	rawValues, err := yaml.Marshal(v)
	if err != nil {
		return nil, fmt.Errorf("Failed to marshal the values struct: %w", err)
	}

	err = yaml.Unmarshal(rawValues, &valuesMap)
	if err != nil {
		return nil, fmt.Errorf("Failed to Unmarshal Values into a map: %w", err)
	}

	return valuesMap, nil
}

// DeepCopy creates a deep copy of the Values struct by marshalling to yaml and
// then unmarshalling a new struct.
func (v *Values) DeepCopy() (*Values, error) {
	dst := Values{}
	bytes, err := yaml.Marshal(v)
	if err != nil {
		return nil, err
	}
	err = yaml.Unmarshal(bytes, &dst)
	if err != nil {
		return nil, err
	}
	return &dst, nil
}

func (v *Values) String() string {
	bytes, _ := yaml.Marshal(v)
	return string(bytes)
}
