package configeval

import (
	"context"
	"fmt"
	"math"
	"reflect"
	"strconv"
	"strings"

	"github.com/antonmedv/expr"
	"github.com/fatih/structs"
	"github.com/pingcap/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

type EvalContext struct {
	Memory     *ResRequest
	CPU        *ResRequest
	IOPS       int64
	Throughput int64
}

// defines resource requests and limits
type ResRequest struct {
	Request int64
	Limit   int64
}

// component names to be used by gitops expression
const (
	PDEvalName        = "PD"
	TiDBEvalName      = "TiDB"
	TiProxyEvalName   = "TiProxy"
	TiKVEvalName      = "TiKV"
	TiFlashEvalName   = "TiFlash"
	LightningEvalName = "Lightning"
	EmptyValue        = "0"
	EmptyName         = ""
)

var componentList = []string{"pd", "tidb", "tikv", "tiflash", "tiproxy"}

func EvalClusterConfig(infraApiCli client.Client, cc *cloudv1.ClusterConfig,
	c *cloudv1.Cluster) error {

	evalEnv, err := constructEvalEnv(infraApiCli, c)
	if err != nil {
		return err
	}

	if cc.Spec.PD != nil {
		pdConfig, err := evalComponentConfig(infraApiCli, evalEnv, cloudv1.ComponentPD, cc.Spec.PD)
		if err != nil {
			return err
		}
		cc.Spec.PD.Config = pdConfig.Config
	}

	if cc.Spec.TiDB != nil {
		tidbConfig, err := evalComponentConfig(infraApiCli, evalEnv, cloudv1.ComponentTiDB, cc.Spec.TiDB)
		if err != nil {
			return err
		}
		cc.Spec.TiDB.Config = tidbConfig.Config
	}

	if cc.Spec.TiKV != nil {
		tikvConfig, err := evalComponentConfig(infraApiCli, evalEnv, cloudv1.ComponentTiKV, cc.Spec.TiKV)
		if err != nil {
			return err
		}
		cc.Spec.TiKV.Config = tikvConfig.Config
	}

	if cc.Spec.TiFlash != nil && FindMachineType(c, cloudv1.ComponentTiFlash) != "" {
		tiflashConfig, err := evalComponentConfig(infraApiCli, evalEnv, cloudv1.ComponentTiFlash, cc.Spec.TiFlash)
		if err != nil {
			return err
		}
		cc.Spec.TiFlash.Config = tiflashConfig.Config
	}

	if cc.Spec.TiProxy != nil && FindMachineType(c, cloudv1.ComponentTiProxy) != "" {
		tiproxyConfig, err := evalComponentConfig(infraApiCli, evalEnv, cloudv1.ComponentTiProxy, cc.Spec.TiProxy)
		if err != nil {
			return err
		}
		cc.Spec.TiProxy.Config = tiproxyConfig.Config
	}

	return nil
}

func RemoveExpClusterConifg(cc *cloudv1.ClusterConfig, c *cloudv1.Cluster) error {
	if cc.Spec.PD != nil {
		pdConfig, err := removeExpComponentConfig(
			&cloudv1.Configuration{
				Config: cc.Spec.PD.Config,
			})
		if err != nil {
			return err
		}
		cc.Spec.PD.Config = pdConfig.Config
	}

	if cc.Spec.TiDB != nil {
		tidbConfig, err := removeExpComponentConfig(
			&cloudv1.Configuration{
				Config: cc.Spec.TiDB.Config,
			})
		if err != nil {
			return err
		}
		cc.Spec.TiDB.Config = tidbConfig.Config
	}

	if cc.Spec.TiKV != nil {
		tikvConfig, err := removeExpComponentConfig(
			&cloudv1.Configuration{
				Config: cc.Spec.TiKV.Config,
			})
		if err != nil {
			return err
		}
		cc.Spec.TiKV.Config = tikvConfig.Config
	}

	if cc.Spec.TiFlash != nil && FindMachineType(c, cloudv1.ComponentTiFlash) != "" {
		tiflashConfig, err := removeExpComponentConfig(
			&cloudv1.Configuration{
				Config: cc.Spec.TiFlash.Config,
			})
		if err != nil {
			return err
		}
		cc.Spec.TiFlash.Config = tiflashConfig.Config
	}

	if cc.Spec.TiProxy != nil && FindMachineType(c, cloudv1.ComponentTiProxy) != "" {
		tiproxyConfig, err := removeExpComponentConfig(
			&cloudv1.Configuration{
				Config: cc.Spec.TiProxy.Config,
			})
		if err != nil {
			return err
		}
		cc.Spec.TiProxy.Config = tiproxyConfig.Config
	}

	return nil
}

func removeExpComponentConfig(compConfig *cloudv1.Configuration) (*cloudv1.Configuration, error) {
	err := RemoveExp(compConfig.Config.MP)
	if err != nil {
		return nil, err
	}
	return compConfig, nil
}

func constructComponentEnv(infraAPICli client.Client, cluster *cloudv1.Cluster, compName cloudv1.ComponentType) (string, map[string]interface{}, error) {
	var (
		compIOPS, compThroughput, evalName string
	)

	switch compName {
	case cloudv1.ComponentTiDB:
		compIOPS = EmptyValue
		compThroughput = EmptyValue
		evalName = TiDBEvalName
	case cloudv1.ComponentPD:
		compIOPS = PDGp3IOPS
		compThroughput = PDGp3ThroughputMBps
		evalName = PDEvalName
	case cloudv1.ComponentTiKV:
		compIOPS = TiKVGp3IOPS
		compThroughput = TiKVGp3ThroughputMBps
		evalName = TiKVEvalName
	case cloudv1.ComponentTiFlash:
		compIOPS = TiFlashGp3IOPS
		compThroughput = TiFlashGp3ThroughputMBps
		evalName = TiFlashEvalName
	case cloudv1.ComponentTiProxy:
		compIOPS = EmptyValue
		compThroughput = EmptyValue
		evalName = TiProxyEvalName
	default:
		compIOPS = EmptyValue
		compThroughput = EmptyValue
		evalName = TiDBEvalName
	}

	iops, err := strconv.ParseInt(compIOPS, 10, 64)
	if err != nil {
		return evalName, nil, errors.Annotate(err, "parsing IOPS")
	}
	throughput, err := strconv.ParseInt(compThroughput, 10, 64)
	if err != nil {
		return evalName, nil, errors.Annotate(err, "parsing thoughput")
	}

	machineType := FindMachineType(cluster, compName)
	providerType := cluster.GetProviderType()
	if providerType == "" {
		// this might happen when revision_webhook are called before cluster_webhook.
		nt := cloudv1.Network{}
		err = infraAPICli.Get(context.TODO(), cluster.GetNetworkRef(), &nt)
		if err != nil {
			return evalName, nil, err
		}
		providerType = nt.Labels[cloudv1.ProviderTypeLabel]
	}

	instanceSpec, err := FindInstanceType(infraAPICli, providerType, machineType)
	if err != nil {
		return evalName, nil, err
	}

	evalCtx := &EvalContext{
		Memory: &ResRequest{
			Request: instanceSpec.Memory.Value(),
			Limit:   instanceSpec.Memory.Value(),
		},
		CPU: &ResRequest{
			Request: instanceSpec.CPU.Value(),
			Limit:   instanceSpec.CPU.Value(),
		},
		IOPS:       iops,
		Throughput: throughput,
	}

	evalEnv := structs.Map(evalCtx)
	return evalName, evalEnv, nil
}

func constructEvalEnv(infraAPICli client.Client, cluster *cloudv1.Cluster) (map[string]interface{}, error) {
	m := make(map[string]interface{})
	m["round"] = roundFunc
	m["string"] = stringFunc
	m["max"] = maxFunc
	m["min"] = minFunc

	for _, v := range componentList {
		if v == "tiflash" && cluster.Spec.Cluster.TiFlash.Replicas == 0 {
			continue
		}
		if v == "tiproxy" && cluster.Spec.Cluster.TiProxy.Replicas == 0 {
			continue
		}
		evalName, evalEnv, err := constructComponentEnv(infraAPICli, cluster, cloudv1.ComponentType(v))
		if err != nil {
			return nil, err
		}
		m[evalName] = evalEnv
	}
	return m, nil
}

func evalComponentConfig(infraAPICli client.Client, evalEnv map[string]interface{}, compName cloudv1.ComponentType,
	compConfig *cloudv1.Configuration) (*cloudv1.Configuration, error) {
	if compConfig.Config == nil || compConfig.Config.MP == nil {
		return compConfig, nil
	}
	// eval the base level configs
	err := EvalConfigs(compConfig.Config.MP, evalEnv)
	if err != nil {
		return nil, err
	}
	return compConfig, nil
}

func convertToFloat64(i interface{}) float64 {
	switch v := i.(type) {
	case int, int8, int16, int32, int64:
		return float64(reflect.ValueOf(v).Int())
	default:
		return reflect.ValueOf(v).Float()
	}
}

func roundFunc(f interface{}) int64 {
	return int64(math.Round(convertToFloat64(f)))
}

func stringFunc(i interface{}) string {
	if v, ok := i.(int64); ok {
		return fmt.Sprintf("%d", v)
	}
	if v, ok := i.(float64); ok {
		return fmt.Sprintf("%.2f", v)
	}
	return ""
}

func maxFunc(a, b interface{}) interface{} {
	if math.Max(convertToFloat64(a), convertToFloat64(b)) == convertToFloat64(a) {
		return a
	}
	return b
}

func minFunc(a, b interface{}) interface{} {
	if math.Min(convertToFloat64(a), convertToFloat64(b)) == convertToFloat64(a) {
		return a
	}
	return b
}

func EvalConfigs(base map[string]interface{}, evalEnv map[string]interface{}, keyPath ...string) error {
	child, err := GetChild(base, keyPath...)
	if err != nil {
		return errors.Annotate(err, "get child")
	}

	for k, v := range child {
		switch v.(type) {
		case map[string]interface{}:
			if err := EvalConfigs(child, evalEnv, k); err != nil {
				return errors.Annotatef(err, "error processing key %s", k)
			}
		default:
			exp := fmt.Sprintf("%v", v)
			if strings.HasPrefix(exp, "{") && strings.HasSuffix(exp, "}") {
				exp = strings.TrimPrefix(exp, "{")
				exp = strings.TrimSuffix(exp, "}")
				env := evalEnv

				value, err := expr.Eval(exp, env)
				if err != nil {
					return err
				}
				child[k] = value
			}
		}
	}
	return nil
}

func GetChild(base map[string]interface{}, keyPath ...string) (map[string]interface{}, error) {
	child := base
	for _, key := range keyPath {
		next, ok := child[key]
		if !ok {
			return nil, errors.New(fmt.Sprintf("key %s not found", strings.Join(keyPath, ".")))
		}
		child, ok = next.(map[string]interface{})
		if !ok {
			return nil, errors.New(fmt.Sprintf("key %s is not an Object", strings.Join(keyPath, ".")))
		}
	}
	return child, nil
}

func RemoveExp(base map[string]interface{}, keyPath ...string) error {
	child, err := GetChild(base, keyPath...)
	if err != nil {
		return errors.Annotate(err, "get child")
	}

	for k, v := range child {
		switch v.(type) {
		case map[string]interface{}:
			if err := RemoveExp(child, k); err != nil {
				return errors.Annotate(err, fmt.Sprintf("error processing key %s", k))
			}
		default:
			exp := fmt.Sprintf("%v", v)
			if strings.HasPrefix(exp, "{") && strings.HasSuffix(exp, "}") {
				delete(child, k)
			}
		}
	}
	return nil
}
