package chart

import (
	"context"
	"embed"
	"fmt"
	"io/fs"
	"path/filepath"
	"strings"

	"helm.sh/helm/v3/pkg/chart"
	"helm.sh/helm/v3/pkg/chart/loader"
	"helm.sh/helm/v3/pkg/chartutil"
	"helm.sh/helm/v3/pkg/engine"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/util/json"
	"k8s.io/klog/v2"
	"sigs.k8s.io/yaml"

	clustermanagerchart "open-cluster-management.io/ocm/deploy/cluster-manager/chart"
	klusterletchart "open-cluster-management.io/ocm/deploy/klusterlet/chart"
)

func NewDefaultClusterManagerChartConfig() *ClusterManagerChartConfig {
	return &ClusterManagerChartConfig{
		ReplicaCount:         3,
		CreateBootstrapToken: false,
		ClusterManager: ClusterManagerConfig{
			Create: true,
		},
	}
}

func NewDefaultKlusterletChartConfig() *KlusterletChartConfig {
	return &KlusterletChartConfig{
		ReplicaCount: 1,
		Klusterlet: KlusterletConfig{
			Create: true,
		},
	}
}

// RenderClusterManagerChart renders the ClusterManager objects to be created on the hub.
// It returns three values: CRD objects(which usually should be created before other objects), other Kubernetes objects, error.
func RenderClusterManagerChart(ctx context.Context, config *ClusterManagerChartConfig, namespace string) ([][]byte, [][]byte, error) {
	if namespace == "" {
		return nil, nil, fmt.Errorf("cluster manager chart namespace is required")
	}
	return renderChart(ctx, config, namespace, config.CreateNamespace,
		clustermanagerchart.ChartName, clustermanagerchart.ChartFiles)
}

// RenderKlusterletChart renders the Klusterlet objects to be created on the managed cluster.
// It returns three values: CRD objects(which usually should be created before other objects), other Kubernetes objects, error.
func RenderKlusterletChart(ctx context.Context, config *KlusterletChartConfig, namespace string) ([][]byte, [][]byte, error) {
	if namespace == "" {
		return nil, nil, fmt.Errorf("klusterlet chart namespace is required")
	}
	return renderChart(ctx, config, namespace, config.CreateNamespace,
		klusterletchart.ChartName, klusterletchart.ChartFiles)
}

func renderChart[T *ClusterManagerChartConfig | *KlusterletChartConfig](ctx context.Context, config T,
	namespace string, createNamespace bool, chartName string, fs embed.FS) ([][]byte, [][]byte, error) {
	logger := klog.FromContext(ctx)
	// chartName is the prefix of chart path here
	operatorChart, err := LoadChart(fs, chartName, logger)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to load %s chart: %w", chartName, err)
	}

	configValues, err := JsonStructToValues(config)
	if err != nil {
		return nil, nil, fmt.Errorf("error generating values for chartConfig: %v", err)
	}

	releaseOptions := chartutil.ReleaseOptions{
		Name:      chartName,
		Namespace: namespace,
	}

	values, err := chartutil.ToRenderValues(operatorChart, configValues,
		releaseOptions, &chartutil.Capabilities{})
	if err != nil {
		logger.Error(err, "failed to render helm chart", "values", values)
		return nil, nil, err
	}

	crdObjects, rawObjects, err := renderManifests(operatorChart, values, logger)
	if err != nil {
		return nil, nil, fmt.Errorf("error rendering cluster manager chart: %v", err)
	}

	// make sure the ns object is at the top of slice when createNamespace is true.
	rstObjects := [][]byte{}
	if createNamespace {
		nsObj, err := newNamespaceRawObject(namespace)
		if err != nil {
			return nil, nil, err
		}
		rstObjects = [][]byte{nsObj}
	}
	rstObjects = append(rstObjects, rawObjects...)

	return crdObjects, rstObjects, nil
}

func getFiles(manifestFS embed.FS) ([]string, error) {
	var res []string
	err := fs.WalkDir(manifestFS, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		res = append(res, path)
		return nil
	})
	return res, err
}

func stripPrefix(chartPrefix, path string) string {
	prefixNoPathSeparatorSuffix := strings.TrimSuffix(chartPrefix, string(filepath.Separator))
	chartPrefixLen := len(strings.Split(prefixNoPathSeparatorSuffix, string(filepath.Separator)))
	pathValues := strings.Split(path, string(filepath.Separator))
	return strings.Join(pathValues[chartPrefixLen:], string(filepath.Separator))
}

func LoadChart(chartFS embed.FS, chartPrefix string, logger klog.Logger) (*chart.Chart, error) {
	files, err := getFiles(chartFS)
	if err != nil {
		return nil, err
	}

	var bfs []*loader.BufferedFile
	for _, fileName := range files {
		b, err := fs.ReadFile(chartFS, fileName)
		if err != nil {
			logger.Error(err, "failed to read file", "fileName", fileName)
			return nil, err
		}
		if !strings.HasPrefix(fileName, chartPrefix) {
			continue
		}
		bf := &loader.BufferedFile{
			Name: stripPrefix(chartPrefix, fileName),
			Data: b,
		}
		bfs = append(bfs, bf)
	}

	userChart, err := loader.LoadFiles(bfs)
	if err != nil {
		logger.Error(err, "failed to load chart")
		return nil, err
	}
	return userChart, nil
}

// JsonStructToValues converts the given json struct to a Values
func JsonStructToValues(a interface{}) (chartutil.Values, error) {
	raw, err := json.Marshal(a)
	if err != nil {
		return nil, err
	}

	vals, err := chartutil.ReadValues(raw)
	if err != nil {
		return nil, err
	}
	return vals, nil
}

func renderManifests(chart *chart.Chart, values chartutil.Values, logger klog.Logger) ([][]byte, [][]byte, error) {
	var rawCRDObjects, rawObjects [][]byte

	// make sure the CRDs are at the top.
	crds := chart.CRDObjects()
	for _, crd := range crds {
		logger.V(4).Info("crd data", "crdName", crd.Name, "crdData", string(crd.File.Data))
		rawCRDObjects = append(rawCRDObjects, crd.File.Data)
	}

	helmEngine := engine.Engine{
		Strict:   true,
		LintMode: false,
	}

	templates, err := helmEngine.Render(chart, values)
	if err != nil {
		return rawCRDObjects, rawObjects, err
	}

	namespaceObjects := [][]byte{}
	for _, template := range templates {
		// skip the template only including `\n`
		if len(template) < 2 {
			continue
		}

		objects := strings.Split(template, "---")
		for _, data := range objects {
			// skip the data only including `\n`
			if len(data) < 2 {
				continue
			}
			// remove invalid template
			unstructuredObj := &unstructured.Unstructured{}
			if err = yaml.Unmarshal([]byte(data), unstructuredObj); err != nil {
				return nil, nil, fmt.Errorf("error unmarshalling template: %v", err)
			}
			kind := unstructuredObj.GetKind()
			if kind == "" {
				continue
			}

			if kind == "Namespace" {
				namespaceObjects = append(namespaceObjects, []byte(data))
				continue
			}

			rawObjects = append(rawObjects, []byte(data))
		}

	}
	// will create open-cluster-management-agent ns in klusterlet operator,
	// so need make sure namespaces are at the top of slice.
	if len(namespaceObjects) != 0 {
		result := append(namespaceObjects, rawObjects...) //nolint:gocritic
		return rawCRDObjects, result, nil
	}
	return rawCRDObjects, rawObjects, nil
}

func newNamespaceRawObject(namespace string) ([]byte, error) {
	ns := &corev1.Namespace{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "v1",
			Kind:       "Namespace",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: namespace,
		},
	}

	return yaml.Marshal(ns)
}
