package helm

import (
	"context"
	"errors"
	"fmt"
	"strconv"

	hashstructure "github.com/mitchellh/hashstructure/v2"
	"helm.sh/helm/v3/pkg/action"
	"helm.sh/helm/v3/pkg/chart"
	"helm.sh/helm/v3/pkg/release"
	"helm.sh/helm/v3/pkg/storage/driver"
	corev1 "k8s.io/api/core/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/cli-runtime/pkg/resource"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/yaml"
)

const (
	valuesIdKey    = "helm-values-id"
	chartIdKey     = "helm-chart-id"
	managedByKey   = "app.kubernetes.io/managed-by"
	managedByValue = "helm"
)

type Package interface {
	GetName() string
	GetCRDs() ([]*apiextensionsv1.CustomResourceDefinition, error)
	GetNamespace() string
	ToHelmChart() (*chart.Chart, error)
	ToHelmValues() map[string]interface{}
}

type DryRunablePackage interface {
	Package
	DryRun() bool
}

// Applier makes unit-test easier for helm.Apply.
type Applier interface {
	Apply(ctx context.Context, restConfig *rest.Config, p Package) error
}

type ApplyFunc func(ctx context.Context, restConfig *rest.Config, p Package) error

func (f ApplyFunc) Apply(ctx context.Context, restConfig *rest.Config, p Package) error {
	return f(ctx, restConfig, p)
}

func Apply(ctx context.Context, restConfig *rest.Config, p Package) error {
	dryRun := false
	dp, ok := p.(DryRunablePackage)
	if ok {
		dryRun = dp.DryRun()
	}
	// Apply the CRD first in case the chart needs it
	crds, err := p.GetCRDs()
	if err != nil {
		return fmt.Errorf("failed to get CRDs: %w", err)
	}
	cli, err := GetClient(ctx, restConfig)
	if err != nil {
		return fmt.Errorf("failed to get Kubernetes client: %w", err)
	}

	// ensure the target namespace exists for actual deployment
	if _, err = controllerutil.CreateOrUpdate(ctx, cli, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: p.GetNamespace(),
			Annotations: map[string]string{
				managedByKey: managedByValue,
			},
		},
	}, func() error { return nil }); err != nil {
		return fmt.Errorf("failed to create or update target namespace %s: %w", p.GetNamespace(), err)
	}

	for _, crd := range crds {
		obj := &apiextensionsv1.CustomResourceDefinition{}
		if err = cli.Get(ctx, client.ObjectKeyFromObject(crd), obj); err != nil {
			if !apierrors.IsNotFound(err) {
				return fmt.Errorf("failed to get CRD %s: %w", crd.Name, err)
			}
			if err = cli.Create(ctx, crd); err != nil {
				return fmt.Errorf("failed to create CRD %s: %w", crd.Name, err)
			}
		} else {
			obj.Spec = crd.Spec
			obj.Status = crd.Status
			if err = cli.Update(ctx, obj); err != nil {
				return fmt.Errorf("failed to update CRD %s: %w", crd.Name, err)
			}
		}
	}

	chart, err := p.ToHelmChart()
	if err != nil {
		return fmt.Errorf("failed to get helm chart: %w", err)
	}
	values := p.ToHelmValues()
	if chart.Metadata.Annotations == nil {
		chart.Metadata.Annotations = map[string]string{}
	}

	valuesId, chartId := Hash(values), Hash(chart)
	chart.Metadata.Annotations[valuesIdKey] = valuesId
	chart.Metadata.Annotations[chartIdKey] = chartId

	config, err := GetHelmConfig(ctx, restConfig, p.GetNamespace())
	if err != nil {
		return fmt.Errorf("failed to get Helm config: %w", err)
	}
	getclient := action.NewGet(config)

	// ensure release pacakge namespace exists
	rel, err := getclient.Run(p.GetName())
	if err != nil {
		// The release does not exist
		installclient := action.NewInstall(config)
		installclient.Namespace = p.GetNamespace()
		installclient.CreateNamespace = true
		installclient.ReleaseName = p.GetName()
		installclient.Wait = false
		installclient.WaitForJobs = false
		installclient.DryRun = dryRun
		_, err = installclient.Run(chart, values)
		return err
	}

	var appliedValuesId, appliedChartId string
	if rel.Chart.Metadata.Annotations != nil {
		appliedValuesId = rel.Chart.Metadata.Annotations[valuesIdKey]
		appliedChartId = rel.Chart.Metadata.Annotations[chartIdKey]
	}
	// If release deploy failed, we should upgrade it
	fmt.Println("helm chart:", appliedValuesId, valuesId, appliedChartId, chartId, rel.Info.Status)
	if appliedValuesId == valuesId && appliedChartId == chartId && rel.Info.Status == release.StatusDeployed {
		return nil
	}

	client := action.NewUpgrade(config)
	client.Namespace = p.GetNamespace()
	client.Install = true
	client.Wait = false
	client.WaitForJobs = false
	client.MaxHistory = 5
	client.CleanupOnFail = true
	client.DryRun = dryRun
	rel, err = client.Run(p.GetName(), chart, values)
	if err != nil {
		return fmt.Errorf("failed to apply helm chart: %w", err)
	}
	// check release info
	if rel == nil || rel.Info == nil {
		return fmt.Errorf("failed to retrieve helm release info for chart '%s'", p.GetName())
	}
	if rel.Info.Status == release.StatusDeployed {
		return nil
	}
	return fmt.Errorf("helm release '%s' deploy status: %s", p.GetName(), rel.Info.Status.String())
}

// Uninstall executes `helm uninstall`.
// It will return nil if the release does not exist.
func Uninstall(ctx context.Context, restConfig *rest.Config, p Package) error {
	releaseName := p.GetName()
	dryRun := false
	dp, ok := p.(DryRunablePackage)
	if ok {
		dryRun = dp.DryRun()
	}

	config, err := GetHelmConfig(ctx, restConfig, p.GetNamespace())
	if err != nil {
		return fmt.Errorf("failed to get Helm config: %v", err)
	}

	getclient := action.NewGet(config)
	_, err = getclient.Run(releaseName)
	if err != nil {
		// The release does not exist
		if errors.Is(err, driver.ErrReleaseNotFound) {
			return nil
		}
	}

	uninstallClient := action.NewUninstall(config)
	uninstallClient.DryRun = dryRun
	_, err = uninstallClient.Run(releaseName)
	return err
}

func RemoveNamespaceIfExists(ctx context.Context, restConfig *rest.Config, p Package) error {
	namespace := p.GetNamespace()
	cli, err := GetClient(ctx, restConfig)
	if err != nil {
		return fmt.Errorf("failed to get Kubernetes client: %w", err)
	}

	ns := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: namespace,
		},
	}
	if err = cli.Get(ctx, client.ObjectKeyFromObject(ns), ns); err != nil {
		if apierrors.IsNotFound(err) {
			return nil // Namespace does not exist, nothing to do
		}
		return fmt.Errorf("failed to get namespace %s: %w", namespace, err)
	}

	// Check if the namespace was created by this Helm infrastructure
	// Only delete if it has the managed-by annotation, otherwise skip silently
	if ns.Annotations == nil {
		return nil // Skip deletion - namespace was not created by Helm infrastructure
	}

	managedBy, exists := ns.Annotations[managedByKey]
	if !exists || managedBy != managedByValue {
		return nil // Skip deletion - namespace was not created by Helm infrastructure
	}

	if err = cli.Delete(ctx, ns); err != nil {
		return fmt.Errorf("failed to delete namespace %s: %w", namespace, err)
	}
	return nil
}

func RemoveIfExists(ctx context.Context, restConfig *rest.Config, p Package) error {
	dryRun := false
	dp, ok := p.(DryRunablePackage)
	if ok {
		dryRun = dp.DryRun()
	}
	config, err := GetHelmConfig(ctx, restConfig, p.GetNamespace())
	if err != nil {
		return fmt.Errorf("failed to get Helm config: %v", err)
	}

	getclient := action.NewGet(config)
	_, err = getclient.Run(p.GetName())
	if err != nil {
		if errors.Is(err, driver.ErrReleaseNotFound) {
			return nil
		}

		return err
	}

	uninstallClient := action.NewUninstall(config)
	uninstallClient.DryRun = dryRun
	_, err = uninstallClient.Run(p.GetName())
	return err
}

func Hash(p interface{}) string {
	h, _ := hashstructure.Hash(p, hashstructure.FormatV2, nil)
	return strconv.FormatUint(h, 10)
}

func LoadCRD(path string) ([]*apiextensionsv1.CustomResourceDefinition, error) {
	// The CRD file can be a single YAML document or multiple YAML documents.
	// Use the library from kubectl to handle all the cases.
	scheme := runtime.NewScheme()
	err := apiextensionsv1.AddToScheme(scheme)
	if err != nil {
		return nil, err
	}

	res := resource.
		NewLocalBuilder().
		WithScheme(scheme, apiextensionsv1.SchemeGroupVersion).
		Path(false, path).
		Do()
	infos, err := res.Infos()
	if err != nil {
		return nil, err
	}
	var crds []*apiextensionsv1.CustomResourceDefinition
	for _, info := range infos {
		crd, ok := info.Object.(*apiextensionsv1.CustomResourceDefinition)
		if ok {
			crds = append(crds, crd)
		}
	}
	return crds, nil
}

// LoadValuesFromYAML deal with value maps from yaml string.
// refer to: https://github.com/helm/helm/blob/v3.9.3/pkg/cli/values/options.go#L44-L58
func LoadValuesFromYAML(valuesYamlData string) (map[string]interface{}, error) {
	currentMap := map[string]interface{}{}

	if err := yaml.Unmarshal([]byte(valuesYamlData), &currentMap); err != nil {
		return nil, fmt.Errorf("failed to unmarshal yaml, %v", err)
	}
	return currentMap, nil
}
