package resource

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/gofrs/flock"
	"github.com/pkg/errors"
	"helm.sh/helm/v3/pkg/action"
	"helm.sh/helm/v3/pkg/chart/loader"
	"helm.sh/helm/v3/pkg/cli"
	"helm.sh/helm/v3/pkg/downloader"
	"helm.sh/helm/v3/pkg/getter"
	"helm.sh/helm/v3/pkg/release"
	"helm.sh/helm/v3/pkg/repo"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"sigs.k8s.io/yaml"
)

const (
	HelmCacheHomeEnvVar  = "/tmp/cache"
	HelmConfigHomeEnvVar = "/tmp/config"
	HelmDataHomeEnvVar   = "/tmp/data"
	HelmDriver           = "secret"
	stableRepoURL        = "https://charts.helm.sh/stable"
	chartLocalPath       = "/tmp/chart.tgz"
	caLocalPath          = "/tmp/ca.pem"
)

type HelmStatusData struct {
	Status       release.Status `json:",omitempty"`
	Namespace    string         `json:",omitempty"`
	ChartName    string         `json:",omitempty"`
	ChartVersion string         `json:",omitempty"`
	Chart        string         `json:",omitempty"`
	Manifest     string         `json:",omitempty"`
	Description  string         `json:",omitempty"`
}
type HelmListData struct {
	ReleaseName  string `json:",omitempty"`
	ChartName    string `json:",omitempty"`
	ChartVersion string `json:",omitempty"`
	Chart        string `json:",omitempty"`
	Namespace    string `json:",omitempty"`
}

type ReleaseState string

const (
	ReleaseFound            ReleaseState = "ReleaseFound"
	ReleaseNotFound         ReleaseState = "ReleaseNotFound"
	ReleasePending          ReleaseState = "ReleasePending"
	ReleaseError            ReleaseState = "ReleaseError"
	ReleaseAlreadyExistsMsg              = "release already exists"
)

// HelmClientInvoke generates the namespaced helm client
func helmClientInvoke(namespace *string, getter genericclioptions.RESTClientGetter) (*action.Configuration, error) {
	if namespace == nil {
		namespace = aws.String("default")
	}
	actionConfig := new(action.Configuration)
	if err := actionConfig.Init(getter, *namespace, os.Getenv("HELM_DRIVER"), func(format string, v ...interface{}) {
		fmt.Sprintf(format, v)
	}); err != nil {
		return nil, genericError("Helm client", err)
	}
	return actionConfig, nil
}

// addHelmRepoUpdate Add the repo and fire repo update
func addHelmRepoUpdate(name string, url string, username string, password string, tlsverify bool, localCA bool, settings *cli.EnvSettings) error {
	file := settings.RepositoryConfig
	os.Remove(file)
	//Ensure the file directory exists as it is required for file locking
	err := os.MkdirAll(filepath.Dir(file), os.ModePerm)
	if err != nil && !os.IsExist(err) {
		return genericError("Adding helm repository", err)
	}

	// Acquire a file lock for process synchronization
	fileLock := flock.New(strings.Replace(file, filepath.Ext(file), ".lock", 1))
	lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	locked, err := fileLock.TryLockContext(lockCtx, time.Second)
	if err == nil && locked {
		defer fileLock.Unlock()
	}
	if err != nil {
		return genericError("Adding helm repository", err)
	}

	b, err := ioutil.ReadFile(file)
	if err != nil && !os.IsNotExist(err) {
		return genericError("Adding helm repository", err)
	}

	var f repo.File
	if err := yaml.Unmarshal(b, &f); err != nil {
		return genericError("Adding helm repository", err)
	}

	c := repo.Entry{
		Name:                  name,
		URL:                   url,
		InsecureSkipTLSverify: tlsverify,
	}

	if !IsZero(username) && !IsZero(password) {
		c.Username = username
		c.Password = password
	}

	if localCA {
		c.CAFile = caLocalPath
	}

	r, err := repo.NewChartRepository(&c, getter.All(settings))
	if err != nil {
		return genericError("Adding helm repository", err)
	}

	if _, err := r.DownloadIndexFile(); err != nil {
		return genericError("Adding helm repository", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", url))
	}

	f.Update(&c)

	if err := f.WriteFile(file, 0644); err != nil {
		return genericError("Adding helm repository", err)
	}
	log.Printf("%q has been added to your repositories\n", name)
	var repos []*repo.ChartRepository
	for _, cfg := range f.Repositories {
		r, err := repo.NewChartRepository(cfg, getter.All(settings))
		if err != nil {
			genericError("Adding helm repository", err)
		}
		repos = append(repos, r)
	}
	log.Printf("Hang tight while we grab the latest from your chart repositories...")
	var wg sync.WaitGroup
	for _, re := range repos {
		wg.Add(1)
		go func(re *repo.ChartRepository) {
			defer wg.Done()
			if _, err := re.DownloadIndexFile(); err != nil {
				log.Printf("...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
			} else {
				log.Printf("...Successfully got an update from the %q chart repository\n", re.Config.Name)
			}
		}(re)
	}
	wg.Wait()
	log.Printf("Update Complete. ⎈ Happy Helming!⎈ ")
	return nil
}

// HelmInstall invokes the helm install client
func (c *Clients) HelmInstall(config *Config, values map[string]interface{}, chart *Chart, id string) error {
	var cp string
	var err error
	var state ReleaseState
	client := action.NewInstall(c.HelmClient)
	client.Description = id
	client.ReleaseName = *config.Name

	state, err = c.HelmVerifyRelease(*config.Name, id)
	if err != nil {
		return genericError("Helm install", err)
	}
	switch state {
	case ReleasePending:
		log.Printf("Release with name: %s and ID: %s is pending state.", *config.Name, id)
		return nil
	case ReleaseError:
		return err
	case ReleaseFound:
		log.Printf("Found release with name: %s and ID: %s. Please check..", *config.Name, id)
		return genericError("Helm install", errors.New(ReleaseAlreadyExistsMsg))
	}

	log.Printf("Installing release %s", *config.Name)

	switch *chart.ChartType {
	case "Remote":
		if chart.ChartVersion != nil {
			client.Version = *chart.ChartVersion
		}
		err = addHelmRepoUpdate(aws.StringValue(chart.ChartRepo), aws.StringValue(chart.ChartRepoURL), aws.StringValue(chart.ChartUsername), aws.StringValue(chart.ChartPassword), aws.BoolValue(chart.ChartSkipTLSVerify), aws.BoolValue(chart.ChartLocalCA), c.Settings)
		if err != nil {
			return genericError("Helm Install", err)
		}
		client.ChartPathOptions.InsecureSkipTLSverify = *chart.ChartSkipTLSVerify
		if !IsZero(chart.ChartUsername) && !IsZero(chart.ChartPassword) {
			client.ChartPathOptions.Username = *chart.ChartUsername
			client.ChartPathOptions.Password = *chart.ChartPassword
		}
		if *chart.ChartLocalCA {
			client.ChartPathOptions.CaFile = caLocalPath
		}
		cp, err = client.ChartPathOptions.LocateChart(*chart.Chart, c.Settings)
		if err != nil {
			return genericError("Helm Install", err)
		}
	default:
		err = c.downloadChart(*chart.ChartPath, chartLocalPath, chart.ChartUsername, chart.ChartPassword)
		if err != nil {
			return err
		}
		cp = *chart.Chart
	}
	p := getter.All(c.Settings)
	chartRequested, err := loader.Load(cp)
	if err != nil {
		return genericError("Helm install", err)
	}

	if req := chartRequested.Metadata.Dependencies; req != nil {
		if err := action.CheckDependencies(chartRequested, req); err != nil {
			if client.DependencyUpdate {
				man := &downloader.Manager{
					ChartPath:        cp,
					Keyring:          client.ChartPathOptions.Keyring,
					SkipUpdate:       false,
					Getters:          p,
					RepositoryConfig: c.Settings.RepositoryConfig,
					RepositoryCache:  c.Settings.RepositoryCache,
					Debug:            true,
				}
				if err := man.Update(); err != nil {
					return genericError("Helm install", err)
				}
			} else {
				return genericError("Helm install", err)
			}
		}
	}

	err = c.createNamespace(*config.Namespace)
	// Here is fine still
	if err != nil {
		return err
	}
	client.Namespace = *config.Namespace
	_, err = client.Run(chartRequested, values)
	if err != nil {
		return genericError("Helm install", err)
	}
	log.Printf("Release installation completed. Waiting for resources to stablize.")
	return nil
}

// HelmUninstall invokes the helm uninstaller client
func (c *Clients) HelmUninstall(name string) error {
	log.Printf("Uninstalling release %s", name)
	client := action.NewUninstall(c.HelmClient)
	res, err := client.Run(name)
	re := regexp.MustCompile(`not found`)
	if err != nil {
		if re.MatchString(err.Error()) {
			log.Printf("Release not found..")
			return fmt.Errorf(ErrCodeNotFound)
		}
		return genericError("Helm Uninstall", err)
	}
	if res != nil && res.Info != "" {
		log.Printf(res.Info)
	}
	log.Printf("Release \"%s\" uninstalled\n", name)
	return nil
}

// HelmStatus check the Status for specified release
func (c *Clients) HelmStatus(name string) (*HelmStatusData, error) {
	log.Printf("Checking release status %s", name)
	h := &HelmStatusData{}
	client := action.NewStatus(c.HelmClient)
	res, err := client.Run(name)
	re := regexp.MustCompile(`not found`)
	if err != nil {
		if re.MatchString(err.Error()) {
			log.Printf("Release not found..")
			return nil, fmt.Errorf(ErrCodeNotFound)
		}
		return nil, err
	}
	if res != nil {
		h.Namespace = res.Namespace
		h.Manifest = res.Manifest
		if res.Info != nil {
			h.Status = res.Info.Status
			h.Description = res.Info.Description
		}
		if res.Chart != nil {
			h.ChartName = res.Chart.Metadata.Name
			h.ChartVersion = res.Chart.Metadata.Version
			h.Chart = res.Chart.Metadata.Name + "-" + res.Chart.Metadata.Version
		}
	}
	log.Printf("Found release in %s status", h.Status)
	return h, nil
}

// HelmList list the release with specific chart and version in a namespace.
func (c *Clients) HelmList(config *Config, chart *Chart) ([]HelmListData, error) {
	a := []HelmListData{}
	l := HelmListData{}
	client := action.NewList(c.HelmClient)
	client.All = true
	client.AllNamespaces = true
	client.SetStateMask()
	res, err := client.Run()
	if err != nil {
		return nil, err
	}
	for _, r := range res {
		if chart.ChartVersion != nil {
			if r.Namespace == *config.Namespace && r.Chart.Metadata.Name == *chart.ChartName && r.Chart.Metadata.Version == *chart.ChartVersion {
				l.ReleaseName = r.Name
				l.Namespace = r.Namespace
				l.ChartName = r.Chart.Metadata.Name
				l.ChartVersion = r.Chart.Metadata.Version
				l.Chart = r.Chart.Metadata.Name + "-" + r.Chart.Metadata.Version
			}
		} else {
			if r.Namespace == *config.Namespace && r.Chart.Metadata.Name == *chart.ChartName {
				l.ReleaseName = r.Name
				l.Namespace = r.Namespace
				l.ChartName = r.Chart.Metadata.Name
				l.ChartVersion = r.Chart.Metadata.Version
				l.Chart = r.Chart.Metadata.Name + "-" + r.Chart.Metadata.Version
			}
		}

		if l.ReleaseName != "" {
			a = append(a, l)
		}
	}
	return a, nil
}

// HelmUpgrade invokes the helm upgrade client
func (c *Clients) HelmUpgrade(name string, config *Config, values map[string]interface{}, chart *Chart, id string) error {
	log.Printf("Upgrading release %s", name)
	client := action.NewUpgrade(c.HelmClient)
	var cp string
	var err error
	var state ReleaseState
	client.Description = id

	state, err = c.HelmVerifyRelease(name, id)
	if err != nil {
		return genericError("Helm Upgrade", err)
	}
	switch state {
	case ReleasePending:
		log.Printf("Release with name: %s and ID: %s is pending state.", name, id)
		return nil
	case ReleaseNotFound:
		return errors.New(ErrCodeNotFound)
	case ReleaseError:
		return err
	case ReleaseFound:
		log.Printf("Found release with name: %s and ID: %s. Proceeding with upgrade..", name, id)
		switch *chart.ChartType {
		case "Remote":
			if chart.ChartVersion != nil {
				client.Version = *chart.ChartVersion
			}
			err = addHelmRepoUpdate(aws.StringValue(chart.ChartRepo), aws.StringValue(chart.ChartRepoURL), aws.StringValue(chart.ChartUsername), aws.StringValue(chart.ChartPassword), aws.BoolValue(chart.ChartSkipTLSVerify), aws.BoolValue(chart.ChartLocalCA), c.Settings)
			if err != nil {
				return genericError("Helm Upgrade", err)
			}
			client.ChartPathOptions.InsecureSkipTLSverify = *chart.ChartSkipTLSVerify
			if !IsZero(chart.ChartUsername) && !IsZero(chart.ChartPassword) {
				client.ChartPathOptions.Username = *chart.ChartUsername
				client.ChartPathOptions.Password = *chart.ChartPassword
			}
			if *chart.ChartLocalCA {
				client.ChartPathOptions.CaFile = caLocalPath
			}
			cp, err = client.ChartPathOptions.LocateChart(*chart.Chart, c.Settings)
			if err != nil {
				return genericError("Helm Upgrade", err)
			}
		default:
			err = c.downloadChart(*chart.ChartPath, chartLocalPath, chart.ChartUsername, chart.ChartPassword)
			if err != nil {
				return err
			}
			cp = *chart.Chart
		}
		// Check chart dependencies to make sure all are present in /charts
		ch, err := loader.Load(cp)
		if err != nil {
			return genericError("Helm Upgrade", err)
		}
		if req := ch.Metadata.Dependencies; req != nil {
			if err := action.CheckDependencies(ch, req); err != nil {
				return genericError("Helm Upgrade", err)
			}
		}

		rel, err := client.Run(name, ch, values)
		if err != nil {
			return genericError("Helm Upgrade", err)
		}
		log.Printf("Release %q has been upgraded. Happy Helming!\n", rel.Name)
		return nil
	}

	return errors.New("unknown error")
}

// HelmVerifyDescription verifies the if the description matches ID
func (c *Clients) HelmVerifyRelease(name string, id string) (ReleaseState, error) {
	status, staterr := c.HelmStatus(name)
	if staterr != nil {
		if staterr.Error() == ErrCodeNotFound {
			return ReleaseNotFound, nil
		}
		return ReleaseError, staterr
	}

	switch status.Status {
	case release.StatusPendingInstall, release.StatusPendingUpgrade, release.StatusPendingRollback:
		log.Printf("Release: %s in status: %s", name, status.Status)
		return ReleasePending, nil
	case release.StatusDeployed:
		if status.Description == id {
			return ReleaseFound, nil
		}
		return ReleaseError, fmt.Errorf("another release exists with the same name but different ID %s instead of %s", status.Description, id)
	case release.StatusFailed:
		return ReleaseError, errors.New("release in failed status")
	default:
		return ReleaseError, errors.New("unknown error")
	}
	return ReleaseFound, nil
}