package main

import (
	"bytes"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"os"
	"os/exec"
	"path"
	"strings"
	"text/template"
	"time"

	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/gogo/protobuf/proto"
	"sigs.k8s.io/yaml"

	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var (
	kubeconfig      = flag.String("infra-kubeconfig", "", "path of the infra kubeconfig")
	config          = flag.String("config", "", "config of the prototype")
	delete          = flag.Bool("delete", false, "whether to delete the cluster")
	clusterInfoPath = flag.String("cluster-info", "", "path of the cluster info")

	created = clusterInfo{}

	readFileErr = fmt.Errorf("read file error")
)

func main() {
	flag.Parse()
	infractl := kubectl{kubeconfig: *kubeconfig}

	if *clusterInfoPath == "" {
		log.Fatal("--cluster-info must be set")
	}
	if *config == "" {
		log.Fatal("--config must be set")
	}
	req, err := loadRequest(*config)
	if err != nil {
		log.Fatal(err)
	}
	created, err = loadClusterInfo(*clusterInfoPath)
	if err != nil {
		log.Fatal(err)
	}

	if *delete {
		deleteCluster(req)
		return
	}

	defer func() {
		output, err := yaml.Marshal(created)
		if err != nil {
			panic(err)
		}
		err = os.WriteFile(*clusterInfoPath, output, 0666)
		if err != nil {
			fmt.Println(" >>> Please save the following information for cluster deletion, failed writing to file:", *clusterInfoPath)
			fmt.Println(string(output))
			panic(err)
		}
	}()
	fmt.Println(" >>>>>> Creating EKS....")
	created.Name = req.Name
	if err := createEKS(infractl, req); err != nil {
		log.Fatal(err)
	}
	// if err := configureECRPolicies(req); err != nil {
	// 	log.Fatal(err)
	// }

	fmt.Println(" >>>>>> Creating Shared Node Group....")
	if err := createSharedNodeGroup(infractl, req); err != nil {
		log.Fatal(err)
	}

	fmt.Println(" >>>>>> Creating TiDB Node Group....")
	if err := createTiDBNodeGroup(infractl, req); err != nil {
		log.Fatal(err)
	}

	fmt.Println(" >>>>>> Waiting for EKS to be ready....")
	if err := waitForEKSClusterReady(req); err != nil {
		log.Fatal(err)
	}

	envs, err := getAssumeCredentialEnvs(req)
	if err != nil {
		log.Fatal(err)
	}
	a := aws{command{envs: envs}}

	fmt.Println(" >>>>>> Creating Kubernetes Endpoint....")
	c, err := getCluster(a, req)
	if err != nil {
		log.Fatal(err)
	}
	endpoints, err := createKubernetesEndpoint(a, c, req)
	if err != nil {
		log.Fatal(err)
	}

	endpoint, err := waitForEKSEndpointReady(endpoints)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(" >>>>>> Getting kubeconfig....")
	ctl := kubectl{kubeconfig: req.Kubeconfig, command: command{envs: envs}}
	if err := updateKubeconfig(a, ctl, req, c.ARN, endpoint); err != nil {
		log.Fatal(err)
	}

	fmt.Println(" >>>>>> Creating TiDB operator....")
	time.Sleep(10 * time.Second)
	if err := createTiDBOperator(ctl, req); err != nil {
		log.Fatal(err)
	}

	fmt.Println(" >>>>>> Creating CSI Driver....")
	if err := createCSIDriver(ctl, req); err != nil {
		log.Fatal(err)
	}

	fmt.Println(" >>>>>> Creating TiDB Cluster....")
	if err := createTiDBCluster(ctl, req); err != nil {
		log.Fatal(err)
	}

	fmt.Println(" >>>>>> Creating TiDB Service....")
	if err := createTiDBService(ctl, req); err != nil {
		log.Fatal(err)
	}

	output, err := ctl.output([]string{"get", "svc", "-n", "tidb1", "tidb-service", "-o", "yaml"})
	if err == nil {
		fmt.Println(string(output))
	}
}

func updateKubeconfig(a aws, ctl kubectl, req request, eksARN, endpoint string) error {
	args := []string{
		"eks", "update-kubeconfig",
		"--name", req.Name,
		"--region", req.Region,
		"--kubeconfig", req.Kubeconfig,
	}
	if err := a.run(args); err != nil {
		return err
	}
	args = []string{
		"config",
		"set-cluster", eksARN,
		"--server=https://" + endpoint,
		"--insecure-skip-tls-verify=true",
	}
	return ctl.run(args)
}

func createTiDBService(ctl kubectl, req request) error {
	return ctl.applyf("tidb-service.yaml")
}

func createTiDBCluster(ctl kubectl, req request) error {
	if err := ctl.run([]string{"create", "ns", "tidb1"}); err != nil {
		return err
	}
	t, err := template.ParseFiles("cluster-template.yaml")
	if err != nil {
		return err
	}

	var buf bytes.Buffer
	if err := t.Execute(&buf, req); err != nil {
		return err
	}
	data := buf.String()
	if err := ctl.apply(data); err != nil {
		return err
	}
	return nil
}

func createCSIDriver(ctl kubectl, req request) error {
	h := helm{kubeconfig: ctl.kubeconfig, command: ctl.command}
	if err := h.run([]string{"repo", "add", "aws-ebs-csi-driver", "https://kubernetes-sigs.github.io/aws-ebs-csi-driver"}); err != nil {
		return err
	}
	if err := h.run([]string{"repo", "update"}); err != nil {
		return err
	}
	err := h.run([]string{
		"upgrade", "--install",
		"aws-ebs-csi-driver", "aws-ebs-csi-driver/aws-ebs-csi-driver",
		"-n", "kube-system",
		"-f", "csi-values.yaml"})
	if err != nil {
		return err
	}

	return ctl.applyf("gp3.yaml")
}

func createTiDBOperator(ctl kubectl, req request) error {
	if err := ctl.run([]string{"create", "ns", "tidb-admin"}); err != nil {
		return err
	}
	if err := ctl.applyf("https://raw.githubusercontent.com/pingcap/tidb-operator/v1.2.3/manifests/crd.yaml"); err != nil {
		return err
	}
	h := helm{kubeconfig: ctl.kubeconfig, command: ctl.command}
	if err := h.run([]string{"repo", "add", "pingcap", "https://charts.pingcap.org/"}); err != nil {
		return err
	}
	return h.run([]string{
		"install",
		"--namespace", "tidb-admin",
		"tidb-operator", "pingcap/tidb-operator",
		"--version", "v1.2.3",
		"--set", "operatorImage=385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tidb-operator:v1.2.3,scheduler.kubeSchedulerImageName=385595570414.dkr.ecr.us-west-2.amazonaws.com/dbaas/tangent/kube-scheduler,kubeSchedulerImageTag=v1.21.2",
	})
}

func createSharedNodeGroup(infractl kubectl, req request) error {
	ng := manualv1alpha1.NodeGroup{
		TypeMeta: v1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1alpha1",
			Kind:       "NodeGroup",
		},
		ObjectMeta: v1.ObjectMeta{
			Name: req.Name + "-shared",
		},
		Spec: manualv1alpha1.NodeGroupSpec{
			ForProvider: manualv1alpha1.NodeGroupParameters{
				ClusterName:   req.Name,
				Region:        req.Region,
				InstanceTypes: []string{"t3.medium"},
				ScalingConfig: &manualv1alpha1.NodeGroupScalingConfig{
					DesiredSize: proto.Int(2),
					MaxSize:     proto.Int(2),
					MinSize:     proto.Int(2),
				},
				NodeRole: req.EKSNodeARN,
				Subnets:  req.SubnetIDs,
				Labels:   map[string]string{"tidbcloud.com/component": "shared"},
				Tags:     map[string]string{"Vendor": "PingCAP"},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: req.Name},
			},
		},
	}
	out, err := yaml.Marshal(ng)
	if err != nil {
		return err
	}
	return infractl.apply(string(out))
}

func createTiDBNodeGroup(infractl kubectl, req request) error {
	ng := manualv1alpha1.NodeGroup{
		TypeMeta: v1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1alpha1",
			Kind:       "NodeGroup",
		},
		ObjectMeta: v1.ObjectMeta{
			Name: req.Name + "-tidb",
		},
		Spec: manualv1alpha1.NodeGroupSpec{
			ForProvider: manualv1alpha1.NodeGroupParameters{
				ClusterName:   req.Name,
				Region:        req.Region,
				InstanceTypes: []string{"t3.medium"},
				ScalingConfig: &manualv1alpha1.NodeGroupScalingConfig{
					DesiredSize: proto.Int(2),
					MaxSize:     proto.Int(2),
					MinSize:     proto.Int(2),
				},
				NodeRole: req.EKSNodeARN,
				Subnets:  req.SubnetIDs,
				Labels:   map[string]string{"tidbcloud.com/component": "tidb"},
				Tags:     map[string]string{"Vendor": "PingCAP"},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: req.Name},
			},
		},
	}
	out, err := yaml.Marshal(ng)
	if err != nil {
		return err
	}
	return infractl.apply(string(out))
}

func createEKS(infractl kubectl, req request) error {
	t, err := template.ParseFiles("provider-config-template.yaml")
	if err != nil {
		return err
	}

	var buf bytes.Buffer
	if err := t.Execute(&buf, req); err != nil {
		return err
	}
	data := buf.String()
	if err := infractl.apply(data); err != nil {
		return err
	}

	cluster := v1beta1.Cluster{
		TypeMeta: v1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1beta1",
			Kind:       "Cluster",
		},
		ObjectMeta: v1.ObjectMeta{
			Name: req.Name,
		},
		Spec: v1beta1.ClusterSpec{
			ForProvider: v1beta1.ClusterParameters{
				Region: proto.String(req.Region),
				ResourcesVpcConfig: v1beta1.VpcConfigRequest{
					SecurityGroupIDs:      []string{req.SecurityGroupID},
					SubnetIDs:             req.SubnetIDs,
					EndpointPrivateAccess: proto.Bool(true),
					EndpointPublicAccess:  proto.Bool(false),
				},
				RoleArn: req.EKSRole,
				Tags: map[string]string{
					"Vendor": "PingCAP",
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: req.Name},
			},
		},
	}

	out, err := yaml.Marshal(cluster)
	if err != nil {
		return err
	}
	return infractl.apply(string(out))
}

func waitForEKSClusterReady(req request) error {
	envs, err := getAssumeCredentialEnvs(req)
	if err != nil {
		return err
	}
	a := aws{command{envs: envs}}
	timeout := time.After(20 * time.Minute)
	interval := time.Tick(time.Minute)
	for {
		select {
		case <-timeout:
			return fmt.Errorf("error timeout waiting for EKS cluster ready")
		case <-interval:
			fmt.Println("Polling the cluster status....")
			c, err := getCluster(a, req)
			if err != nil {
				fmt.Println("Error: ", err)
				continue
			}
			if c.status != "ACTIVE" {
				fmt.Println("Cluster status not active: ", c.status)
				continue
			}
			return nil
		}
	}
}

func waitForEKSEndpointReady(endpoints []string) (string, error) {
	timeout := time.After(7 * time.Minute)
	interval := time.Tick(10 * time.Second)
	for {
		select {
		case <-timeout:
			return "", fmt.Errorf("error timeout waiting for EKS endpoint ready")
		case <-interval:
			for _, endpoint := range endpoints {
				addr := "https://" + endpoint
				cmd := exec.Command("curl", "-k", addr, "-m", "5")
				fmt.Println("curling ", addr)
				err := cmd.Run()
				if err == nil {
					return endpoint, nil
				}
				continue
			}
		}
	}
}

type cluster struct {
	status   string
	endpoint string
	ARN      string
}

func getCluster(a aws, req request) (cluster, error) {
	var result cluster

	args := []string{"eks", "describe-cluster", "--name", req.Name, "--region", req.Region}
	data, err := a.output(args)
	// c := command{binary: "env", envs: a.envs}
	// data, err := c.combinedOutput([]string{}, cmdOpt{})
	if err != nil {
		return result, fmt.Errorf("error describing the cluster: %v", err)
	}
	dataStr := string(data)
	status, err := parseJSON(dataStr, ".cluster.status")
	if err != nil {
		return result, err
	}
	endpoint, err := parseJSON(dataStr, ".cluster.endpoint")
	if err != nil {
		return result, err
	}
	arn, err := parseJSON(dataStr, ".cluster.arn")
	if err != nil {
		return result, err
	}
	result.status = status
	result.endpoint = endpoint
	result.ARN = arn
	return result, nil
}

func getAssumeCredentialEnvs(req request) (map[string]string, error) {
	a := aws{}

	session := fmt.Sprint(rand.Int())
	args := []string{"sts", "assume-role", "--role-arn", req.AssumeRole, "--role-session-name", "kubectl-" + session, "--external-id", req.ExternalID}
	data, err := a.output(args)
	if err != nil {
		return nil, err
	}

	accessKey, _ := parseJSON(string(data), ".Credentials.AccessKeyId")
	secretKey, _ := parseJSON(string(data), ".Credentials.SecretAccessKey")
	sessionToken, _ := parseJSON(string(data), ".Credentials.SessionToken")

	return map[string]string{
		"AWS_ACCESS_KEY_ID":     accessKey,
		"AWS_SECRET_ACCESS_KEY": secretKey,
		"AWS_SESSION_TOKEN":     sessionToken,
	}, nil
}

func createKubernetesEndpoint(a aws, c cluster, req request) ([]string, error) {
	args := []string{
		"elbv2", "create-target-group",
		"--name", req.Name,
		"--protocol", "TCP",
		"--port", "443",
		"--vpc-id", req.VpcID,
		"--target-type", "ip",
		"--tags", "Key=Vendor,Value=PingCAP",
		"--region", req.Region,
	}
	output, err := a.output(args)
	if err != nil {
		return nil, err
	}
	targetGroupArn, err := parseJSON(string(output), ".TargetGroups[0].TargetGroupArn")
	if err != nil {
		return nil, err
	}
	if targetGroupArn == "" {
		return nil, fmt.Errorf("empty target group ARN")
	}
	created.TargetGroupArn = targetGroupArn

	hostname := strings.TrimPrefix(c.endpoint, "https://")
	hostname = strings.TrimPrefix(hostname, "http://")

	addrs, err := net.LookupHost(hostname)
	if err != nil {
		return nil, err
	}
	var addrsStr []string
	for _, addr := range addrs {
		addrsStr = append(addrsStr, fmt.Sprintf("Id=%s", addr))
	}

	args = []string{
		"elbv2", "register-targets",
		"--target-group-arn", targetGroupArn,
		"--targets",
	}
	args = append(args, addrsStr...)
	_, err = a.output(args)
	if err != nil {
		return nil, err
	}

	args = []string{
		"elbv2", "create-load-balancer",
		"--name", req.Name,
		"--scheme", "internal",
		"--tags", "Key=Vendor,Value=PingCAP",
		"--type", "network",
		"--subnets",
	}
	args = append(args, req.SubnetIDs...)
	output, err = a.output(args)
	if err != nil {
		return nil, err
	}
	loadBalancerArn, err := parseJSON(string(output), ".LoadBalancers[0].LoadBalancerArn")
	if err != nil {
		return nil, err
	}
	created.LoadBalancerArn = loadBalancerArn

	args = []string{
		"elbv2", "create-listener",
		"--load-balancer-arn", loadBalancerArn,
		"--port", "443",
		"--protocol", "TCP",
		"--default-actions", "Type=forward,TargetGroupArn=" + targetGroupArn,
		"--tags", "Key=Vendor,Value=PingCAP",
	}
	_, err = a.output(args)
	if err != nil {
		return nil, err
	}

	var serviceId, serviceName string
	args = []string{
		"ec2", "create-vpc-endpoint-service-configuration",
		"--no-acceptance-required",
		"--network-load-balancer-arns", loadBalancerArn,
		"--tag-specifications", "ResourceType=vpc-endpoint-service,Tags=[{Key=Vendor,Value=PingCAP}]",
	}
	output, err = a.output(args)
	if err != nil {
		return nil, err
	}
	serviceId, err = parseJSON(string(output), ".ServiceConfiguration.ServiceId")
	if err != nil {
		return nil, err
	}
	serviceName, err = parseJSON(string(output), ".ServiceConfiguration.ServiceName")
	if err != nil {
		return nil, err
	}

	if serviceId == "" || serviceName == "" {
		args = []string{
			"ec2", "describe-vpc-endpoint-service-configurations",
		}
		output, err = a.output(args)
		if err != nil {
			return nil, err
		}
		serviceId, err = parseJSON(string(output), fmt.Sprintf(".ServiceConfigurations[]| select(.NetworkLoadBalancerArns[0]==\"%s\") | .ServiceId", loadBalancerArn))
		if err != nil {
			return nil, err
		}
		serviceName, err = parseJSON(string(output), fmt.Sprintf(".ServiceConfigurations[]| select(.NetworkLoadBalancerArns[0]==\"%s\") | .ServiceName", loadBalancerArn))
		if err != nil {
			return nil, err
		}
	}

	if serviceId == "" || serviceName == "" {
		return nil, fmt.Errorf("empty service ID or name: id = '%v', name = '%v'", serviceId, serviceName)
	}
	created.VpcEndpointServiceId = serviceId
	args = []string{
		"ec2", "modify-vpc-endpoint-service-permissions",
		"--service-id", serviceId,
		"--add-allowed-principals", fmt.Sprintf("arn:aws:iam::%s:root", req.AccountID),
	}
	output, err = a.output(args)
	if err != nil {
		return nil, err
	}

	nativeAws := aws{}
	args = []string{
		"ec2", "create-vpc-endpoint",
		"--vpc-id", req.HostVpcID,
		"--service-name", serviceName,
		"--security-group-ids", req.HostSecurityGroupId,
		"--vpc-endpoint-type", "Interface",
	}
	output, err = nativeAws.output(args)
	if err != nil {
		return nil, err
	}
	vpcEndpointId, err := parseJSON(string(output), ".VpcEndpoint.VpcEndpointId")
	if err != nil {
		return nil, err
	}
	created.VpcEndpointId = vpcEndpointId

	fmt.Println("Sleep for 30 seconds until VPC endpoint is ready")
	time.Sleep(time.Second * 30)
	args = []string{
		"ec2", "modify-vpc-endpoint",
		"--vpc-endpoint-id", vpcEndpointId,
		"--add-subnet-ids", req.HostSubnet,
	}
	_, err = nativeAws.output(args)
	if err != nil {
		return nil, err
	}

	fmt.Println("Sleep for 60 seconds until VPC endpoint DNS record is updated")
	time.Sleep(time.Second * 30)
	args = []string{
		"ec2", "describe-vpc-endpoints",
		"--vpc-endpoint-ids", vpcEndpointId,
	}
	output, err = nativeAws.output(args)
	if err != nil {
		return nil, err
	}
	endpoints, err := parseJSONMulti(string(output), ".VpcEndpoints[0].DnsEntries[].DnsName")
	if err != nil {
		return nil, err
	}
	return endpoints, nil
}

type request struct {
	AccountID           string `yaml:"AccountID,omitempty"`
	HostSubnet          string `yaml:"HostSubnet,omitempty"`
	HostVpcID           string `yaml:"HostVpcID,omitempty"`
	HostSecurityGroupId string `yaml:"HostSecurityGroupId,omitempty"`

	AssumeRole string `yaml:"AssumeRole,omitempty"`
	ExternalID string `yaml:"ExternalID,omitempty"`
	Name       string `yaml:"Name,omitempty"`
	Kubeconfig string `yaml:"Kubeconfig,omitempty"`

	VpcID           string   `yaml:"VpcID,omitempty"`
	Region          string   `yaml:"Region,omitempty"`
	SecurityGroupID string   `yaml:"SecurityGroupID,omitempty"`
	SubnetIDs       []string `yaml:"SubnetIDs,omitempty"`

	EKSRole    string `yaml:"EKSRole,omitempty"`
	EKSNodeARN string `yaml:"EKSNodeARN,omitempty"`
	KeyARN     string `yaml:"KeyARN,omitempty"`
}

func loadRequest(path string) (request, error) {
	var req request
	content, err := ioutil.ReadFile(path)
	if err != nil {
		return req, err
	}
	if err = yaml.Unmarshal(content, &req); err != nil {
		return req, err
	}
	return req, nil
}

func loadClusterInfo(path string) (info clusterInfo, err error) {
	defer func() {
		if err == readFileErr {
			err = nil
		}
	}()
	content, err := ioutil.ReadFile(path)
	if err != nil {
		return info, readFileErr
	}
	if err = yaml.Unmarshal(content, &info); err != nil {
		return info, err
	}
	return info, nil
}

type clusterInfo struct {
	Name                 string `yaml:"Name,omitempty"`
	VpcEndpointId        string `yaml:"VpcEndpointId,omitempty"`
	VpcEndpointServiceId string `yaml:"VpcEndpointServiceId,omitempty"`
	TargetGroupArn       string `yaml:"TargetGroupArn,omitempty"`
	LoadBalancerArn      string `yaml:"LoadBalancerArn,omitempty"`
}

func deleteCluster(req request) {
	// Remove endpoint
	if created.VpcEndpointId != "" {
		fmt.Println(">>> Deleting EKS endpoint")
		nativeAws := aws{}
		args := []string{
			"ec2", "delete-vpc-endpoints", "--vpc-endpoint-ids", created.VpcEndpointId,
		}
		_, err := nativeAws.output(args)
		if err != nil {
			log.Fatal(err)
		}
	}

	// Remove endpoint service
	envs, err := getAssumeCredentialEnvs(req)
	if err != nil {
		log.Fatal(err)
	}
	a := aws{command{envs: envs}}
	if created.VpcEndpointServiceId != "" {
		fmt.Println(">>> Deleting EKS endpoint service")
		args := []string{
			"ec2", "delete-vpc-endpoint-service-configurations", "--service-ids", created.VpcEndpointServiceId,
		}
		_, err = a.output(args)
		if err != nil {
			log.Fatal(err)
		}
	}

	// Remove load balancer and EKS
	if created.Name != "" {
		fmt.Println(">>> Deleting EKS load balancer")
		args := []string{
			"elb", "delete-load-balancer", "--load-balancer-name", created.Name,
		}
		_, err = a.output(args)
		if err != nil {
			log.Fatal(err)
		}

		infractl := kubectl{kubeconfig: *kubeconfig}

		fmt.Println(">>> Deleting EKS nodegroups")
		args = []string{
			"delete", "nodegroups", created.Name + "-shared", "--wait=false",
		}
		if err := infractl.run(args); err != nil {
			log.Fatal(err)
		}

		args = []string{
			"delete", "nodegroups", created.Name + "-tidb", "--wait=false",
		}
		if err := infractl.run(args); err != nil {
			log.Fatal(err)
		}

		fmt.Println(">>> Deleting EKS cluster")
		args = []string{
			"delete", "clusters.eks.aws.crossplane.io", created.Name, "--wait=false",
		}
		if err := infractl.run(args); err != nil {
			log.Fatal(err)
		}
	}
}

type command struct {
	binary string
	envs   map[string]string
}

type cmdOpt struct {
	stdin string
}

func (c *command) combinedOutput(args []string, opt cmdOpt) ([]byte, error) {
	cmd := exec.Command(c.binary, args...)
	if cmd.Env == nil {
		cmd.Env = []string{}
	}
	for key, value := range c.envs {
		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
	}
	if _, ok := c.envs["PATH"]; !ok {
		cmd.Env = append(cmd.Env, fmt.Sprintf("PATH=%s", os.Getenv("PATH")))
	}

	if opt.stdin != "" {
		p, err := cmd.StdinPipe()
		if err == nil {
			io.WriteString(p, opt.stdin)
			p.Close()
		}
	}
	fmt.Println("Running: "+c.binary, args)

	output, err := cmd.CombinedOutput()

	printout := string(output)
	if len(printout) > 200 {
		printout = printout[:200] + "\n ..."
	}
	fmt.Println("Output: ", printout)
	if err != nil {
		return nil, nil
	}
	return output, err
}

type kubectl struct {
	command
	kubeconfig string
}

func (c *kubectl) run(args []string) error {
	c.binary = "kubectl"
	defaultargs := []string{"--kubeconfig", c.kubeconfig}
	defaultargs = append(defaultargs, args...)

	_, err := c.combinedOutput(defaultargs, cmdOpt{})
	return err
}

func (c *kubectl) output(args []string) ([]byte, error) {
	c.binary = "kubectl"
	defaultargs := []string{"--kubeconfig", c.kubeconfig}
	defaultargs = append(defaultargs, args...)

	return c.combinedOutput(defaultargs, cmdOpt{})
}

func (c *kubectl) apply(data string) error {
	c.binary = "kubectl"

	dir, err := ioutil.TempDir("/tmp", "kubectl")
	if err != nil {
		return err
	}
	f := path.Join(dir, "data")
	if err := ioutil.WriteFile(f, []byte(data), 0644); err != nil {
		return err
	}

	defaultargs := []string{"--kubeconfig", c.kubeconfig, "apply", "-f", f}
	_, err = c.combinedOutput(defaultargs, cmdOpt{})
	return err
}

func (c *kubectl) applyf(filename string) error {
	c.binary = "kubectl"
	defaultargs := []string{"--kubeconfig", c.kubeconfig, "apply", "-f", filename}
	_, err := c.combinedOutput(defaultargs, cmdOpt{})
	return err
}

type helm struct {
	command
	kubeconfig string
}

func (c *helm) run(args []string) error {
	c.binary = "helm"

	if c.kubeconfig != "" {
		if c.envs == nil {
			c.envs = map[string]string{}
		}
		c.envs["KUBECONFIG"] = c.kubeconfig
	}
	_, err := c.combinedOutput(args, cmdOpt{})
	return err
}

type aws struct {
	command
}

func (a *aws) run(args []string) error {
	a.binary = "aws"
	_, err := a.combinedOutput(args, cmdOpt{})
	return err
}

func (a *aws) output(args []string) ([]byte, error) {
	a.binary = "aws"
	return a.combinedOutput(args, cmdOpt{})
}

func parseJSON(input string, pattern string) (string, error) {
	args := []string{pattern}
	c := command{binary: "jq"}
	output, err := c.combinedOutput(args, cmdOpt{stdin: input})
	s := strings.Trim(string(output), "\n")
	s = strings.Trim(s, "\"")
	return s, err
}

func parseJSONMulti(input string, pattern string) ([]string, error) {
	args := []string{pattern}
	c := command{binary: "jq"}
	output, err := c.combinedOutput(args, cmdOpt{stdin: input})
	if err != nil {
		return nil, err
	}
	lines := strings.Split(string(output), "\n")
	var result []string
	for _, line := range lines {
		line = strings.Trim(line, "\"")
		if line != "" {
			result = append(result, line)
		}
	}
	return result, nil
}

func GetAvailableHost(hosts []string) string {
	for _, host := range hosts {
		cmd := exec.Command("nslookup", host)
		if err := cmd.Run(); err == nil {
			return host
		}
	}
	return ""
}
