package proto

import (
	"encoding/json"
	"reflect"
	"strings"

	"github.com/pingcap-inc/checker/pkg/util"

	"github.com/Masterminds/semver"
	tidbCfg "github.com/pingcap/tidb/config"
	tidbLogUtil "github.com/pingcap/tidb/util/logutil"
	pdCfg "github.com/tikv/pd/server/config"
)

const (
	PdComponentName      = "PdConfig"
	TidbComponentName    = "TidbConfig"
	TikvComponentName    = "TikvConfig"
	TiflashComponentName = "TiflashConfig"
)

type SourceData struct {
	ClusterId   string
	ClusterName string
	TidbVersion string
	SampleData  Sample
	NodesData   map[string]NodeData
}

type SourceDataV2 struct {
	ClusterId   string
	ClusterName string
	TidbVersion string
	SampleData  Sample
	NodesData   map[string][]Config // {"component": {config, config, config, nil}}
}
type OutputData struct {
	ClusterId   string
	ClusterName string
	TidbVersion string
	ActionId    string
	SampleData  Sample
	NodesData   []RuleResult // rule name
}

type RuleResult struct {
	RuleName      string
	RuleId        int64
	Variation     string
	AlertingRule  string
	DeployResults []DeployResult // todo init
	Suggestion    string
}

type DeployResult struct {
	Id    string `header:"node"`  // component_ip:port
	Value string `header:"value"` // name:val,name:val
	Res   string `header:"res"`   // warning ok info nodata
}

type Sample struct {
	SampleId      string
	SampleContent []string // e.g. {"Pd", "TiDB"....}
}

type NodeData struct {
	Id         string
	Timestamp  string
	Configs    []Config
	DeviceData DeviceData
}
type Config interface {
	GetComponent() string
	GetPort() int
	GetHost() string
	CheckNil() bool
	// GetValueByTagPath is used in gengine
	GetValueByTagPath(tagPath string) reflect.Value
}

type DeprecatedTidbConfig struct {
	EnableStreaming bool `toml:"enable-streaming" json:"enable-streaming"`
}

type TidbConfigData struct {
	*tidbCfg.Config     // embedded field
	Port            int // TODO move to meta
	Host            string
}

// UnmarshalJSON set deprecated field for tidb config, a workaround solution.
func (cfg *TidbConfigData) UnmarshalJSON(data []byte) (err error) {
	deprecatedData := &DeprecatedTidbConfig{}
	if err := json.Unmarshal(data, deprecatedData); err != nil {
		return err
	}
	if err := json.Unmarshal(data, cfg.Config); err != nil {
		return err
	}
	cfg.EnableStreaming = deprecatedData.EnableStreaming
	return nil
}

func (cfg *TidbConfigData) GetComponent() string {
	return TidbComponentName
}

func (cfg *TidbConfigData) GetValueByTagPath(tagPath string) reflect.Value {
	tags := strings.Split(tagPath, ".")
	if len(tags) == 0 {
		return reflect.ValueOf(cfg.Config)
	}
	value := util.VisitByTagPath(reflect.ValueOf(cfg.Config), tags, 0)
	return value
}

func (cfg *TidbConfigData) GetPort() int {
	return cfg.Port
}

func (cfg *TidbConfigData) GetHost() string {
	return cfg.Host
}

func (cfg *TidbConfigData) CheckNil() bool {
	return cfg.Config == nil
}

func NewTidbConfigData() *TidbConfigData {
	return &TidbConfigData{Config: &tidbCfg.Config{
		Log: tidbCfg.Log{
			Level:               "info",
			Format:              "text",
			File:                tidbLogUtil.NewFileLogConfig(tidbLogUtil.DefaultLogMaxSize),
			SlowQueryFile:       "tidb-slow.log",
			SlowThreshold:       tidbLogUtil.DefaultSlowThreshold,
			ExpensiveThreshold:  10000,
			QueryLogMaxLen:      tidbLogUtil.DefaultQueryLogMaxLen,
			RecordPlanInSlowLog: tidbLogUtil.DefaultRecordPlanInSlowLog,
			EnableSlowLog:       tidbLogUtil.DefaultTiDBEnableSlowLog,
		},
	}}
}

type PerformanceData struct{}

type PdConfigData struct {
	*pdCfg.Config
	Port int // TODO move to meta
	Host string
}

func (cfg *PdConfigData) GetPort() int {
	return cfg.Port
}

func (cfg *PdConfigData) GetHost() string {
	return cfg.Host
}

func (cfg *PdConfigData) GetComponent() string {
	return PdComponentName
}

func (cfg *PdConfigData) CheckNil() bool {
	return cfg.Config == nil
}

func (cfg *PdConfigData) GetValueByTagPath(tagPath string) reflect.Value {
	tags := strings.Split(tagPath, ".")
	if len(tags) <= 1 {
		return reflect.ValueOf(cfg)
	}
	value := util.VisitByTagPath(reflect.ValueOf(cfg.Config), tags, 0)
	return value
}

func NewPdConfigData() *PdConfigData {
	return &PdConfigData{Config: pdCfg.NewConfig()}
}

type DeviceData struct{}

type Rule struct {
	// version
	Id           int64  `yaml:"id" toml:"id"`
	Name         string `yaml:"name" toml:"name"`
	Description  string `yaml:"description" toml:"description"`
	ExecuteRule  string `yaml:"execute_rule" toml:"execute_rule"`
	NameStruct   string `yaml:"name_struct" toml:"name_struct"` // -> "TidbConfigData" or "PdConfigData" or "TikvConfigData"
	ExpectRes    string `yaml:"expect_res" toml:"expect_res"`
	WarnLevel    string `yaml:"warn_level" toml:"warn_level"`
	Variation    string `yaml:"variation" toml:"variation"` // e.g. tidb.file.max_days,
	AlertingRule string `yaml:"alerting_rule" toml:"alerting_rule"`
	Suggestion   string `yaml:"suggestion" toml:"suggestion"`
}

type RuleSet map[string][]*Rule // e.g {"TidbConfigData": {&Rule{}, &Rule{}}}
type Result struct {
	Name        string `header:"rule"`
	CheckState  bool   `header:"pass"`
	WarnLevel   string `header:"warn-level"`
	Component   string `header:"component"`
	Node        string `header:"node"`
	ExpectValue string `header:"expect-value"`
	Port        int    `header:"port"`
}

type VersionRange string

func (vr VersionRange) Contain(target string) (bool, error) {
	if len(vr) == 0 {
		return true, nil
	}
	verCheck, err := semver.NewConstraint(string(vr))
	if err != nil {
		return false, err
	}
	ver, err := semver.NewVersion(target)
	if err != nil {
		return false, err
	}
	return verCheck.Check(ver), nil
}
