package bootstrap

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"path"
	"time"

	"github.com/avast/retry-go/v4"
	"gopkg.in/yaml.v3"
)

var _ FluxWrapper = (*localFluxWrapper)(nil)

func NewLocalFluxWrapper(
	cmd Cmd,
	binary string,
	manifestPath string,
	helmRepositoryName string,
	helmRepositoryNamespace string,
	helmRepositoryFilepath string,
	helmReleaseName string,
	helmReleaseNamespace string,
	helmReleaseFilepath string,
	helmReleaseTargetNamespace string,
	helmReleaseValues []string,
	helmReleaseValuesFrom []string,
	fluxSourceType string,
	fluxSourceNamespace string,
	fluxSourceName string,
) FluxWrapper {
	return &localFluxWrapper{
		cmd:                        cmd,
		binary:                     binary,
		manifestPath:               manifestPath,
		helmRepositoryName:         helmRepositoryName,
		helmRepositoryNamespace:    helmRepositoryNamespace,
		helmRepositoryFilepath:     helmRepositoryFilepath,
		helmReleaseName:            helmReleaseName,
		helmReleaseNamespace:       helmReleaseNamespace,
		helmReleaseFilepath:        helmReleaseFilepath,
		helmReleaseTargetNamespace: helmReleaseTargetNamespace,
		helmReleaseValues:          helmReleaseValues,
		helmReleaseValuesFrom:      helmReleaseValuesFrom,
		fluxSourceType:             fluxSourceType,
		fluxSourceNamespace:        fluxSourceNamespace,
		fluxSourceName:             fluxSourceName,

		reconcileRetryDelay: 10 * time.Second,
	}
}

type localFluxWrapper struct {
	cmd                        Cmd
	binary                     string
	manifestPath               string
	helmRepositoryName         string
	helmRepositoryNamespace    string
	helmRepositoryFilepath     string
	helmReleaseName            string
	helmReleaseNamespace       string
	helmReleaseFilepath        string
	helmReleaseTargetNamespace string
	helmReleaseValues          []string
	helmReleaseValuesFrom      []string
	fluxSourceType             string
	fluxSourceNamespace        string
	fluxSourceName             string
	reconcileRetryDelay        time.Duration
}

func (f *localFluxWrapper) createHelmRepositoryManifest() (file, error) {
	helmRepositoryYAML, err := f.cmd.RunWithOutput(
		f.binary,
		"create",
		"source",
		"helm",
		f.helmRepositoryName,
		"--export",
		fmt.Sprintf("-n=%s", f.helmRepositoryNamespace),
		"--url=https://charts.gitlab.io",
	)
	if err != nil {
		return file{}, err
	}

	return file{path: path.Join(f.manifestPath, f.helmRepositoryFilepath), content: helmRepositoryYAML}, nil
}

type agentHelmChartValues struct {
	Config *agentHelmChartValuesConfig `yaml:"config"`
}

type agentHelmChartValuesConfig struct {
	KASAddress string `yaml:"kasAddress"`
	SecretName string `yaml:"secretName"`
}

func (f *localFluxWrapper) createHelmReleaseManifest(kasAddress string) (file, error) {
	// create temporary file for Flux CLI to read values from.
	// The Flux CLI does not yet support reading values from literal flags.
	valuesFile, err := os.CreateTemp("", "glab-bootstrap-helmrelease-values")
	if err != nil {
		return file{}, err
	}
	defer os.Remove(valuesFile.Name())
	defer valuesFile.Close()

	cfg := &agentHelmChartValues{
		Config: &agentHelmChartValuesConfig{
			KASAddress: kasAddress,
			SecretName: "gitlab-agent-token",
		},
	}

	enc := yaml.NewEncoder(valuesFile)
	if err = enc.Encode(cfg); err != nil {
		return file{}, err
	}

	args := []string{
		"create",
		"helmrelease",
		f.helmReleaseName,
		"--export",
		fmt.Sprintf("-n=%s", f.helmReleaseNamespace),
		fmt.Sprintf("--target-namespace=%s", f.helmReleaseTargetNamespace),
		"--create-target-namespace=true",
		fmt.Sprintf("--source=HelmRepository/%s.%s", f.helmRepositoryName, f.helmRepositoryNamespace),
		"--chart=gitlab-agent",
		fmt.Sprintf("--release-name=%s", f.helmReleaseName),
		fmt.Sprintf("--values=%s", valuesFile.Name()),
	}

	for _, v := range f.helmReleaseValues {
		args = append(args, fmt.Sprintf("--values=%s", v))
	}
	for _, v := range f.helmReleaseValuesFrom {
		args = append(args, fmt.Sprintf("--values-from=%s", v))
	}

	helmReleaseYAML, err := f.cmd.RunWithOutput(
		f.binary,
		args...,
	)
	if err != nil {
		return file{}, err
	}

	return file{path: path.Join(f.manifestPath, f.helmReleaseFilepath), content: helmReleaseYAML}, nil
}

func (f *localFluxWrapper) reconcile() error {
	// reconcile flux source to pull new HelmRepository source
	err := f.cmd.Run(f.binary, "reconcile", "source", f.fluxSourceType, f.fluxSourceName, fmt.Sprintf("-n=%s", f.fluxSourceNamespace))
	if err != nil {
		return err
	}

	// just reconciling doesn't mean that the HelmRelease now exists ... (bug in flux? At least very unfortunate behavior)
	err = retry.Do(func() error {
		output, err := f.cmd.RunWithOutput(f.binary, "get", "helmreleases", f.helmReleaseName, fmt.Sprintf("-n=%s", f.helmReleaseNamespace))
		if err != nil {
			// flux always returns with exit code 0, even when the helmrelease does not exist (yet)
			return retry.Unrecoverable(err)
		}

		if bytes.Contains(output, []byte(fmt.Sprintf(`HelmRelease object '%s' not found in "%s" namespace`, f.helmReleaseName, f.helmReleaseNamespace))) {
			return errors.New(string(output))
		}

		return nil
	}, retry.Attempts(6), retry.Delay(f.reconcileRetryDelay))
	if err != nil {
		return err
	}

	return f.cmd.Run(f.binary, "reconcile", "helmrelease", f.helmReleaseName, fmt.Sprintf("-n=%s", f.helmReleaseNamespace), "--with-source")
}
