package steps

import (
	"context"
	"fmt"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	apiresource "github.com/pingcap/test-infra/common/model/resource"
	"github.com/pingcap/test-infra/sdk/core"
	"github.com/pingcap/test-infra/sdk/resource"
	"go.uber.org/zap"
)

type chaosTask struct {
	target   *apiresource.ResourceHandle
	other    *apiresource.ResourceHandle
	duration time.Duration
	// how long the chaos will exist after the creation.
	spec         resource.ExperimentSpec
	selectOption resource.TiDBClusterSelectorOpts
	otherOption  *resource.TiDBClusterSelectorOpts
}

func NewChaosTask(target *apiresource.ResourceHandle, selectOption resource.TiDBClusterSelectorOpts, spec resource.ExperimentSpec, duration time.Duration) *chaosTask {
	return &chaosTask{
		target:       target,
		selectOption: selectOption,
		duration:     duration,
		spec:         spec,
	}
}

func (s *chaosTask) Execute(ctx context.Context) error {
	resolved, err := core.ResolveResourceHandle(s.target)
	if err != nil {
		return errors.Trace(err)
	}
	tc, ok := resolved.(resource.TiDBCluster)
	if !ok {
		return fmt.Errorf("chaos source should be tidb cluster, but got: %+v", resolved)
	}

	selector, err := tc.ChaosSelect(s.selectOption)
	if err != nil {
		return errors.Trace(err)
	}

	chaosOption := resource.InjectChaosOptions{
		Selector:       selector,
		ExperimentSpec: s.spec,
	}

	if s.otherOption != nil {
		if s.other == nil {
			return fmt.Errorf("other tidb cluster handler failed to find")
		}
		resolved, err = core.ResolveResourceHandle(s.other)
		if err != nil {
			return errors.Trace(err)
		}
		otherTc, ok := resolved.(resource.TiDBCluster)
		if !ok {
			return fmt.Errorf("other tidb cluster failed to find")
		}

		others, err := otherTc.ChaosSelect(*s.otherOption)
		if err != nil {
			return errors.Trace(err)
		}
		chaosOption.Target = &others
	}

	chaosID, err := tc.ChaosClient().Create(chaosOption)
	if err != nil {
		log.Info("create chaos failed", zap.Error(err), zap.Reflect("chaosOption", chaosOption))
		return errors.Trace(err)
	}
	defer func() {
		if err := tc.ChaosClient().Delete(chaosID); err != nil {
			log.Warn("delete chaos failed", zap.Error(err), zap.Reflect("chaosOption", chaosOption))
		}
	}()

	if chaosID == nil {
		log.Info("create chaos failed", zap.Reflect("chaosOption", chaosOption), zap.Reflect("chaosID", chaosID))
		return fmt.Errorf("create chaos failed, option=%+v", chaosOption)
	}

	time.Sleep(s.duration)

	log.Info("run chaos finished")

	return nil

}

func (s *chaosTask) String() string {
	return fmt.Sprintf("ChaosTask: spec=%+v, duration=%+v", s.spec, s.duration)
}

type networkLossChaosTask struct {
	target       *apiresource.ResourceHandle
	selectOption resource.TiDBClusterSelectorOpts

	duration    time.Duration
	loss        string
	correlation string
}

func newNetworkLossChaosTask(target *apiresource.ResourceHandle, selectOption resource.TiDBClusterSelectorOpts, duration time.Duration) *networkLossChaosTask {
	return &networkLossChaosTask{
		target:       target,
		selectOption: selectOption,
		duration:     duration,
		loss:         "100",
		correlation:  "25",
	}
}

func (s *networkLossChaosTask) String() string {
	return fmt.Sprintf("networkLossChaos: selectOption=%+v, duration=%+v, loss=%+v, correlation=%+v",
		s.selectOption, s.duration, s.loss, s.correlation)
}

func (s *networkLossChaosTask) Execute(ctx context.Context) error {
	return NewChaosTask(
		s.target,
		s.selectOption,
		&resource.NetworkLossSpec{Loss: s.loss, Correlation: s.correlation},
		s.duration).Execute(ctx)
}

type networkLatencyChaosTask struct {
	selectOption resource.TiDBClusterSelectorOpts
	duration     time.Duration
	latency      string
	jitter       string
	target       *apiresource.ResourceHandle
}

func newNetworkLatencyChaosTask(target *apiresource.ResourceHandle, selectOption resource.TiDBClusterSelectorOpts, duration time.Duration, latency, jitter string) *networkLatencyChaosTask {
	return &networkLatencyChaosTask{
		target:       target,
		selectOption: selectOption,
		duration:     duration,
		latency:      latency,
		jitter:       jitter,
	}
}

func (s *networkLatencyChaosTask) String() string {
	return fmt.Sprintf("networkLatencyChaos: selectOption=%+v, duration=%+v, latency=%+v, jitter=%+v",
		s.selectOption, s.duration, s.latency, s.jitter)
}

func (s *networkLatencyChaosTask) Execute(ctx context.Context) error {
	return NewChaosTask(
		s.target,
		s.selectOption,
		&resource.NetworkLatencySpec{
			Latency: s.latency,
			Jitter:  s.jitter,
		},
		s.duration).Execute(ctx)
}

type networkPartitionChaosTask struct {
	sourceOption resource.TiDBClusterSelectorOpts
	targetOption resource.TiDBClusterSelectorOpts
	duration     time.Duration
	source       *apiresource.ResourceHandle
	target       *apiresource.ResourceHandle
	spec         *resource.NetworkPartitionSpec
}

func NewNetworkPartitionChaosTask(source, target *apiresource.ResourceHandle, sourceOption, targetOption resource.TiDBClusterSelectorOpts, duration time.Duration, spec *resource.NetworkPartitionSpec) *networkPartitionChaosTask {
	return &networkPartitionChaosTask{
		source:       source,
		target:       target,
		sourceOption: sourceOption,
		targetOption: targetOption,
		duration:     duration,
		spec:         spec,
	}
}

func (s *networkPartitionChaosTask) String() string {
	return fmt.Sprintf("networkPartitionChaosTask: source=%+v,target=%+v, duration=%+v", *s.source, *s.target, s.duration)
}

func (s *networkPartitionChaosTask) Execute(ctx context.Context) error {
	chaosStep := &chaosTask{target: s.source, other: s.target, duration: s.duration, spec: s.spec, selectOption: s.sourceOption, otherOption: &s.targetOption}
	return chaosStep.Execute(ctx)
}
