package command

import (
	"fmt"
	"io/ioutil"
	"math"
	"strconv"
	"strings"

	"github.com/juju/errors"
	"github.com/pingcap/schrodinger/pkg/types"
	"github.com/spf13/cobra"
	"gopkg.in/yaml.v2"
)

var (
	testFile  string
	binaryURL string
	tag       string

	testFormat     string
	testDataDir    string
	testFilePrefix string
)

const (
	tagLatest           = "latest"
	tagPrPrefix         = "pr/"
	schrodingerTestRepo = "git@github.com:pingcap/schrodinger-test.git"
)

// NewTestCommand returns the cobra command for "test".
func NewTestCommand() *cobra.Command {
	testCmd := &cobra.Command{
		Use:   "test <subcommand>",
		Short: "Test Template related commands",
	}

	testCmd.AddCommand(NewTestSetCommand())
	testCmd.AddCommand(NewTestRemoveCommand())
	testCmd.AddCommand(NewTestCheckCommand())
	testCmd.AddCommand(NewTestDumpCommand())
	testCmd.AddCommand(NewTestLoadCommand())

	return testCmd
}

// NewTestSetCommand returns the cobra command for "test set"
func NewTestSetCommand() *cobra.Command {
	tc := &cobra.Command{
		Use:   "set [options]",
		Short: "add or update a test to schrodinger",

		Run: testSetCommandFunc,
	}

	tc.Flags().StringVar(&testFile, "test-file", "./schrodinger.yaml", "comma separated description file for the new test.")
	tc.Flags().StringVar(&binaryURL, "binary-url", "", "the url of test binary")
	tc.Flags().StringVar(&tag, "tag", "", "the tag of test template")
	return tc
}

// NewTestRemoveCommand returns the cobra command for "test remove"
func NewTestRemoveCommand() *cobra.Command {
	tc := &cobra.Command{
		Use:   "remove <testname> <id>",
		Short: "remove a test template from schrodinger",

		Run: testRemoveCommandFunc,
	}

	return tc
}

// NewTestCheckCommand returns the cobra command for "test check",
// "test check" checks if the test-file if available
func NewTestCheckCommand() *cobra.Command {
	tc := &cobra.Command{
		Use:   "check [options]",
		Short: "check if the test-file is available",

		Run: testCheckCommandFunc,
	}

	tc.Flags().StringVar(&testFile, "test-file", "./schrodinger.yaml", "comma separated description file for the new test.")

	return tc
}

// NewTestDumpCommand returns the cobra command for "test dump",
// "test dump" dumps all test templates,
// file formats supported: json
func NewTestDumpCommand() *cobra.Command {
	tc := &cobra.Command{
		Use:   "dump [options]",
		Short: "dump all test templates, file formats supported: json",

		Run: testDumpCommandFunc,
	}

	tc.Flags().StringVar(&testFormat, "format", "json", "file format for data")
	tc.Flags().StringVar(&testDataDir, "dir", "./", "the storage directory of data")
	tc.Flags().StringVar(&testFilePrefix, "file-prefix", "testTemplates", "the prefix of dump files")

	return tc
}

// NewTestLoadCommand returns the cobra command for "test load",
// "test load" loads test templates,
// file formats supported: json
func NewTestLoadCommand() *cobra.Command {
	tc := &cobra.Command{
		Use:   "load [options]",
		Short: "load test templates from files, file format supported: json",

		Run: testLoadCommandFunc,
	}

	tc.Flags().StringVar(&testFormat, "format", "json", "file format for data")
	tc.Flags().StringVar(&testDataDir, "dir", "./", "the storage directory of data")

	return tc
}

func testSetCommandFunc(cmd *cobra.Command, args []string) {
	if len(args) > 0 {
		ev := "too many arguments"
		for _, s := range args {
			if strings.HasSuffix(strings.ToLower(s), ".yaml") ||
				strings.HasSuffix(strings.ToLower(s), ".yml") {
				ev += fmt.Sprintf(`, did you mean --test-file=%s`, s)
			}

			if strings.HasSuffix(strings.ToLower(s), "http") ||
				strings.HasSuffix(strings.ToLower(s), "ftp") {
				ev += fmt.Sprintf(`, did you mean --binary-url=%s`, s)
			}
		}

		ExitWithError(ExitBadArgs, errors.New(ev))
	}

	if len(testFile) == 0 {
		ExitWithError(ExitBadArgs, errors.New("test file not provided"))
	}

	if tag != tagLatest &&
		!strings.HasPrefix(strings.ToLower(tag), tagPrPrefix) {
		ExitWithError(ExitBadArgs, errors.New("illegal tag"))
	}

	testYamlData, err := ioutil.ReadFile(testFile)
	if err != nil {
		ExitWithError(ExitError, err)
	}

	newTestTemplate := &types.TestTemplate{}
	if err = yaml.Unmarshal(testYamlData, &newTestTemplate); err != nil {
		ExitWithError(ExitError, err)
	}

	if err = newTestTemplate.Verify2(); err != nil {
		ExitWithError(ExitError, err)
	}

	setTestTemplateSource(newTestTemplate, tag, binaryURL)

	if err := newTestTemplate.Verify(); err != nil {
		ExitWithError(ExitError, err)
	}

	newTestTemplate.Args = newTestTemplate.Environment.Args

	schCli := mustClientFromCmd(cmd)
	if _, err := schCli.SetTestTemplate(newTestTemplate); err != nil {
		ExitWithError(ExitError, err)
	}
}

func setTestTemplateSource(tpl *types.TestTemplate, tag, binaryURL string) {
	if len(binaryURL) != 0 {
		// compatible with older versions
		binaryName := tpl.Name
		if len(tpl.Source.BinaryName) != 0 {
			binaryName = tpl.Source.BinaryName
		}

		if tag == tagLatest {
			tpl.Source = types.TestSource{
				BinaryName: binaryName,
				Type:       types.Latest,
				URL:        binaryURL,
				Image:      tpl.Environment.Image,
			}
			return
		}

		if strings.HasPrefix(strings.ToLower(tag), tagPrPrefix) {
			tag := strings.ToLower(tag)
			prID := strings.Split(tag, tagPrPrefix)[1]
			if _, err := strconv.ParseInt(prID, 10, 64); err != nil {
				ExitWithError(ExitError, err)
			}

			tpl.Source = types.TestSource{
				BinaryName: binaryName,
				Type:       types.GitSource,
				URL:        binaryURL,
				Image:      tpl.Environment.Image,
				GitRepo:    schrodingerTestRepo,
				GitValue: types.GitValue{
					GitType: types.GitTypePr,
					Value:   prID,
				},
			}
			return
		}
		ExitWithError(ExitError, errors.Errorf("unreachable"))
	}
}

func testRemoveCommandFunc(cmd *cobra.Command, args []string) {
	if len(args) < 1 {
		ExitWithError(ExitBadArgs, errors.New("test template name is not provided"))
	}

	if len(args) < 2 {
		ExitWithError(ExitBadArgs, errors.New("test template id is not provided"))
	}

	name := args[0]
	id, err := strconv.ParseInt(args[1], 10, 64)

	if err != nil {
		ExitWithError(ExitError, errors.Errorf("illegal template id: %s", args[1]))
	}

	if err := mustClientFromCmd(cmd).DeleteTestTemplate(name, id); err != nil {
		ExitWithError(ExitError, err)
	}
}

func testCheckCommandFunc(cmd *cobra.Command, args []string) {
	if len(args) > 0 {
		ev := "too many arguments"
		for _, s := range args {
			if strings.HasSuffix(strings.ToLower(s), ".yaml") ||
				strings.HasSuffix(strings.ToLower(s), ".yml") {
				ev += fmt.Sprintf(`, did you mean --test-file=%s`, s)
			}
		}

		ExitWithError(ExitBadArgs, errors.New(ev))
	}

	if len(testFile) == 0 {
		ExitWithError(ExitBadArgs, errors.New("test file not provided"))
	}

	testYamlData, err := ioutil.ReadFile(testFile)
	if err != nil {
		ExitWithError(ExitError, err)
	}

	newTestTemplate := &types.TestTemplate{}
	if err = yaml.Unmarshal(testYamlData, &newTestTemplate); err != nil {
		ExitWithError(ExitError, err)
	}

	if err = newTestTemplate.Verify2(); err != nil {
		ExitWithError(ExitError, err)
	}

	NormalExit("test-file is OK")
}

func testDumpCommandFunc(cmd *cobra.Command, args []string) {
	checkDumpAndLoadArgs(args, testFormat, testDataDir, testFilePrefix)

	schCli := mustClientFromCmd(cmd)
	tests, err := schCli.ListTestTemplates(math.MaxInt32, 0)
	if err != nil {
		ExitWithError(ExitError, err)
	}

	saveDumpFile(tests, testDataDir, testFilePrefix, testFormat)
}

func testLoadCommandFunc(cmd *cobra.Command, args []string) {
	checkDumpAndLoadArgs(args, testFormat, testDataDir, testFilePrefix)

	schCli := mustClientFromCmd(cmd)

	tests, err := readTestTemplatesFromJSONFile(testDataDir, testFilePrefix)
	if err != nil {
		ExitWithError(ExitError, err)
	}

	for _, test := range tests {
		if len(test.Source.URL) == 0 {
			continue
		}

		if err := test.Verify(); err != nil {
			ExitWithError(ExitError, err)
		}

		if _, err = schCli.SetTestTemplate(test); err != nil {
			ExitWithError(ExitError, err)
		}
	}
}
