package cmd

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/url"
	"os"
	"path/filepath"
	"time"

	"github.com/fatih/color"
	"github.com/go-playground/validator/v10"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
	"sigs.k8s.io/yaml"

	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/rms/request"
	"github.com/pingcap/test-infra/common/model/rms/response"
	respM "github.com/pingcap/test-infra/common/model/tcms/response"
	testbedpkg "github.com/pingcap/test-infra/tools/tcctl/pkg/testbed"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/util"
)

var (
	interval          time.Duration
	timeout           time.Duration
	outSpec           string
	credentialPath    string
	testbedPlanExecID uint64
)

// applyCmd represents the apply command
var testbedCmd = &cobra.Command{
	Use:   "testbed",
	Short: "Controls the rms",
}

var createTestbedCmd = &cobra.Command{
	Use:          "create",
	Short:        "create subcommand create the test-bed",
	Example:      `tcctl testbed create -f ./testbed.yaml`,
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		var err error
		var gtr *response.GetTestbedResponse
		var testbedName string
		testbed := (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()

		defer func() {
			if err != nil && testbedName != "" {
				util.LogInfof("delete the testbed %s since an error occurred: %v", testbedName, err)

				if err := testbed.Delete(testbedName); err != nil {
					util.LogErrorf("delete the testbed %s error: %v", testbedName, err)
				}
			}
		}()
		isInteractive, _ := cmd.Flags().GetBool("interactive")

		var rawYAML []byte
		if isInteractive {
			myui := testbedpkg.NewTestbedUi()
			rawYAML = myui.Run()
		} else {
			reader, err := util.ReadFromFile(inputFile)
			if err != nil {
				util.LogErrorf("read the spec file error: %v", err)
				return err
			}
			rawYAML, err = ioutil.ReadAll(reader)
			if err != nil {
				return err
			}
		}
		request, err := createRequest(rawYAML)
		if err != nil {
			return err
		}
		resp, err := testbed.Create(request)
		if err != nil {
			util.LogErrorf("create a testbed error: %v", err)
			return err
		}
		testbedName = resp.Name
		// generate the credential after testbed created
		// this is useful when rms runs on aws and credential is ssh-config
		if credentialPath != "" {
			if err := testbed.GenerateCredential(testbedName, credentialPath); err != nil {
				util.LogErrorf("generate the credential error: %v", err)
			}
		}
		// fixes: https://github.com/pingcap/test-infra/issues/177
		if err = writeTestbedOutput(&response.GetTestbedResponse{
			Name: testbedName,
		}); err != nil {
			return errors.Wrap(err, "write testbed response error")
		}
		gtr, err = testbed.WaitForReady(resp.Name, interval, timeout)
		if err != nil {
			return err
		}
		proxies, err := testbed.GetProxies()
		if err != nil {
			return err
		}
		if err := writeTestbedResponseOutputAndEnvFiles(gtr, proxies); err != nil {
			return err
		}
		return nil
	},
}

func getTestbedByPlanExecID(planExecID model.ID) (*model.TestBedInfo, error) {
	testbed := &respM.TestBedInfoListResponse{}
	_, err := util.Req().
		Host(serverConfig.TcmsHost).
		Bearer(serverConfig.Token).
		Path("api/v1/testbeds/info").
		Param(map[string]string{
			"count":        "1",
			"plan_exec_id": planExecID.String(),
		}).
		Get(testbed)
	if err != nil {
		return nil, err
	}
	if len(testbed.Data) == 0 {
		return nil, fmt.Errorf("testbed for plan-exec #%d not found", planExecID)
	}
	return testbed.Data[0], nil
}

var getTestbedCmd = &cobra.Command{
	Use:          "get",
	Short:        "get subcommand get the testbed .env file in the current dir",
	Example:      `tcctl testbed get <testbed>`,
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		var testbedName string
		if len(args) == 0 {
			if testbedPlanExecID != 0 {
				testbed, err := getTestbedByPlanExecID(model.ID(testbedPlanExecID))
				if err != nil {
					return err
				}
				url, err := url.Parse(testbed.URL)
				if err != nil {
					return err
				}
				serverConfig.RmsHost = url.Scheme + "://" + url.Host
				testbedName = testbed.Name
			} else {
				ctr, err := readOutput()
				if err != nil {
					return err
				}
				testbedName = ctr.Name
			}
		} else {
			testbedName = args[0]
		}

		var testbedCli = (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()
		gtr, err := testbedCli.Get(testbedName)
		if err != nil {
			return err
		}

		proxies, err := testbedCli.GetProxies()
		if err != nil {
			return err
		}
		if err := writeTestbedResponseOutputAndEnvFiles(gtr, proxies); err != nil {
			return err
		}

		err = testbedCli.Auth(testbedName)
		if err != nil {
			return err
		}

		if len(credentialPath) == 0 {
			return nil
		}
		if _, err = os.Stat(credentialPath); os.IsNotExist(err) ||
			util.AskForConfirmation(fmt.Sprintf("File '%s' already exists, do you want to override it?", credentialPath), false) {
			if err = testbedCli.GenerateCredential(testbedName, credentialPath); err != nil {
				util.LogErrorf("generate the credential error: %v", err)
			}
		}
		return nil
	},
}

var (
	promTarballURL string
	clusterType    string
)

var restoreMonitorCmd = &cobra.Command{
	Use:          "restore-monitor",
	Short:        "restore-monitor restores tidb monitor from given testbed or prometheus backup",
	Example:      "tcctl testbed restore-monitor <testbed>",
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		testbedCli := (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()
		testbedName := ""

		if len(args) == 0 && len(promTarballURL) == 0 {
			return fmt.Errorf("<testbed> or <prom-tarball-url> should be provided")
		}
		if len(args) > 0 {
			testbedName = args[0]
		}
		resp, err := testbedCli.RestoreMonitor(testbedName, promTarballURL, clusterType)
		if err != nil {
			return err
		}

		_, err = testbedCli.WaitForReady(resp.TestbedName, interval, timeout)
		if err != nil {
			return err
		}

		util.LogInfof(color.GreenString("monitor restored in testbed %s", resp.TestbedName))
		util.LogInfof(color.GreenString("Dashboard in: %s, wait 1 minute till it works", resp.GrafanaURL))

		return nil
	},
}

var authTestbedCmd = &cobra.Command{
	Use:          "auth",
	Short:        "DEPRECATED! Please use 'tcctl testbed get <testbed>' (auth grant user debug privilege to testbed)",
	Example:      "tcctl testbed auth <testbed>",
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		util.LogWarningf("This subcommand has been DEPRECATED, please use 'tcctl testbed get <testbed>' instead")
		if len(args) == 0 {
			return fmt.Errorf("expect a testbed name")
		}
		testbedName := args[0]
		testbedCli := (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()
		err := testbedCli.Auth(testbedName)
		if err != nil {
			return err
		}

		if len(credentialPath) == 0 {
			return nil
		}
		if _, err = os.Stat(credentialPath); os.IsNotExist(err) {
			if err = testbedCli.GenerateCredential(testbedName, credentialPath); err != nil {
				util.LogErrorf("generate the credential error: %v", err)
			}
		}

		return nil
	},
}

var deleteTestbedCmd = &cobra.Command{
	Use:          "delete",
	Example:      `tcctl testbed delete -f output`,
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		var name string
		var testbedCli = (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()
		if len(args) == 0 {
			ctr, err := readOutput()
			if err != nil {
				return err
			}
			name = ctr.Name
		} else {
			name = args[0]
		}
		return testbedCli.Delete(name)
	},
}

var (
	completeType string
)

var completeTestbedCmd = &cobra.Command{
	Use:          "complete",
	Example:      `tcctl testbed complete testbed_xxx --type Success`,
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		testbedCli := (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()
		if len(args) == 0 {
			return fmt.Errorf("expect a testbed name")
		}
		return testbedCli.Complete(args[0], request.OnTestbedCompletionType(completeType))
	},
}

var renewLease time.Duration

var renewLeaseTestbedCmd = &cobra.Command{
	Use:          "renew",
	Example:      `tcctl testbed renew $testbed`,
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		testbedCli := (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()
		if len(args) == 0 {
			return fmt.Errorf("expect a testbed name")
		}
		return testbedCli.RenewalLease(args[0], renewLease.String())
	},
}

var listTestbedCmd = &cobra.Command{
	Use:          "list",
	Example:      `tcctl testbed list`,
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		testbedCli := (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()
		return testbedCli.ListTestbed()
	},
}

var logCmd = &cobra.Command{
	Use:          "log",
	Example:      "tcctl testbed log $testbed",
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		testbedCli := (&testbedpkg.Testbed{ServerConfig: serverConfig, ECRServer: ecrServer}).Init()
		if len(args) == 0 {
			return fmt.Errorf("expect a testbed name")
		}
		getAll, _ := cmd.Flags().GetBool("all")
		return testbedCli.GetLogFromMinio(args[0], getAll)
	},
}

func init() {
	testbedCmd.PersistentFlags().StringVar(&ecrServer, "ecr-server", ecrServerDefault, "the ecr server")
	createTestbedCmd.PersistentFlags().DurationVar(&interval, "wait-interval", 30*time.Second, "the wait interval")
	createTestbedCmd.PersistentFlags().DurationVar(&timeout, "wait-timeout", 30*time.Minute, "the wait timeout")
	createTestbedCmd.PersistentFlags().StringVar(&outSpec, "output-spec", `{"output":"output"}`, "the output spec")
	createTestbedCmd.PersistentFlags().StringVar(&credentialPath, "credential", "kubeconfig.yml", "the output credential path")
	createTestbedCmd.Flags().BoolP("interactive", "i", false, "interactive ui for testbed creation")
	completeTestbedCmd.PersistentFlags().StringVar(&completeType, "type", "", "the complete type: Success|Failure")
	_ = completeTestbedCmd.MarkPersistentFlagRequired("type")
	renewLeaseTestbedCmd.PersistentFlags().DurationVar(&renewLease, "lease", 24*time.Hour, "the renew lease")

	restoreMonitorCmd.PersistentFlags().StringVar(&promTarballURL, "prom-tarball-url", "", "prometheus tarball URL")
	restoreMonitorCmd.PersistentFlags().StringVar(&clusterType, "cluster-type", "k8s", "cluster type of prometheus backup: k8s|tiup")

	getTestbedCmd.Flags().Uint64VarP(&testbedPlanExecID, "plan-exec", "p", 0, "get testbed by plan execution id")

	logCmd.Flags().BoolP("all", "A", false, "download all logs from the testbed")

	testbedCmd.AddCommand(createTestbedCmd)
	testbedCmd.AddCommand(getTestbedCmd)
	testbedCmd.AddCommand(deleteTestbedCmd)
	testbedCmd.AddCommand(completeTestbedCmd)
	testbedCmd.AddCommand(renewLeaseTestbedCmd)
	testbedCmd.AddCommand(authTestbedCmd)
	testbedCmd.AddCommand(listTestbedCmd)
	testbedCmd.AddCommand(restoreMonitorCmd)
	testbedCmd.AddCommand(logCmd)
	rootCmd.AddCommand(testbedCmd)
}

func createRequest(rawYAML []byte) (*request.CreateTestbedRequest, error) {
	content, err := yaml.YAMLToJSON(rawYAML)
	if err != nil {
		return nil, err
	}
	ctr := new(request.CreateTestbedRequest)
	if err := json.Unmarshal(content, &ctr); err != nil {
		util.LogErrorf("unmarshal spec error: %v", err)
		return nil, err
	}
	return ctr, nil
}

func readOutput() (*response.GetTestbedResponse, error) {
	filename := "output"
	if inputFile != "" {
		filename = inputFile
	}
	reader, err := util.ReadFromFile(filename)
	if err != nil {
		return nil, err
	}
	content, err := ioutil.ReadAll(reader)
	if err != nil {
		return nil, err
	}
	testbed := &response.GetTestbedResponse{}
	if err := json.Unmarshal(content, testbed); err != nil {
		return nil, err
	}
	return testbed, nil
}

func writeTestbedResponseOutputAndEnvFiles(res *response.GetTestbedResponse, proxies map[string]string) error {
	if err := writeTestbedOutput(res); err != nil {
		return err
	}
	if err := writeTestbedItems(res); err != nil {
		return err
	}
	if err := buildEnvFile(res, proxies); err != nil {
		return err
	}
	return nil
}

func writeTestbedOutput(res *response.GetTestbedResponse) error {
	testbedOutputSpec := &TestbedOutputSpec{}
	if err := json.Unmarshal([]byte(outSpec), testbedOutputSpec); err != nil {
		return err
	}
	validate := validator.New()
	if err := validate.Struct(testbedOutputSpec); err != nil {
		return err
	}

	b, err := json.Marshal(res)
	if err != nil {
		return err
	}
	if err := createDirIfNotExist(testbedOutputSpec.Output); err != nil {
		return err
	}
	if err := ioutil.WriteFile(testbedOutputSpec.Output, b, 0644); err != nil {
		return err
	}
	return nil
}

func writeTestbedItems(res *response.GetTestbedResponse) error {
	testbedOutputSpec := &TestbedOutputSpec{}
	if err := json.Unmarshal([]byte(outSpec), testbedOutputSpec); err != nil {
		return err
	}
	validate := validator.New()
	if err := validate.Struct(testbedOutputSpec); err != nil {
		return err
	}
Loop:
	for _, specItem := range testbedOutputSpec.Items {
		for _, item := range res.Items {
			if specItem.Name == item.Name {
				c, err := json.Marshal(item)
				if err != nil {
					return err
				}
				if err := createDirIfNotExist(specItem.Path); err != nil {
					return err
				}
				if err := ioutil.WriteFile(specItem.Path, c, 0644); err != nil {
					return err
				}
				continue Loop
			}
		}
		return fmt.Errorf("unknown resource name: %s", specItem.Name)
	}
	return nil
}

func buildEnvFile(resp *response.GetTestbedResponse, proxies map[string]string) error {
	prefix := "TI_PARAM_RES_"
	content := `# This file was generated by tcctl testbed create/get
# GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
`

	for _, item := range resp.Items {
		key := prefix + item.Name
		value, err := json.Marshal(item)
		if err != nil {
			return err
		}
		line := key + ": " + string(value) + "\n"
		content += line
	}

	for name, value := range proxies {
		line := name + ": " + value + "\n"
		content += line
	}
	if err := ioutil.WriteFile(".env", []byte(content), 0644); err != nil {
		return err
	}
	util.LogInfof("'.env' updated")
	return nil
}

func createDirIfNotExist(path string) error {
	dir := filepath.Dir(path)
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		return os.MkdirAll(dir, 0755)
	}
	return nil
}

type TestbedOutputSpecItem struct {
	Name string `json:"name"`
	Path string `json:"path"`
}

type TestbedOutputSpec struct {
	Output string                  `json:"output" validate:"required"`
	Items  []TestbedOutputSpecItem `json:"items"`
}
