package helm

import (
	"context"
	"fmt"
	"github.com/KubeOperator/kubepi/internal/server"
	"github.com/gofrs/flock"
	"github.com/pkg/errors"
	"gopkg.in/yaml.v3"
	"helm.sh/helm/v3/cmd/helm/search"
	"helm.sh/helm/v3/pkg/action"
	"helm.sh/helm/v3/pkg/chart"
	"helm.sh/helm/v3/pkg/chart/loader"
	"helm.sh/helm/v3/pkg/cli"
	"helm.sh/helm/v3/pkg/getter"
	"helm.sh/helm/v3/pkg/helmpath"
	"helm.sh/helm/v3/pkg/release"
	"helm.sh/helm/v3/pkg/repo"
	"io/ioutil"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/client-go/rest"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

const (
	helmDriver = "secrets"
)

func nolog(format string, v ...interface{}) {}

type Config struct {
	Host          string
	BearerToken   string
	OldNamespace  string
	Namespace     string
	Architectures string
	ClusterName   string
	KubeConfig    *rest.Config
}
type Client struct {
	actionConfig  *action.Configuration
	Namespace     string
	settings      *cli.EnvSettings
	Architectures string
	ClusterName   string
}

func GetSettings(cluster string) *cli.EnvSettings {
	checkFiles(cluster)
	return &cli.EnvSettings{
		PluginsDirectory: helmpath.DataPath(cluster + "/plugins"),
		RegistryConfig:   helmpath.ConfigPath(cluster + "/registry.json"),
		RepositoryConfig: helmpath.ConfigPath(cluster + "/repositories.yaml"),
		RepositoryCache:  helmpath.CachePath(cluster + "/repository"),
	}
}

func checkFiles(cluster string) {

	var dataPath = helmpath.ConfigPath(cluster)
	var repositoryPath = dataPath + "/repositories.yaml"
	var registryPath = dataPath + "/registry.json"

	if _, err := os.Stat(dataPath); os.IsNotExist(err) {
		os.MkdirAll(dataPath, 0777)
		os.Chmod(dataPath, 0777)
	}
	if _, err := os.Stat(repositoryPath); os.IsNotExist(err) {
		os.Create(repositoryPath)
	}
	if _, err := os.Stat(registryPath); os.IsNotExist(err) {
		os.Create(registryPath)
	}
	return
}

func NewClient(config *Config) (*Client, error) {
	client := Client{
		Architectures: config.Architectures,
	}
	client.settings = GetSettings(config.ClusterName)
	cf := genericclioptions.NewConfigFlags(true)
	apiServer := config.Host
	cf.APIServer = &apiServer
	kubeConfig := config.KubeConfig
	cf.WrapConfigFn = func(config *rest.Config) *rest.Config {
		return kubeConfig
	}
	cf.CacheDir = nil
	if config.Namespace != "" {
		client.Namespace = config.Namespace
		cf.Namespace = &client.Namespace
	}
	client.ClusterName = config.ClusterName
	actionConfig := new(action.Configuration)
	if err := actionConfig.Init(cf, config.Namespace, helmDriver, nolog); err != nil {
		return nil, err
	}
	client.actionConfig = actionConfig

	return &client, nil
}

func (c Client) List(limit, offset int, pattern string) ([]*release.Release, int, error) {
	client := action.NewList(c.actionConfig)
	if c.Namespace == "" {
		client.AllNamespaces = true
		client.All = true
	}
	client.SetStateMask()
	list, err := client.Run()
	if err != nil {
		return nil, 0, err
	}
	client.Limit = limit
	client.Offset = offset - 1
	if pattern != "" {
		client.Filter = pattern
	}
	result, err := client.Run()
	if err != nil {
		return nil, 0, err
	}

	return result, len(list), nil
}

func (c Client) GetDetail(name string) (*release.Release, error) {
	client := action.NewGet(c.actionConfig)
	result, err := client.Run(name)
	if err != nil {
		return nil, err
	}
	return result, nil
}

func (c Client) Install(name, repoName, chartName, chartVersion string, values map[string]interface{}) (*release.Release, error) {
	repos, err := c.ListRepo()
	if err != nil {
		return nil, err
	}
	var rp *repo.Entry
	for _, r := range repos {
		if r.Name == repoName {
			rp = r
		}
	}
	if rp == nil {
		return nil, errors.New("get chart detail failed, repo not found")
	}
	client := action.NewInstall(c.actionConfig)
	client.ReleaseName = name
	client.Namespace = c.Namespace
	client.RepoURL = rp.URL
	client.Username = rp.Username
	client.Password = rp.Password
	client.ChartPathOptions.InsecureSkipTLSverify = true
	if len(chartVersion) != 0 {
		client.ChartPathOptions.Version = chartVersion
	}
	p, err := client.ChartPathOptions.LocateChart(chartName, c.settings)
	if err != nil {
		return nil, fmt.Errorf("locate chart %s failed: %v", chartName, err)
	}
	ct, err := loader.Load(p)
	if err != nil {
		return nil, fmt.Errorf("load chart %s failed: %v", chartName, err)
	}
	re, err := client.Run(ct, values)
	if err != nil {
		return re, errors.Wrap(err, fmt.Sprintf("install %s with chart %s failed: %v", name, chartName, err))
	}
	return re, nil
}

func (c Client) Upgrade(name, repoName, chartName, chartVersion string, values map[string]interface{}) (*release.Release, error) {
	repos, err := c.ListRepo()
	if err != nil {
		return nil, err
	}
	var rp *repo.Entry
	for _, r := range repos {
		if r.Name == repoName {
			rp = r
		}
	}
	if rp == nil {
		return nil, errors.New("get chart detail failed, repo not found")
	}

	client := action.NewUpgrade(c.actionConfig)
	client.Namespace = c.Namespace
	client.RepoURL = rp.URL
	client.Username = rp.Username
	client.Password = rp.Password
	client.DryRun = false
	client.ChartPathOptions.InsecureSkipTLSverify = true
	client.ChartPathOptions.Version = chartVersion
	p, err := client.ChartPathOptions.LocateChart(chartName, c.settings)
	if err != nil {
		return nil, errors.Wrap(err, fmt.Sprintf("locate chart %s failed: %v", chartName, err))
	}
	ct, err := loader.Load(p)
	if err != nil {
		return nil, errors.Wrap(err, fmt.Sprintf("load chart %s failed: %v", chartName, err))

	}

	release, err := client.Run(name, ct, values)
	if err != nil {
		return release, errors.Wrap(err, fmt.Sprintf("upgrade tool %s with chart %s failed: %v", name, chartName, err))
	}
	return release, nil
}

func (c Client) Uninstall(name string) (*release.UninstallReleaseResponse, error) {
	client := action.NewUninstall(c.actionConfig)
	res, err := client.Run(name)
	if err != nil {
		return nil, errors.Wrap(err, fmt.Sprintf("uninstall tool %s failed: %v", name, err))
	}
	return res, nil
}

func (c Client) ListCharts(repoName, pattern string) ([]*search.Result, error) {
	repos, err := c.ListRepo()
	if err != nil {
		return nil, fmt.Errorf("list chart failed: %v", err)
	}
	i := search.NewIndex()
	for _, re := range repos {
		if repoName != "KRepoAll" {
			if repoName != re.Name {
				continue
			}
		}
		settings := GetSettings(c.ClusterName)
		path := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(re.Name))
		indexFile, err := repo.LoadIndexFile(path)
		if err != nil {
			return nil, fmt.Errorf("list chart failed: %v", err)
		}
		i.AddRepo(re.Name, indexFile, true)
	}
	var result []*search.Result
	if pattern != "" {
		result = i.SearchLiteral(pattern, 1)
	} else {
		result = i.All()
	}
	search.SortScore(result)
	return result, nil
}

func (c Client) GetCharts(repoName, name string) ([]*search.Result, error) {
	charts, err := c.ListCharts(repoName, name)
	if err != nil {
		return nil, err
	}
	var result []*search.Result
	for _, chart := range charts {
		if chart.Chart.Name == name {
			result = append(result, chart)
		}
	}
	return result, nil
}

func (c Client) GetChartDetail(repoName, name, version string) (*chart.Chart, error) {
	repos, err := c.ListRepo()
	if err != nil {
		return nil, err
	}
	var re *repo.Entry
	for _, r := range repos {
		if r.Name == repoName {
			re = r
		}
	}
	if re == nil {
		return nil, errors.New("get chart detail failed, repo not found")
	}

	cacheFilePath := c.settings.RepositoryCache + "/" + name + "-" + version + ".tgz"
	_, err = os.Stat(cacheFilePath)
	if err != nil {
		client := action.NewShow(action.ShowAll)
		client.Version = version
		client.RepoURL = re.URL
		client.Username = re.Username
		client.Password = re.Password
		cacheFilePath, err = client.LocateChart(name, c.settings)
		if err != nil {
			return nil, err
		}
	}
	ct, err := loader.Load(cacheFilePath)
	if err != nil {
		return nil, err
	}
	return ct, nil
}

func (c Client) ListRepo() ([]*repo.Entry, error) {
	settings := GetSettings(c.ClusterName)
	var repos []*repo.Entry
	f, err := repo.LoadFile(settings.RepositoryConfig)
	if err != nil {
		return repos, err
	}
	return f.Repositories, nil
}

func (c Client) RemoveRepo(name string) (bool, error) {
	settings := GetSettings(c.ClusterName)
	f, err := repo.LoadFile(settings.RepositoryConfig)
	if err != nil {
		return false, err
	}
	success := f.Remove(name)
	if !success {
		return false, err
	}
	if err := f.WriteFile(settings.RepositoryConfig, 0644); err != nil {
		return false, err
	}
	return true, nil
}

func (c Client) AddRepo(name string, url string, username string, password string) error {
	settings := GetSettings(c.ClusterName)

	repoFile := settings.RepositoryConfig

	err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm)
	if err != nil && !os.IsExist(err) {
		return err
	}

	fileLock := flock.New(strings.Replace(repoFile, filepath.Ext(repoFile), ".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 func() {
			if err := fileLock.Unlock(); err != nil {
				server.Logger().Errorf("addRepo fileLock.Unlock failed, error: %s", err.Error())
			}
		}()
	}
	if err != nil {
		return err
	}

	b, err := ioutil.ReadFile(repoFile)
	if err != nil && !os.IsNotExist(err) {
		return err
	}

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

	if f.Has(name) {
		return errors.Errorf("repository name (%s) already exists, please specify a different name", name)
	}

	e := repo.Entry{
		Name:                  name,
		URL:                   url,
		Username:              username,
		Password:              password,
		InsecureSkipTLSverify: true,
	}

	r, err := repo.NewChartRepository(&e, getter.All(settings))
	if err != nil {
		return err
	}
	r.CachePath = settings.RepositoryCache
	if _, err := r.DownloadIndexFile(); err != nil {
		return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", url)
	}
	f.Update(&e)
	if err := f.WriteFile(repoFile, 0644); err != nil {
		return err
	}
	return nil
}

func (c Client) UpdateRepo(repoName string) error {
	repos, err := c.ListRepo()
	if err != nil {
		return err
	}
	var re *repo.Entry
	for _, r := range repos {
		if r.Name == repoName {
			re = r
		}
	}
	if re == nil {
		return errors.New("repo not found")
	}

	settings := GetSettings(c.ClusterName)
	repoFile := settings.RepositoryConfig
	repoCache := settings.RepositoryCache
	f, err := repo.LoadFile(repoFile)
	if err != nil {
		return fmt.Errorf("load file of repo %s failed: %v", repoFile, err)
	}
	f.Update(re)
	var rps []*repo.ChartRepository
	for _, cfg := range f.Repositories {
		r, err := repo.NewChartRepository(cfg, getter.All(settings))
		if err != nil {
			return err
		}
		if repoCache != "" {
			r.CachePath = repoCache
		}
		rps = append(rps, r)
	}
	updateCharts(rps)
	return nil
}

func (c Client) GetRepo(name string) (*repo.Entry, error) {
	repos, err := c.ListRepo()
	if err != nil {
		return nil, err
	}
	var re *repo.Entry
	for _, r := range repos {
		if r.Name == name {
			re = r
			break
		}
	}
	return re, nil
}

func updateCharts(repos []*repo.ChartRepository) {
	fmt.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 {
				fmt.Printf("...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
			} else {
				fmt.Printf("...Successfully got an update from the %q chart repository\n", re.Config.Name)
			}
		}(re)
	}
	wg.Wait()
	fmt.Printf("Update Complete. ⎈ Happy Helming!⎈ ")
}
