package chaos

import (
	"bytes"
	"fmt"
	"math"
	"math/rand"
	"strconv"
	"strings"

	"github.com/BurntSushi/toml"
	"github.com/imdario/mergo"
	"github.com/pingcap/test-infra/sdk/resource"

	"github.com/pingcap/endless/pkg/client/tidb"
	"github.com/pingcap/endless/pkg/util/schedule"
	"github.com/pingcap/endless/testcase/tiflash/pkg"
)

// new value = original value * factor. factor is [0.1,2] and the increase step is 0.1.
const (
	maxFactor = 2
	minFactor = 0.1
	step      = 0.1
)

const tiFlashConfigPath = "/data0/config.toml"

var _ schedule.DoUndo = &HotReloadTiFlashConfig{}

// PdScheduler reload tiflash nodes without restarting them
type HotReloadTiFlashConfig struct {
	tc            resource.TiDBCluster
	defaultConfig map[string]*attribute
	tidbCli       *tidb.Client
}

type attrTp int

const (
	tpFloat attrTp = iota
	tpInt
	tpString
)

type attribute struct {
	name       string
	nameInToml string
	value      interface{}
	tp         attrTp
}

func (a *attribute) initValueFromStr(str string) error {
	switch a.tp {
	case tpFloat:
		value, err := strconv.ParseFloat(str, 64)
		if err != nil {
			return err
		}
		a.value = value
	case tpInt:
		value, err := strconv.Atoi(str)
		if err != nil {
			return err
		}
		a.value = value
	case tpString:
		a.value = str
	default:
		return fmt.Errorf("%d data type not found", a.tp)
	}
	return nil
}

func (a *attribute) getRandomValue() interface{} {
	f := float64(1+rand.Intn(maxFactor/minFactor)) * step
	switch a.tp {
	case tpFloat:
		return f * a.value.(float64)
	case tpInt:
		return int(math.Round(float64(a.value.(int)) * f))
	}
	return a.value
}

func (a *attribute) getFormatValue(v interface{}) string {
	switch a.tp {
	case tpFloat:
		return fmt.Sprintf("%f", v)
	case tpInt:
		return fmt.Sprintf("%d", v)
	}
	return fmt.Sprintf(`"%v"`, v)
}

func initDefaultTiFlashDtConfig(tc resource.TiDBCluster) (map[string]*attribute, error) {
	// the config can be updated
	// https://docs.google.com/spreadsheets/d/1Y_t-4Qbrf17m_TlJPauNjk8ZzOf28akcBqH_OhHJBFM/edit?n=Untitled_spreadsheet#gid=0
	var defaultConfig = map[string]*attribute{
		"dt_segment_limit_rows":                {"dt_segment_limit_rows", "profiles.default.dt_segment_limit_rows", nil, tpInt},
		"dt_segment_limit_size":                {"dt_segment_limit_size", "profiles.default.dt_segment_limit_size", nil, tpInt},
		"dt_segment_delta_limit_rows":          {"dt_segment_delta_limit_rows", "profiles.default.dt_segment_delta_limit_rows", nil, tpInt},
		"dt_segment_delta_limit_size":          {"dt_segment_delta_limit_size", "profiles.default.dt_segment_delta_limit_size", nil, tpInt},
		"dt_segment_force_merge_delta_deletes": {"dt_segment_force_merge_delta_deletes", "profiles.default.dt_segment_force_merge_delta_deletes", nil, tpInt},
		"dt_segment_force_merge_delta_rows":    {"dt_segment_force_merge_delta_rows", "profiles.default.dt_segment_force_merge_delta_rows", nil, tpInt},
		"dt_segment_force_merge_delta_size":    {"dt_segment_force_merge_delta_size", "profiles.default.dt_segment_force_merge_delta_size", nil, tpInt},
		"dt_segment_stop_write_delta_rows":     {"dt_segment_stop_write_delta_rows", "profiles.default.dt_segment_stop_write_delta_rows", nil, tpInt},
		"dt_segment_stop_write_delta_size":     {"dt_segment_stop_write_delta_size", "profiles.default.dt_segment_stop_write_delta_size", nil, tpInt},
		"dt_segment_delta_cache_limit_rows":    {"dt_segment_delta_cache_limit_rows", "profiles.default.dt_segment_delta_cache_limit_rows", nil, tpInt},
		"dt_segment_delta_cache_limit_size":    {"dt_segment_delta_cache_limit_size", "profiles.default.dt_segment_delta_cache_limit_size", nil, tpInt},
		"dt_segment_delta_small_pack_rows":     {"dt_segment_delta_small_pack_rows", "profiles.default.dt_segment_delta_small_pack_rows", nil, tpInt},
		"dt_segment_delta_small_pack_size":     {"dt_segment_delta_small_pack_size", "profiles.default.dt_segment_delta_small_pack_size", nil, tpInt},
		"dt_segment_stable_pack_rows":          {"dt_segment_stable_pack_rows", "profiles.default.dt_segment_stable_pack_rows", nil, tpInt},
		"dt_segment_wait_duration_factor":      {"dt_segment_wait_duration_factor", "profiles.default.dt_segment_wait_duration_factor", nil, tpFloat},
	}
	cmd := `/tiflash/tiflash client -h 127.0.0.1 -q "select * from settings where name like 'dt_%'"`
	stdout, err := pkg.TiFlashExecCmd(tc, cmd, 0)
	if err != nil {
		return nil, err
	}
	for _, v := range strings.Split(stdout, "\n") {
		attributes := strings.Split(v, "\t")
		name := attributes[0]
		value := attributes[1]
		if _, ok := defaultConfig[name]; ok {
			if err := defaultConfig[name].initValueFromStr(value); err != nil {
				return nil, err
			}
		}
	}
	return defaultConfig, nil
}

func NewHotReloadTiFlashConfig(tc resource.TiDBCluster) (*HotReloadTiFlashConfig, error) {
	conn, err := tc.GetDB("")
	if err != nil {
		return nil, err
	}
	defaultConfig, err := initDefaultTiFlashDtConfig(tc)
	if err != nil {
		return nil, err
	}
	return &HotReloadTiFlashConfig{
		defaultConfig: defaultConfig,
		tc:            tc,
		tidbCli:       tidb.NewClient(conn),
	}, nil
}

func (t *HotReloadTiFlashConfig) getTomlStrToChange() (string, error) {
	res := ""
	cmd := fmt.Sprintf("cat %s", tiFlashConfigPath)
	stdout, err := pkg.TiFlashExecCmd(t.tc, cmd, 0)
	if err != nil {
		return res, err
	}
	var config = make(map[string]interface{})
	if err := toml.Unmarshal([]byte(stdout), &config); err != nil {
		return res, err
	}
	var strb strings.Builder
	for _, v := range t.defaultConfig {
		valueToChange := v.getRandomValue()
		if valueToChange == nil {
			continue
		}
		strb.WriteString(fmt.Sprintf("%s = %s\n", v.nameInToml, v.getFormatValue(valueToChange)))
	}
	newConfig := make(map[string]interface{})
	if err := toml.Unmarshal([]byte(strb.String()), &newConfig); err != nil {
		return res, err
	}
	if err := mergo.Merge(&config, newConfig); err != nil {
		return res, err
	}
	buff := new(bytes.Buffer)
	encoder := toml.NewEncoder(buff)
	if err := encoder.Encode(config); err != nil {
		return res, err
	}

	return buff.String(), nil

}

func (t *HotReloadTiFlashConfig) Do() error {
	config, err := t.getTomlStrToChange()
	if err != nil {
		return err
	}
	cmd := fmt.Sprintf(`printf '%s' > %s`, config, tiFlashConfigPath)
	num, err := t.tidbCli.GetInstanceNum("tiflash")
	if err != nil {
		return err
	}
	for i := 0; i < int(num); i++ {
		if _, err := pkg.TiFlashExecCmd(t.tc, cmd, i); err != nil {
			return err
		}
	}
	return nil
}

func (t *HotReloadTiFlashConfig) Undo() error { /* nothing need to be done */ return nil }
