/*
Copyright 2019 The Jetstack cert-manager contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package helm

import (
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"

	flag "github.com/spf13/pflag"

	"github.com/jetstack/cert-manager/hack/release/pkg/bazel"
	logf "github.com/jetstack/cert-manager/hack/release/pkg/log"
)

var (
	Default = &Helm{}
	log     = logf.Log.WithName("helm")
)

type Helm struct {
}

func (g *Helm) AddFlags(fs *flag.FlagSet) {
}

func (g *Helm) Validate() []error {
	var errs []error

	return errs
}

func (g *Helm) Complete() error {
	return nil
}

func (g *Helm) Cmd(ctx context.Context, args ...string) *exec.Cmd {
	if ctx == nil {
		ctx = context.Background()
	}
	return bazel.Default.Run(ctx, "//hack/bin:helm", append([]string{"--"}, args...)...)
}

func (g *Helm) Init(ctx context.Context, clientOnly bool, args ...string) *exec.Cmd {
	tmpl := []string{"init"}
	if clientOnly {
		tmpl = append(tmpl, "--client-only")
	}
	return g.Cmd(ctx, append(tmpl, args...)...)
}

func (g *Helm) Template(ctx context.Context, output io.Writer, target string, args ...string) *exec.Cmd {
	cmd := g.Cmd(ctx, append([]string{"template", target}, args...)...)
	cmd.Stdout = output
	return cmd
}

func (g *Helm) TemplateE(ctx context.Context, target string, args ...string) (string, error) {
	f, err := ioutil.TempFile("", "")
	if err != nil {
		return "", err
	}
	defer f.Close()

	if err := g.Template(ctx, f, target, args...).Run(); err != nil {
		return "", err
	}

	return filepath.Abs(f.Name())
}

// Package will construct a new exec.Cmd that will run "helm package"
func (g *Helm) Package(ctx context.Context, target, outputDir string, args ...string) *exec.Cmd {
	return g.Cmd(ctx, append([]string{"package", target, "--destination", outputDir}, args...)...)
}

func (g *Helm) InitE(ctx context.Context, clientOnly bool, args ...string) error {
	return g.Init(ctx, clientOnly, args...).Run()
}

func (g *Helm) PackageE(ctx context.Context, target string, outputDir string, args ...string) (string, error) {
	if outputDir == "" {
		var err error
		outputDir, err = ioutil.TempDir("", "cert-manager-release-")
		if err != nil {
			return "", err
		}
	}

	// This logic has to be horribly complicated as Helm does not automatically
	// honour the `--version` field for *subcharts* named with file://.
	// To work around this, we find all subcharts and run `helm package` on
	// each one, setting the `--version` flag.
	// We then collate all these packaged charts into the 'charts/' directory
	// before packaging the actual chart.

	var childPackagePaths []string
	err := filepath.Walk(target, func(path string, info os.FileInfo, err error) error {
		if filepath.Base(path) == "Chart.yaml" && filepath.Clean(target) != filepath.Dir(path) {
			log.Info("processing child chart", "path", path)

			dir, err := ioutil.TempDir("", "")
			if err != nil {
				return err
			}
			packagePath, err := g.packageChart(ctx, filepath.Dir(path), dir, args...)
			if err != nil {
				return err
			}

			childPackagePaths = append(childPackagePaths, packagePath)
		}
		return nil
	})
	if err != nil {
		return "", err
	}

	log.Info("built child packages", "packages", childPackagePaths)
	subchartDir := filepath.Join(target, "charts")
	log.Info("cleaning subcharts directory", "path", subchartDir)
	if err := os.RemoveAll(subchartDir); err != nil {
		return "", fmt.Errorf("failed to clean up existing subchart dir: %v", err)
	}
	// #nosec G301 Build component
	if err := os.MkdirAll(subchartDir, 0755); err != nil { // #nosec G301
		return "", fmt.Errorf("failed to create new subchart dir: %v", err)
	}

	for _, path := range childPackagePaths {
		log.Info("copying file", "input", path)
		input, err := ioutil.ReadFile(path) /* #nosec G304 */
		if err != nil {
			return "", fmt.Errorf("error reading file: %v", err)
		}

		outFile := filepath.Join(subchartDir, filepath.Base(path))
		if err := ioutil.WriteFile(outFile, input, 0644); err != nil {
			return "", fmt.Errorf("error writing new file: %v", err)
		}
		if err := os.RemoveAll(path); err != nil {
			return "", fmt.Errorf("error removing old file: %v", err)
		}
		log.Info("copied file", "file", outFile)
	}

	return g.packageChart(ctx, target, outputDir, args...)
}

func (g *Helm) packageChart(ctx context.Context, target, outputDir string, args ...string) (string, error) {
	if err := g.Package(ctx, target, outputDir, args...).Run(); err != nil {
		return "", err
	}

	files, err := ioutil.ReadDir(outputDir)
	if err != nil {
		return "", err
	}

	for _, f := range files {
		if f.IsDir() {
			continue
		}

		if strings.HasSuffix(f.Name(), ".tgz") {
			return path.Join(outputDir, f.Name()), nil
		}
	}

	return "", fmt.Errorf("cannot find chart package output file in directory %q", outputDir)
}
