package helmcli

import (
	"context"
	"errors"
	"fmt"
	"strings"

	helmclient "github.com/mittwald/go-helm-client"
	"github.com/pingcap/log"
	"helm.sh/helm/v3/pkg/action"
	"helm.sh/helm/v3/pkg/repo"
	"helm.sh/helm/v3/pkg/storage/driver"

	"github.com/tidbcloud/observability/agent-control-server/pkg/k8sclient/k8scliv1"
)

// BuildHelmClient builds a Helm client for the remote cluster and namespace.
//
// The client reuses the remote Kubernetes REST config so chart operations run
// against the target dataplane cluster instead of the local control plane.
func BuildHelmClient(ctx context.Context, remote k8scliv1.ClientGetter, namespace string) (helmclient.Client, error) {
	helmCfg, err := remote.GetHelmConfig(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get helm config: %w", err)
	}
	restCfg, err := helmCfg.RESTClientGetter.ToRESTConfig()
	if err != nil {
		return nil, fmt.Errorf("failed to build rest config: %w", err)
	}

	helm, err := helmclient.NewClientFromRestConf(&helmclient.RestConfClientOptions{
		RestConfig: restCfg,
		Options: &helmclient.Options{
			Namespace: namespace,
		},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to build helm client: %w", err)
	}

	helm.SetDebugLog(func(format string, v ...any) {
		log.Info(fmt.Sprintf(format, v...))
	})
	return helm, nil
}

// AddOrUpdateChartRepo registers or refreshes a Helm chart repository.
func AddOrUpdateChartRepo(helm helmclient.Client, name, url string) error {
	if err := helm.AddOrUpdateChartRepo(repo.Entry{
		Name: name,
		URL:  url,
	}); err != nil {
		return fmt.Errorf("failed to add or update chart repo: %w", err)
	}
	return nil
}

// isReleaseNotFoundError returns true if the error indicates that a Helm
// release does not exist.
//
// Helm's storage layer has multiple "not found" paths:
//   - driver.ErrReleaseNotFound ("release: not found") — from Query/List returning empty
//   - "no revision for release" — from Storage.Last when History returns empty slice
func isReleaseNotFoundError(err error) bool {
	if errors.Is(err, driver.ErrReleaseNotFound) {
		return true
	}
	msg := err.Error()
	return strings.Contains(msg, "not found") || strings.Contains(msg, "no revision for release")
}

// CleanupPendingRelease checks whether the named Helm release is stuck in a
// pending state.
//
// When a previous Helm operation is interrupted, Helm can leave a pending
// revision in storage and block the next install or upgrade. This cleanup only
// deletes that revision's storage entry and does not affect deployed Kubernetes
// resources or other release revisions. Missing and non-pending releases are
// treated as already clean.
func CleanupPendingRelease(helm helmclient.Client, releaseName string) error {
	rel, err := helm.GetRelease(releaseName)
	if err != nil {
		// Release doesn't exist (fresh install). Helm has multiple error paths
		// for "not found": driver.ErrReleaseNotFound from Query, or
		// "no revision for release" from Storage.Last — treat all as non-pending.
		if isReleaseNotFoundError(err) {
			return nil
		}
		return fmt.Errorf("failed to get release %s: %w", releaseName, err)
	}
	if rel.Info == nil || !rel.Info.Status.IsPending() {
		return nil
	}
	hc, ok := helm.(*helmclient.HelmClient)
	if !ok {
		return fmt.Errorf("unexpected helm client type: %T", helm)
	}
	log.Info(fmt.Sprintf("cleaning up pending release %s (version=%d, status=%s)", releaseName, rel.Version, rel.Info.Status))
	_, err = hc.ActionConfig.Releases.Delete(rel.Name, rel.Version)
	return err
}

// InstallOrUpgradeChart installs or upgrades a Helm chart release.
//
// Pending release cleanup runs first so reconciliation can recover from an
// interrupted Helm operation without requiring manual storage edits. The
// supplied labels are stamped on the release so UninstallUnknownReleases can
// identify it as caller-managed. Pass nil to skip labeling.
func InstallOrUpgradeChart(ctx context.Context, helm helmclient.Client, releaseName, chartName, namespace, version, values string, labels map[string]string) error {
	// Clean up if the release is in a pending state (e.g. a previous operation
	// was interrupted). Delete the stuck revision and proceed with install/upgrade.
	if err := CleanupPendingRelease(helm, releaseName); err != nil {
		return fmt.Errorf("failed to clean up pending release (%s/%s): %w", namespace, releaseName, err)
	}

	spec := &helmclient.ChartSpec{
		ReleaseName:     releaseName,
		ChartName:       chartName,
		Namespace:       namespace,
		Version:         version,
		ValuesYaml:      values,
		CreateNamespace: true,
		MaxHistory:      5,
		Labels:          labels,
	}

	if _, err := helm.InstallOrUpgradeChart(ctx, spec, nil); err != nil {
		return fmt.Errorf("failed to install or upgrade chart (%s/%s/%s): %w", namespace, chartName, releaseName, err)
	}
	return nil
}

// UninstallRelease uninstalls a Helm release by name.
//
// Missing releases are treated as success because delete reconciliation should
// be idempotent.
func UninstallRelease(ctx context.Context, helm helmclient.Client, name string) error {
	err := helm.UninstallReleaseByName(name)
	if err != nil && !isReleaseNotFoundError(err) {
		return fmt.Errorf("failed to uninstall chart %s: %w", name, err)
	}
	return nil
}

// UninstallUnknownReleases uninstalls deployed Helm releases in namespace
// matching matchLabels, skipping any in keep, and returns the names removed.
//
// namespace and matchLabels must be non-empty so the prune can never widen to
// releases the caller does not own.
func UninstallUnknownReleases(ctx context.Context, helm helmclient.Client, namespace string, matchLabels map[string]string, keep map[string]struct{}) ([]string, error) {
	if namespace == "" {
		return nil, fmt.Errorf("requires a non-empty namespace to avoid pruning unrelated releases")
	}
	if len(matchLabels) == 0 {
		return nil, fmt.Errorf("requires non-empty matchLabels to avoid pruning unrelated releases")
	}
	releases, err := helm.ListReleasesByStateMask(action.ListDeployed)
	if err != nil {
		return nil, fmt.Errorf("failed to list releases: %w", err)
	}
	var uninstalled []string
	for _, r := range releases {
		if r.Namespace != namespace {
			continue
		}
		if !releaseMatchesLabels(r.Labels, matchLabels) {
			continue
		}
		if _, ok := keep[r.Name]; ok {
			continue
		}
		if err := UninstallRelease(ctx, helm, r.Name); err != nil {
			return uninstalled, fmt.Errorf("failed to uninstall %s: %w", r.Name, err)
		}
		uninstalled = append(uninstalled, r.Name)
	}
	return uninstalled, nil
}

// releaseMatchesLabels reports whether releaseLabels contains every key/value
// pair in expected. Callers must guard against an empty expected (which would
// vacuously match every release); UninstallUnknownReleases rejects that up
// front rather than relying on this vacuous match.
func releaseMatchesLabels(releaseLabels, expected map[string]string) bool {
	for k, v := range expected {
		if releaseLabels[k] != v {
			return false
		}
	}
	return true
}
