package resource

import (
	"bufio"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"regexp"
	"strings"

	"gopkg.in/yaml.v3"

	"github.com/pkg/errors"

	comM "github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/config"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/model"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/util"
)

// Resource is a rich domain model
type Resource interface {
	IsExist() (bool, error)
	Create(dryrun bool) (interface{}, error)
	Update(dryrun bool) (interface{}, error)
	Delete() error
	SyncID() error
	Name() string
}

func DocToResource(serverConfig config.ApiServerConfig, doc model.Resource) (Resource, error) {
	switch doc.Kind {
	case "Case":
		return NewTestCase(serverConfig, doc)
	case "Plan":
		util.LogWarningf("`tcctl apply` the plan has been deprecated, please use the `tcctl run` command")
		return NewTestPlan(serverConfig, doc)
	default:
		panic("unknown kind")
	}
}

func ResourcesFromYAMLStream(config config.ApiServerConfig, reader io.Reader) ([]Resource, error) {
	var rs = make([]Resource, 0)
	de := yaml.NewDecoder(reader)
	for {
		doc := model.Resource{}
		err := de.Decode(&doc)
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return nil, err
		}
		r, err := DocToResource(config, doc)
		if err != nil {
			return nil, err
		}
		rs = append(rs, r)
	}
	for _, each := range rs {
		err := each.SyncID()
		if err != nil {
			return nil, err
		}
	}
	return rs, nil
}

var keyDict = map[string]string{
	"":                                  "_ingored_empty_key",
	"Name":                              "name",
	"Feature":                           "features",
	"Summary":                           "summary",
	"Preconditions":                     "preconditions",
	"Steps":                             "steps",
	"Expected Result":                   "expectedResult",
	"Case Type":                         "category",
	"Designer":                          "designer",
	"Automation":                        "automation",
	"Priority":                          "priority",
	"Supported Versions":                "supportedVersions",
	"Tags":                              "tags",
	"Components":                        "components",
	"parameters":                        "parameters",
	"framework.generic.command":         "framework.generic.command",
	"framework.generic.workingDir":      "framework.generic.workingDir",
	"framework.generic.env":             "framework.generic.env",
	"framework.generic.image":           "framework.generic.image",
	"framework.generic.imagePullPolicy": "framework.generic.imagePullPolicy",
}

var keyValueCategory = map[string]string{
	"Functionality Test":   string(comM.CaseCategoryFunctionality),
	"Performance Test":     string(comM.CaseCategoryPerformance),
	"Durability Test":      string(comM.CaseCategoryDurability),
	"Reliability Test":     string(comM.CaseCategoryReliability),
	"Compatibility Test":   string(comM.CaseCategoryCompatibility),
	"Maintainability Test": string(comM.CaseCategoryMaintainability),
	"Security Test":        string(comM.CaseCategorySecurity),
	"Scene Test":           string(comM.CaseCategoryScene),
}

var keyValueAutomation = map[string]string{
	"Automated":          string(comM.CaseAutomationStatusAutomated),
	"Not Automated":      string(comM.CaseAutomationStatusNotAutomated),
	"Unable to Automate": string(comM.CaseAutomationStatusUnable),
	"In Progress":        string(comM.CaseAutomationStatusInProgress),
}

func substitudeFieldsKey(fields []string) error {
	for k := range fields {
		tmp, ok := keyDict[fields[k]]
		if !ok {
			return fmt.Errorf("field \"%s\" doesn't supported", fields[k])
		}
		fields[k] = tmp
	}
	return nil
}

func substitudeFieldsValue(val *string, field string) error {
	switch field {
	// FUNCTIONALITY | PERFORMANCE | DURABILITY | RELIABILITY | COMPATIBILITY | MAINTAINABILITY | SECURITY | SCENE
	case "category":
		tmp, ok := keyValueCategory[*val]
		if !ok {
			return fmt.Errorf("value \"%s\" under field \"%s\" is not supported", *val, field)
		}
		*val = tmp
		// AUTOMATED | NOT_AUTOMATED | UNABLE | IN_PROGRESS
	case "automation":
		tmp, ok := keyValueAutomation[*val]
		if !ok {
			return fmt.Errorf("value \"%s\" under field \"%s\" is not supported", *val, field)
		}
		*val = tmp
	}

	return nil
}

func ResourcesFromCSVStream(config config.ApiServerConfig, reader io.Reader) ([]Resource, error) {
	var rs = make([]Resource, 0)
	csvReader := csv.NewReader(bufio.NewReader(reader))
	csvReader.Comma = ','
	csvReader.LazyQuotes = true

	doc := model.Resource{}
	doc.ApiVersion = "api/v1"
	doc.Kind = "Case"

	if res, err := csvReader.ReadAll(); err == nil {
		err = substitudeFieldsKey(res[0])
		if err != nil {
			return nil, err
		}
		for l, item := range res[1:] {
			itemMap := make(map[string]interface{})
			for k, v := range res[0] {
				err = substitudeFieldsValue(&item[k], v)
				if err != nil {
					return nil, errors.Wrap(err, fmt.Sprintf("found invalid value in csv file in line %d, colume %d", l+1, k))
				}
				itemMap[v] = item[k]
			}
			doc.RawContent = itemMap
			if err := formatCaseDoc(&doc); err != nil {
				return nil, errors.Wrap(err, fmt.Sprintf("found invalid value in csv file in line %d ", l+1))
			}
			r, err := DocToResource(config, doc)
			if err != nil {
				return nil, errors.Wrap(err, fmt.Sprintf("found invalid value in csv file in line %d ", l+1))
			}
			rs = append(rs, r)
		}
	} else {
		return nil, err
	}

	for _, each := range rs {
		err := each.SyncID()
		if err != nil {
			return nil, err
		}
	}
	return rs, nil
}

func stringToArray(str string) []string {
	re := regexp.MustCompile(",")
	split := re.Split(str, -1)
	return split
}

// Due to csv doesn't support embedded arrays, so for those mutiple values we need to change string into []string
func formatCaseDoc(doc *model.Resource) error {
	for _, key := range []string{"features", "components", "tags"} {
		if v, existed := doc.RawContent[key]; existed && reflect.TypeOf(v).Name() == "string" {
			doc.RawContent[key] = stringToArray(v.(string))
		}
	}

	// SupportedVersions can be both string and array: `['>v5.0.1,<v5.2', `nightly`]` or just `>v5.0.1,<v5.2`.
	supportedVersions := []string{}
	k := "supportedVersions"
	if v, existed := doc.RawContent[k]; existed && reflect.TypeOf(v).Name() == "string" {
		s := strings.ReplaceAll(v.(string), `'`, `"`)

		if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
			if err := json.Unmarshal([]byte(s), &supportedVersions); err != nil {
				return errors.Wrap(err, fmt.Sprintf("current field \"%s\" ", k))
			}
		} else if s != "" {
			supportedVersions = []string{s}
		}
		doc.RawContent[k] = supportedVersions
	}

	k = "parameters"
	parameters := make(map[string]interface{})
	if v, existed := doc.RawContent[k]; existed && reflect.TypeOf(v).Name() == "string" {
		if err := yaml.Unmarshal([]byte(doc.RawContent[k].(string)), parameters); err != nil {
			return errors.Wrap(err, fmt.Sprintf("current field \"%s\" ", k))
		}
		doc.RawContent[k] = parameters["parameters"]
	}

	framework := make(map[string]map[string]interface{})
	framework["generic"] = make(map[string]interface{})
	for _, key := range []string{"framework.generic.command", "framework.generic.workingDir", "framework.generic.env", "framework.generic.image", "framework.generic.imagePullPolicy"} {
		if v, existed := doc.RawContent[key]; existed && reflect.TypeOf(v).Name() == "string" {
			switch key {
			case "framework.generic.command":
				framework["generic"]["command"] = []string{"/bin/sh", "-c", doc.RawContent[key].(string)}
			case "framework.generic.workingDir":
				framework["generic"]["workingDir"] = doc.RawContent[key]
			case "framework.generic.env":
				envs := make(map[string]string)
				if doc.RawContent[key].(string) == "" {
				} else if err := json.Unmarshal([]byte(doc.RawContent[key].(string)), &envs); err != nil {
					return errors.Wrap(err, fmt.Sprintf("current field \"%s\" ", key))
				}
				framework["generic"]["env"] = envs
			case "framework.generic.image":
				framework["generic"]["image"] = doc.RawContent[key]
			case "framework.generic.imagePullPolicy":
				framework["generic"]["imagePullPolicy"] = doc.RawContent[key]
			}
			delete(doc.RawContent, key)
			doc.RawContent["framework"] = framework
		}
	}

	return nil
}
