package types

import (
	"reflect"
	"time"

	"github.com/juju/errors"
	"github.com/pingcap/schrodinger/util"
)

// BoxState is box state
type BoxState string

// CatStage
const (
	BoxStateOnline  BoxState = "ONLINE"
	BoxStateOffline          = "OFFLINE"
	BoxStateDeleted          = "DELETED"
)

// BoxType is box type, it has
type BoxType string

// BoxType
const (
	BoxTypeBench   BoxType = "bench"
	BoxTypeGeneral         = "general"
)

// BoxTypes is box types
var BoxTypes = []BoxType{BoxTypeGeneral, BoxTypeBench}

// JobType is job type, which has cronjob, git webhook.etc
type JobType string

// JobType
const (
	JobCron        JobType = "CRONTAB"
	JobGitWebhook          = "GIT"
	JobImmediately         = "IMMEDIATELY"
)

type stringSlice []string

// Box is box config
type Box struct {
	ID              int64       `json:"id"`
	Name            string      `json:"name"`
	Creator         string      `json:"creator"`
	Desc            string      `json:"description"`
	State           BoxState    `json:"status"`
	Ctime           string      `json:"create_time"`
	Utime           string      `json:"update_time"`
	Tags            stringSlice `json:"tags"`
	ExperimentCount int64       `json:"experiment_count"`

	// used to define what label needs to be updated.
	NodeLabelsForUpdate Labels `json:"update_node"`

	Cat *Cat `json:"cat"`
	// if `WithoutCat` is true, the experiments of the box just run the case.
	WithoutCat bool         `json:"without_cat"`
	Rules      Rules        `json:"rules"`
	Tests      *TestsConfig `json:"tests"`
	Misc       *MiscConfig  `json:"config"`
}

// SetNodeLabelsForUpdate sets the label for updating
func (box *Box) SetNodeLabelsForUpdate(old *Box) {
	updateAll := func() {
		box.NodeLabelsForUpdate["pd"] = util.NodeLabelKeyForPD(box.ID)
		box.NodeLabelsForUpdate["tidb"] = util.NodeLabelKeyForTiDB(box.ID)
		box.NodeLabelsForUpdate["tikv"] = util.NodeLabelKeyForTiKV(box.ID)
		box.NodeLabelsForUpdate["test"] = util.NodeLabelKeyForTest(box.ID)
	}

	box.NodeLabelsForUpdate = make(map[string]string)
	if old == nil {
		updateAll()
		return
	}

	if !reflect.DeepEqual(box.Cat.PDConfig.ResourceConfig, old.Cat.PDConfig.ResourceConfig) ||
		!reflect.DeepEqual(box.Cat.TiDBConfig.ResourceConfig, old.Cat.TiDBConfig.ResourceConfig) ||
		!reflect.DeepEqual(box.Cat.TiKVConfig.ResourceConfig, old.Cat.TiKVConfig.ResourceConfig) {
		updateAll()
		return
	}

	if box.Cat.LabelStr != old.Cat.LabelStr {
		updateAll()
		return
	}

	defer func() {
		if old.NodeLabelsForUpdate != nil {
			for key, val := range old.NodeLabelsForUpdate {
				box.NodeLabelsForUpdate[key] = val
			}
		}
	}()

	if box.Cat.PDConfig.Size != old.Cat.PDConfig.Size {
		box.NodeLabelsForUpdate["pd"] = util.NodeLabelKeyForPD(box.ID)
		box.NodeLabelsForUpdate["test"] = util.NodeLabelKeyForTest(box.ID)
	}

	if box.Cat.TiDBConfig.Size != old.Cat.TiDBConfig.Size {
		box.NodeLabelsForUpdate["tidb"] = util.NodeLabelKeyForTiDB(box.ID)
		box.NodeLabelsForUpdate["test"] = util.NodeLabelKeyForTest(box.ID)
	}

	if box.Cat.TiKVConfig.Size != old.Cat.TiKVConfig.Size {
		box.NodeLabelsForUpdate["tikv"] = util.NodeLabelKeyForTiKV(box.ID)
		box.NodeLabelsForUpdate["test"] = util.NodeLabelKeyForTest(box.ID)
	}

	return
}

// EmptyNodeLabelsForUpdate emptys the label for updating
func (box *Box) EmptyNodeLabelsForUpdate() {
	box.NodeLabelsForUpdate = make(map[string]string)
}

// Verify verifies box
func (box *Box) Verify() error {
	if box.Name == "" {
		return errors.BadRequestf("box name can't be empty")
	}

	if len(box.Name) > 32 {
		return errors.BadRequestf("box name length should not longer then 32 chars")
	}

	if err := verifyK8sName(box.Name); err != nil {
		return errors.Trace(err)
	}

	if box.WithoutCat {
		return nil
	}

	if box.Cat == nil {
		return errors.BadRequestf("cat can not be empty")
	}

	if !util.IsEmpty(box.Cat.SelectedCat) {
		if box.Rules.Contains(JobGitWebhook) {
			return errors.BadRequestf("using selected cat can not use github web hook")
		}
	}

	return errors.Trace(box.Cat.Verify())
}

// SetDefaultValue sets default value for box
func (box *Box) SetDefaultValue() {
	if !box.WithoutCat {
		box.Cat.SetDefaultConfig()
		box.Cat.Creator = box.Creator
	}

	if box.Misc == nil {
		box.Misc = &MiscConfig{
			DestroyTidbCluster: true,
		}
	}
	box.Misc.setDefaultValue()
}

// Clone clones box
func (box *Box) Clone() *Box {
	boxClone := &Box{}
	if err := util.Clone(boxClone, box); err != nil {
		return nil
	}
	return boxClone
}

// SetOnline sets box online
func (box *Box) SetOnline() {
	box.State = BoxStateOnline
}

// SetDeleted sets box deleted
func (box *Box) SetDeleted() {
	box.State = BoxStateDeleted
}

// SetOffline sets box offline
func (box *Box) SetOffline() {
	box.State = BoxStateOffline
}

// IsOnline returns true if box is online
func (box *Box) IsOnline() bool {
	return box.State == BoxStateOnline
}

// IsOffline returns true if box is offline
func (box *Box) IsOffline() bool {
	return box.State == BoxStateOffline
}

// IsDeleted returns true if box is deleted
func (box *Box) IsDeleted() bool {
	return box.State == BoxStateDeleted
}

// IncExprimentCount increase experiment count
func (box *Box) IncExprimentCount() {
	box.ExperimentCount++
}

// Cat is the target will be test
type Cat struct {
	LabelStr    string `json:"labels"`
	SelectedCat string `json:"selected_cat"`

	Name    string `json:"name"`
	Creator string `json:"creator"`

	PDConfig   ComponentConfig `json:"pd_config"`
	TiDBConfig ComponentConfig `json:"tidb_config"`
	TiKVConfig ComponentConfig `json:"tikv_config"`

	// if `Monitor` set to true, we will monitor the status of the cat,
	// and if the status of cat is abnormal, we will stop the experiment
	Monitor bool `json:"monitor"`
}

// SetDefaultConfig sets default resource config
func (c *Cat) SetDefaultConfig() {
	if !util.IsEmpty(c.SelectedCat) {
		return
	}

	if util.IsEmpty(string(c.PDConfig.LogLevel)) {
		c.PDConfig.LogLevel = LogLevelInfo
	}

	if util.IsEmpty(string(c.TiDBConfig.LogLevel)) {
		c.TiDBConfig.LogLevel = LogLevelInfo
	}

	if util.IsEmpty(string(c.TiKVConfig.LogLevel)) {
		c.TiKVConfig.LogLevel = LogLevelInfo
	}
	if c.PDConfig.Size == 0 {
		c.PDConfig.Size = 1
	}

	c.TiKVConfig.Version.Name = "tikv"
	c.PDConfig.Version.Name = "pd"
	c.TiDBConfig.Version.Name = "tidb"

	// set default resource config
	c.TiKVConfig.ResourceConfig.SetDefaultValue(tikvDefaultConfig)

	c.TiDBConfig.ResourceConfig.SetDefaultValue(tidbDefaultConfig)

	c.PDConfig.ResourceConfig.SetDefaultValue(pdDefaultConfig)
}

// Verify verifys cat
func (c *Cat) Verify() error {
	if !util.IsEmpty(c.SelectedCat) {
		return nil
	}

	if !util.IsEmpty(c.Name) {
		err := verifyK8sName(c.Name)
		if err != nil {
			return errors.Trace(err)
		}
	}

	if !c.PDConfig.isValid() || !c.TiDBConfig.isValid() || !c.TiKVConfig.isValid() {
		return errors.BadRequestf("tidb/tikv/pd config is not valid")
	}
	return nil
}

// IsNameExist checks name if exists
func (c *Cat) IsNameExist() bool {
	if util.IsEmpty(c.Name) {
		return false
	}
	return true
}

// ComponentConfig defines the config for each tidb component
type ComponentConfig struct {
	// defines the number of instances to run
	Size int32 `json:"size"`

	// version to run
	Version VersionInfo `json:"version"`

	ConfigTemplateName string `json:"config_template_name"`

	// extend to config
	LogLevel LogLevel `json:"log_level"`

	ResourceConfig ResourceRequirements `json:"resource_config"`

	Fuse bool `json:"fuse"`

	// args defines some special args for starting component,
	// eg: tidb-server --store=mocktikv
	Args string `json:"args"`
}

func (c *ComponentConfig) isValid() bool {
	if !c.Version.isValid() {
		return false
	}

	if c.LogLevel != LogLevelInfo && c.LogLevel != LogLevelWarn &&
		c.LogLevel != LogLevelDebug && c.LogLevel != LogLevelError &&
		c.LogLevel != LogLevelFatal {
		return false
	}

	return true
}

// Rule is rule to trigger build box
type Rule struct {
	Type  JobType `json:"type"`
	Value string  `json:"value"`
}

// Rules is a set of rule
type Rules []Rule

// Contains checks if rules contains specify type of job
func (r Rules) Contains(jobType JobType) bool {
	for _, r := range r {
		if r.Type == jobType {
			return true
		}
	}
	return false
}

// TestConfig is single test config
type TestConfig struct {
	TestName       string  `json:"test_name"`
	TestTemplateID int64   `json:"test_template_id"`
	OverrideArgs   *string `json:"override_args"`
}

// TestConfigs are collection of test configs
type TestConfigs []TestConfig

// TestsConfig is box's tests config
type TestsConfig struct {
	InOrder  bool        `json:"in_order"`
	Tests    TestConfigs `json:"tests"`
	LabelStr string      `json:"labels"`
}

// MiscConfig is misc config for box
type MiscConfig struct {
	SlackChannelForError   string        `json:"slack_channel_for_error"`
	SlackChannelForSuccess string        `json:"slack_channel_for_success"`
	DestroyTidbCluster     bool          `json:"destroy_tidb_cluster"`
	Timeout                time.Duration `json:"timeout"`
	Type                   BoxType       `json:"type"`
}

func (m *MiscConfig) setDefaultValue() {
	if util.IsEmpty(m.SlackChannelForError) {
		m.SlackChannelForError = "#tid-b"
	}
	if m.Timeout == 0 {
		m.Timeout = 168 * time.Hour
	}
}

// Boxes is an array of Box
type Boxes []*Box

// implement sort.Interface
func (boxes Boxes) Len() int           { return len(boxes) }
func (boxes Boxes) Swap(i, j int)      { boxes[i], boxes[j] = boxes[j], boxes[i] }
func (boxes Boxes) Less(i, j int) bool { return boxes[i].Ctime > boxes[j].Ctime }
