package executables

import (
	"bytes"
	"context"
	"fmt"
	"strings"

	"sigs.k8s.io/yaml"

	"github.com/aws/eks-anywhere/pkg/helm"
	"github.com/aws/eks-anywhere/pkg/logger"
)

const (
	helmPath               = "helm"
	insecureSkipVerifyFlag = "--insecure-skip-tls-verify"
)

type Helm struct {
	executable Executable
	helmConfig *helm.Config // Embed HelmOptions in Helm struct
	env        map[string]string
}

// NewHelm returns a new Helm executable client.
func NewHelm(executable Executable, opts ...helm.Opt) *Helm {
	helmConfig := &helm.Config{
		Insecure: false,
	}
	for _, o := range opts {
		o(helmConfig)
	}

	env := map[string]string{
		"HELM_EXPERIMENTAL_OCI": "1",
	}

	mergeMaps(env, helmConfig.ProxyConfig)

	h := &Helm{
		executable: executable,
		helmConfig: helmConfig,
		env:        env,
	}

	return h
}

// mergeMaps joins the default and the provided maps together, then return the
// new map.
func mergeMaps(defaultEnv, newEnv map[string]string) {
	for k, v := range newEnv {
		defaultEnv[k] = v
	}
}

func (h *Helm) Template(ctx context.Context, ociURI, version, namespace string, values interface{}, kubeVersion string) ([]byte, error) {
	valuesYaml, err := yaml.Marshal(values)
	if err != nil {
		return nil, fmt.Errorf("failed marshalling values for helm template: %v", err)
	}

	params := []string{"template", h.url(ociURI), "--version", version, "--namespace", namespace, "--kube-version", kubeVersion}
	params = h.addInsecureFlagIfProvided(params)
	params = append(params, "-f", "-")

	result, err := h.executable.Command(ctx, params...).WithStdIn(valuesYaml).WithEnvVars(h.env).Run()
	if err != nil {
		return nil, err
	}

	return result.Bytes(), nil
}

func (h *Helm) PullChart(ctx context.Context, ociURI, version string) error {
	params := []string{"pull", h.url(ociURI), "--version", version}
	params = h.addInsecureFlagIfProvided(params)
	_, err := h.executable.Command(ctx, params...).
		WithEnvVars(h.env).Run()
	return err
}

// ShowValues get the values of a chart.
func (h *Helm) ShowValues(ctx context.Context, ociURI, version string) (bytes.Buffer, error) {
	params := []string{"show", "values", h.url(ociURI), "--version", version}
	out, err := h.executable.Command(ctx, params...).
		WithEnvVars(h.env).Run()
	return out, err
}

func (h *Helm) PushChart(ctx context.Context, chart, registry string) error {
	logger.Info("Pushing", "chart", chart)
	params := []string{"push", chart, registry}
	params = h.addInsecureFlagIfProvided(params)
	_, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).Run()
	return err
}

func (h *Helm) RegistryLogin(ctx context.Context, registry, username, password string) error {
	logger.Info("Logging in to helm registry", "registry", registry)
	params := []string{"registry", "login", registry, "--username", username, "--password-stdin"}
	if h.helmConfig.Insecure {
		params = append(params, "--insecure")
	}
	_, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).WithStdIn([]byte(password)).Run()
	return err
}

func (h *Helm) SaveChart(ctx context.Context, ociURI, version, folder string) error {
	params := []string{"pull", h.url(ociURI), "--version", version, "--destination", folder}
	params = h.addInsecureFlagIfProvided(params)
	_, err := h.executable.Command(ctx, params...).
		WithEnvVars(h.env).Run()
	return err
}

func (h *Helm) InstallChartFromName(ctx context.Context, ociURI, kubeConfig, name, version string) error {
	// Using upgrade --install will install the chart if it doesn't exist, but
	// upgrades it otherwise, making this more idempotent than install, which
	// would error out if the chart is already installed, and has no similar
	// "--upgrade" flag.
	params := []string{"upgrade", "--install", name, ociURI, "--version", version, "--kubeconfig", kubeConfig}
	params = h.addInsecureFlagIfProvided(params)
	_, err := h.executable.Command(ctx, params...).
		WithEnvVars(h.env).Run()
	return err
}

// InstallChart installs a helm chart to the target cluster.
//
// If kubeconfigFilePath is the empty string, it won't be passed at all.
func (h *Helm) InstallChart(ctx context.Context, chart, ociURI, version, kubeconfigFilePath, namespace, valueFilePath string, skipCRDs bool, values []string) error {
	valueArgs := GetHelmValueArgs(values)
	params := []string{"upgrade", "--install", chart, ociURI, "--version", version}
	if skipCRDs {
		params = append(params, "--skip-crds")
	}
	params = append(params, valueArgs...)
	if kubeconfigFilePath != "" {
		params = append(params, "--kubeconfig", kubeconfigFilePath)
	}
	if len(namespace) > 0 {
		params = append(params, "--create-namespace", "--namespace", namespace)
	}
	if valueFilePath != "" {
		params = append(params, "-f", valueFilePath)
	}
	params = h.addInsecureFlagIfProvided(params)

	logger.Info("Installing helm chart on cluster", "chart", chart, "version", version)
	_, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).Run()
	return err
}

// Delete removes an installation.
func (h *Helm) Delete(ctx context.Context, kubeconfigFilePath, installName, namespace string) error {
	params := []string{
		"delete", installName,
		"--kubeconfig", kubeconfigFilePath,
	}
	if namespace != "" {
		params = append(params, "--namespace", namespace)
	}

	params = h.addInsecureFlagIfProvided(params)
	if _, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).Run(); err != nil {
		return fmt.Errorf("deleting helm installation %w", err)
	}
	logger.V(6).Info("Deleted helm installation", "name", installName, "namespace", namespace)

	return nil
}

// ListCharts lists helm charts filtered on the given regex filter.
func (h *Helm) ListCharts(ctx context.Context, kubeconfigFilePath, filter string) ([]string, error) {
	params := []string{"list", "-q", "--kubeconfig", kubeconfigFilePath}

	if len(filter) > 0 {
		params = append(params, "--filter", filter)
	}

	out, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).Run()
	if err != nil {
		return nil, err
	}
	charts := strings.FieldsFunc(out.String(), func(c rune) bool {
		return c == '\n'
	})
	return charts, nil
}

func (h *Helm) addInsecureFlagIfProvided(params []string) []string {
	if h.helmConfig.Insecure {
		return append(params, insecureSkipVerifyFlag)
	}
	return params
}

func (h *Helm) url(originalURL string) string {
	registryMirror := h.helmConfig.RegistryMirror
	return registryMirror.ReplaceRegistry(originalURL)
}

func GetHelmValueArgs(values []string) []string {
	valueArgs := []string{}
	for _, value := range values {
		valueArgs = append(valueArgs, "--set", value)
	}

	return valueArgs
}

// UpgradeInstallChartWithValuesFile runs a helm upgrade --install with the provided values file and waits for the
// chart deployment to be ready. This will upgrade the chart if it exists and install if it does not.
func (h *Helm) UpgradeInstallChartWithValuesFile(ctx context.Context, chart, ociURI, version, kubeconfigFilePath, namespace, valuesFilePath string, opts ...helm.Opt) error {
	params := []string{
		"upgrade",
		"--install",
		chart, ociURI,
		"--version", version,
		"--values", valuesFilePath,
		"--kubeconfig", kubeconfigFilePath,
		"--wait",
	}

	if len(namespace) > 0 {
		params = append(params, "--namespace", namespace)
	}
	// TODO: we should not update the receiver here, so this needs to change.
	// This is not thread safe.
	// https://github.com/aws/eks-anywhere/issues/7176
	for _, opt := range opts {
		opt(h.helmConfig)
	}

	mergeMaps(h.env, h.helmConfig.ProxyConfig)

	params = h.addInsecureFlagIfProvided(params)
	params = append(params, h.helmConfig.ExtraFlags...)
	_, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).Run()
	return err
}

// Uninstall runs a helm uninstall for the given chart in given namespace.
func (h *Helm) Uninstall(ctx context.Context, chart, kubeconfigFilePath, namespace string, opts ...helm.Opt) error {
	params := []string{
		"uninstall", chart,
		"--kubeconfig", kubeconfigFilePath,
		"--wait",
	}

	if len(namespace) > 0 {
		params = append(params, "--namespace", namespace)
	}

	for _, opt := range opts {
		opt(h.helmConfig)
	}

	params = append(params, h.helmConfig.ExtraFlags...)

	logger.Info("Uninstalling helm chart on cluster", "chart", chart)
	_, err := h.executable.Command(ctx, params...).WithEnvVars(h.env).Run()
	return err
}
