package oltppkg

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"go.uber.org/zap"

	"github.com/pingcap/endless/pkg/chaos"
	"github.com/pingcap/log"
	"github.com/pingcap/test-infra/sdk/resource"
	_ "github.com/pingcap/test-infra/sdk/resource/impl/k8s"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func GetOptList() []ChaosOpt {
	var optlist []ChaosOpt
	opt := ChaosOpt{
		NodeType:  "tikv",
		Peer:      ToIntPtr(0),
		FaultType: "failure",
		Timeout:   ToDurationPtr(2 * time.Minute),
	}
	optlist = append(optlist, opt)
	opt = ChaosOpt{
		NodeType:  "tikv",
		Peer:      ToIntPtr(0),
		FaultType: "network-loss",
		Timeout:   ToDurationPtr(1 * time.Minute),
		Duration:  ToStringPtr("3s"),
		Schedule:  ToStringPtr("@every 5s"),
	}
	optlist = append(optlist, opt)
	opt = ChaosOpt{
		NodeType:  "tikv",
		Peer:      ToIntPtr(0),
		FaultType: "network-delay",
		Timeout:   ToDurationPtr(1 * time.Minute),
		Duration:  ToStringPtr("3s"),
		Schedule:  ToStringPtr("@every 5s"),
	}
	optlist = append(optlist, opt)
	return optlist
}

type ChaosOpt struct {
	NodeType string
	//choose from tikv pd tidb
	IsLeader *bool
	//only for pd type
	Peer  *int
	All   *bool
	Label *map[string]string
	//only valid for tikv
	FaultType string
	//support failure network-delay network-loss network-partition
	Duration *string `default:"1s"`
	//default is 1s
	Timeout  *time.Duration
	Schedule *string `default:"@every 5s"`
	//default is every 5s
	// Cron defines a cron job rule.
	//
	// Some rule examples:
	// "0 30 * * * *" means to "Every hour on the half hour"
	// "@hourly"      means to "Every hour"
	// "@every 1h30m" means to "Every hour thirty"
	//
	// More rule info: https://godoc.org/github.com/robfig/cron
}

func ToStringPtr(str string) *string {
	return &str
}
func ToIntPtr(i int) *int {
	return &i
}
func ToDurationPtr(t time.Duration) *time.Duration {
	return &t
}
func ToBoolPtr(t bool) *bool {
	return &t
}
func ToPtr(xs interface{}) interface{} {
	// if err, ok := xs[len(xs)-1].(error); ok && err != nil {
	// 	Expect(err).ShouldNot(HaveOccurred())
	// }
	return &xs
}

// examples:
// opt := oltppkg.ChaosOpt{NodeType: "tikv", Peer: &[]int{0}[0], FaultType: "failure",
// Timeout: &[]time.Duration{1 * time.Minute}[0]}
// opt2 := oltppkg.ChaosOpt{NodeType: "tikv", Peer: oltppkg.ToIntPtr(0), FaultType: "failure",
// Timeout: oltppkg.ToDurationPtr(1 * time.Minute)}
// chaoId, err := opt.InjectFaultV2(ctx2, tc)
func (opt *ChaosOpt) InjectFaultV2(ctx context.Context, tc resource.TiDBCluster) (*resource.ChaosIdentifier, error) {
	var chaosId resource.ChaosIdentifier
	var err error
	selector, err := SelectChaos(tc, opt)
	if err != nil {
		return nil, err
	}
	switch strings.ToLower(opt.FaultType) {
	case "failure":
		log.Info(fmt.Sprintf("begin to select %s to shutdown instance.", opt.NodeType))
		if opt.Duration != nil && opt.Schedule != nil {
			chaosId, err = tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.FailureExperimentSpec{
					Duration:  *opt.Duration,
					Scheduler: &resource.ExperimentScheduler{Cron: *opt.Schedule},
				},
			})
		} else {
			chaosId, err = tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector:       selector,
				ExperimentSpec: &resource.FailureExperimentSpec{},
			})
		}
	case "network-partition":
		log.Info(fmt.Sprintf("begin to select %s to network Partition.", opt.NodeType))
		if opt.Duration != nil && opt.Schedule != nil {
			chaosId, err = tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.NetworkPartitionSpec{
					Duration:  *opt.Duration,
					Scheduler: &resource.ExperimentScheduler{Cron: *opt.Schedule},
				},
			})
		} else {
			chaosId, err = tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector:       selector,
				ExperimentSpec: &resource.NetworkPartitionSpec{},
			})
		}
	case "network-delay":
		log.Info(fmt.Sprintf("begin to select %s to network delay.", opt.NodeType))
		if opt.Duration != nil && opt.Schedule != nil {
			chaosId, err = tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.NetworkLatencySpec{
					Duration:    *opt.Duration,
					Scheduler:   &resource.ExperimentScheduler{Cron: *opt.Schedule},
					Latency:     "1s",
					Correlation: "25",
					Jitter:      "20ms",
				},
			})
		} else {
			chaosId, err = tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.NetworkLatencySpec{
					Latency:     "1s",
					Correlation: "25",
					Jitter:      "20ms",
				},
			})
		}
	case "network-loss":
		log.Info(fmt.Sprintf("begin to select %s to network Loss.", opt.NodeType))
		if opt.Duration != nil && opt.Schedule != nil {
			chaosId, err = tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.NetworkLossSpec{
					Duration:    *opt.Duration,
					Scheduler:   &resource.ExperimentScheduler{Cron: *opt.Schedule},
					Loss:        "80",
					Correlation: "25",
				},
			})
		} else {
			chaosId, err = tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.NetworkLossSpec{
					Loss:        "80",
					Correlation: "25",
				},
			})
		}
	default:
		{
			log.Info("unknown fault type")
			err = errors.New("unknown fault type")
			return nil, err
		}
	}
	if err != nil {
		return nil, err
	}
	// var t time.Duration = 60 * time.Second
	var t time.Duration
	if opt.Timeout == nil {
		t = 1 * time.Minute
	} else {
		t = *opt.Timeout
	}
	timer := time.NewTimer(t)
	defer timer.Stop()
	select {
	case <-timer.C:
		log.Info(fmt.Sprintf("delete the %s chaos.", opt.NodeType))
		err = tc.ChaosClient().Delete(chaosId)
	case <-ctx.Done():
		log.Info(fmt.Sprintf("Context timeout, delete the %s chaos.", opt.NodeType))
		time.Sleep(1 * time.Minute)
		err = tc.ChaosClient().Delete(chaosId)
	}
	return &chaosId, nil
}
func SelectChaos(tc resource.TiDBCluster, opt *ChaosOpt) ([]resource.ChaosSelector, error) {
	var opts resource.TiDBClusterSelectorOpts
	switch strings.ToLower(opt.NodeType) {
	case "tikv":
		if opt.Label != nil {
			opts = resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					StoreLabelSelector: &metav1.LabelSelector{
						MatchLabels: *opt.Label,
					},
				},
			}
		} else if opt.Peer != nil {
			opts = resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					Peer: opt.Peer,
				},
			}
		} else if opt.All != nil {
			opts = resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					All: *opt.All,
				},
			}
		} else {
			log.Error("Chaos spec is unvalid.")
			return nil, fmt.Errorf("Chaos spec is unvalid.")
		}
	case "tidb":
		if opt.Peer != nil {
			opts = resource.TiDBClusterSelectorOpts{
				TiDBClusterTiDBSelectorSpec: &resource.TiDBClusterTiDBSelectorSpec{
					Peer: opt.Peer,
				},
			}
		} else if opt.All != nil {
			opts = resource.TiDBClusterSelectorOpts{
				TiDBClusterTiDBSelectorSpec: &resource.TiDBClusterTiDBSelectorSpec{
					All: *opt.All,
				},
			}
		} else {
			log.Error("Chaos spec is unvalid.")
			return nil, fmt.Errorf("Chaos spec is unvalid.")
		}
	case "pd":
		if opt.IsLeader != nil {
			opts = resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					IsLeader: opt.IsLeader,
				},
			}
		} else if opt.Peer != nil {
			opts = resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					Peer: opt.Peer,
				},
			}
		} else if opt.All != nil {
			opts = resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					All: *opt.All,
				},
			}
		} else {
			log.Error("Chaos spec is unvalid.")
			return nil, fmt.Errorf("Chaos spec is unvalid.")
		}
	default:
		{
			log.Info("unknown node type")
			err := errors.New("unknown node type")
			return nil, err
		}

	}
	selector, err := tc.ChaosSelect(opts)
	if err != nil {
		return nil, err
	}
	return selector, nil
}

func InitChaos(tc resource.TiDBCluster, comp resource.TiDBCompType, name string, experimentSpec resource.ExperimentSpec, selectorSpec interface{}) (chaos.Chaos, error) {
	var c chaos.Chaos
	var err error
	c.Tc = tc
	c.Name = name
	c.ExperimentSpec = experimentSpec
	var selectorOpts resource.TiDBClusterSelectorOpts
	selectorSpecType := fmt.Sprintf("%T", selectorSpec)
	fmt.Printf("selectorSpec type:%s\n", selectorSpecType)
	switch comp {
	case resource.TiKV:
		var tikvSpec resource.TiDBClusterTiKVSelectorSpec
		switch selectorSpecType {
		case "*int":
			tikvSpec.Peer = selectorSpec.(*int)
		case "bool":
			tikvSpec.All = selectorSpec.(bool)
		// case "*v1.LabelSelector":
		// 	tikvSpec.StoreLabelSelector = selectorSpec.(*v1.LabelSelector)
		default:
			fmt.Printf("param:%v is an unknown type.\n", selectorSpec)
			err = errors.New("chaos need a valid SelectorSpec")
			return c, err
		}
		selectorOpts.TiDBClusterTiKVSelectorSpec = &tikvSpec
	case resource.PD:
		var pdSpec resource.TiDBClusterPDSelectorSpec
		switch selectorSpecType {
		case "*int":
			pdSpec.Peer = selectorSpec.(*int)
		case "bool":
			pdSpec.All = selectorSpec.(bool)
		case "*bool":
			pdSpec.IsLeader = selectorSpec.(*bool)
		default:
			err = errors.New("chaos need a valid SelectorSpec")
			return c, err
		}
		selectorOpts.TiDBClusterPDSelectorSpec = &pdSpec
	case resource.TiDB:
		var tidbSpec resource.TiDBClusterTiDBSelectorSpec
		switch selectorSpecType {
		case "*int":
			tidbSpec.Peer = selectorSpec.(*int)
		case "bool":
			tidbSpec.All = selectorSpec.(bool)
		default:
			err = errors.New("chaos need a valid SelectorSpec")
			return c, err
		}
		selectorOpts.TiDBClusterTiDBSelectorSpec = &tidbSpec
	default:
		err = errors.New("chaos need a valid TiDBCompType")
		return c, err
	}
	c.SelectorOpts = selectorOpts
	return c, nil
}

func InjectFaultByChaos(c *chaos.Chaos, ctx context.Context, faultDuration time.Duration) error {
	log.Info("fault will last for", zap.Duration("duration", faultDuration))
	var err error
	err = c.Run()
	defer c.Clean()
	if err != nil {
		return err
	}
	timeOut := time.After(faultDuration)
	select {
	case <-timeOut:
		err = c.Clean()
	case <-ctx.Done():
		err = c.Clean()
	}
	return err
}

func InjectChaosToWorkloadNode(ctx context.Context, workloadNode resource.WorkloadNode,
	experiment resource.ExperimentSpec, faultDuration time.Duration) error {
	selectors, _ := workloadNode.ChaosSelect(resource.WorkloadNodeSelectorOpts{All: true})
	log.Info("Inject chaos to ", zap.String("nodeName", workloadNode.Handle().Name))
	chaoId, err := workloadNode.ChaosClient().Create(
		resource.InjectChaosOptions{
			Selector:       selectors,
			ExperimentSpec: experiment,
		},
	)
	if err != nil {
		return err
	}
	timeOut := time.After(faultDuration)
	select {
	case <-timeOut:
		log.Info("Time out, delete chaos in ", zap.String("nodeName", workloadNode.Handle().Name))
		err = workloadNode.ChaosClient().Delete(chaoId)
	case <-ctx.Done():
		log.Info("Context done, delete chaos in ", zap.String("nodeName", workloadNode.Handle().Name))
		err = workloadNode.ChaosClient().Delete(chaoId)
	}
	return err
}
