package k8s

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	"github.com/pingcap/errors"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// Apply run 'kubectl apply -f <fname>`
func Apply(kubeconfig []byte, fname string) (output string, err error) {
	kubeconfigFilename, err := WriteTempFile(kubeconfig)
	if err != nil {
		return "", err
	}
	defer os.Remove(kubeconfigFilename)

	return ApplyWithConfigFile(kubeconfigFilename, fname)
}

// ApplyWithConfigFile run 'kubectl apply --kubeconfig={kubeconfigFilename} -f <fname>`
func ApplyWithConfigFile(kubeconfigFilename string, fname string) (output string, err error) {
	kubeconfigArg := fmt.Sprintf("--kubeconfig=%v", kubeconfigFilename)

	outputData, err := exec.Command("kubectl", kubeconfigArg, "apply", "-f", fname).CombinedOutput()
	if err != nil {
		return output, errors.AddStack(err)
	}

	return string(outputData), err
}

// WriteTempFile write a temp file with specific data and return the filename.
func WriteTempFile(data []byte) (fname string, err error) {
	file, err := os.CreateTemp("", "infra-api.*.temp")
	if err != nil {
		return "", errors.AddStack(err)
	}

	_, err = file.Write(data)
	if err != nil {
		return "", errors.AddStack(err)
	}

	err = file.Close()
	if err != nil {
		return "", errors.AddStack(err)
	}

	return file.Name(), nil
}

type CmdContext struct {
	Env   []string
	Dir   string
	Stdin io.Reader
}

func (ctx *CmdContext) Run(cmd *exec.Cmd) (string, error) {
	cmd.Env = ctx.Env
	cmd.Env = append(os.Environ(), ctx.Env...)
	cmd.Stdin = ctx.Stdin
	output, err := cmd.Output()
	if err != nil {
		return string(output), fmt.Errorf("%s failed with error: (%v) %s", cmd.String(), err, string(output))
	}

	return string(output), nil
}

type Kubectl struct {
	*CmdContext
	Kubeconfig string
	Namespace  string
}

// NewKubectl return kubectl command
func NewKubectl() *Kubectl {
	return &Kubectl{
		CmdContext: &CmdContext{},
	}
}

func (k *Kubectl) SetDir(dir string) *Kubectl {
	k.Dir = dir
	return k
}

func (k *Kubectl) WithKubeConfig(kubeconfig string) *Kubectl {
	k.Kubeconfig = kubeconfig
	return k
}

func (k *Kubectl) WithNamespace(ns string) *Kubectl {
	k.Namespace = ns
	return k
}

func (k *Kubectl) Command(args ...string) (string, error) {
	if k.Kubeconfig != "" {
		args = append([]string{"--kubeconfig", k.Kubeconfig}, args...)
	}
	if k.Namespace != "" {
		args = append([]string{"-n", k.Namespace}, args...)
	}
	cmd := exec.Command("kubectl", args...)

	return k.Run(cmd)
}

func (k *Kubectl) Apply(args ...string) (controllerutil.OperationResult, string, error) {
	var (
		output    string
		err       error
		operation = controllerutil.OperationResultNone
	)
	ops := append([]string{"apply"}, args...)
	output, err = k.Command(ops...)
	if IsConfigured(output) {
		operation = controllerutil.OperationResultUpdated
	}
	return operation, output, err
}

func (k *Kubectl) Replace(args ...string) (controllerutil.OperationResult, string, error) {
	var (
		output    string
		err       error
		operation = controllerutil.OperationResultNone
	)
	ops := append([]string{"replace"}, args...)
	output, err = k.Command(ops...)
	if IsConfigured(output) {
		operation = controllerutil.OperationResultUpdated
	}
	return operation, output, err
}

func (k *Kubectl) ApplyWithData(yaml string) (controllerutil.OperationResult, string, error) {
	k.Stdin = strings.NewReader(yaml)
	return k.Apply("--validate=false", "-f", "-")
}

func (k *Kubectl) ApplyWithFile(file string) (controllerutil.OperationResult, string, error) {
	return k.Apply("-f", file)
}

func (k *Kubectl) ReplaceWithFile(file string) (controllerutil.OperationResult, string, error) {
	return k.Replace("-f", file)
}

func (k *Kubectl) Wait(args ...string) (string, error) {
	ops := append([]string{"wait"}, args...)
	return k.Command(ops...)
}

type Helm struct {
	*CmdContext
	Kubeconfig string
}

// NewHelm return helm command
func NewHelm() *Helm {
	return &Helm{
		CmdContext: &CmdContext{},
	}
}

func (h *Helm) SetDir(dir string) *Helm {
	h.Dir = dir
	return h
}

func (h *Helm) WithKubeConfig(kubeconfig string) *Helm {
	h.Kubeconfig = kubeconfig
	return h
}

func (h *Helm) Command(args ...string) (string, error) {
	if h.Kubeconfig != "" {
		args = append([]string{"--kubeconfig", h.Kubeconfig}, args...)
	}
	cmd := exec.Command("helm", args...)

	return h.Run(cmd)
}

func (h *Helm) Template(args ...string) (string, error) {
	ops := append([]string{"template"}, args...)
	return h.Command(ops...)
}

func (h *Helm) Apply(k *Kubectl, args ...string) (controllerutil.OperationResult, string, error) {
	output, err := h.Template(args...)
	if err != nil {
		return controllerutil.OperationResultNone, output, err
	}
	return k.ApplyWithData(output)
}

func IsConfigured(output string) bool {
	if strings.Contains(output, "configured") {
		return true
	}
	return false
}
