package experiment

import (
	"context"
	"sync"
	"time"

	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/schrodinger/config"
	"github.com/pingcap/schrodinger/manager/controller/cat"
	"github.com/pingcap/schrodinger/manager/controller/test"
	"github.com/pingcap/schrodinger/manager/experiment"
	"github.com/pingcap/schrodinger/pkg/notify"
	"github.com/pingcap/schrodinger/pkg/queue"
	"github.com/pingcap/schrodinger/pkg/storage"
	"github.com/pingcap/schrodinger/pkg/types"
)

// Controller controls missions and tasks
type Controller struct {
	sync.RWMutex
	wg sync.WaitGroup

	ctx      context.Context
	shutdown context.CancelFunc

	storage  *storage.Storage
	catCtrl  *cat.Controller
	testCtrl *test.Controller

	pending queue.Queue

	experiments map[int64]*experiment.Experiment

	notifiers map[string]notify.Notifier

	operator experiment.Operator
}

// NewController creates controller
func NewController(
	s *storage.Storage,
	catCtrl *cat.Controller,
	testCtrl *test.Controller,
	notifiers map[string]notify.Notifier,
	operator experiment.Operator,
) *Controller {
	log.Info("create controller")
	ctx, shutdown := context.WithCancel(context.Background())
	ctrl := &Controller{
		ctx:         ctx,
		shutdown:    shutdown,
		storage:     s,
		experiments: make(map[int64]*experiment.Experiment),
		catCtrl:     catCtrl,
		testCtrl:    testCtrl,
		pending:     queue.NewListQueue(pendingSizeLimit),
		notifiers:   notifiers,
		operator:    operator,
	}

	go ctrl.run()
	return ctrl
}

func (ctrl *Controller) run() {
	log.Info("experiment controller start !")
	ctrl.wg.Add(1)
	defer func() {
		log.Info("experiment controller stop !")
		ctrl.wg.Done()
	}()

	ticker := time.NewTicker(config.GetGlobalConfig().Controller.MonitorInterval.Duration)
	defer ticker.Stop()
	for {
		select {
		case <-ctrl.ctx.Done():
			return
		case <-ticker.C:
			ctrl.scheduleTask()
			ctrl.monitor()
		}
	}
}

func (ctrl *Controller) monitor() {
	ctrl.Lock()
	defer ctrl.Unlock()

	var deleteExperimentIDs []int64

	for id, experiment := range ctrl.experiments {
		if experiment.IsFinished() || experiment.IsDestroyed() {
			deleteExperimentIDs = append(deleteExperimentIDs, id)
		}
	}

	for _, id := range deleteExperimentIDs {
		log.Infof("remove experiment %d from controller", id)
		delete(ctrl.experiments, id)
	}
}

func (ctrl *Controller) close() {
	log.Infof("stop experiment controller")
	ctrl.shutdown()
	ctrl.wg.Wait()
}

// StartExperiment starts experiment
func (ctrl *Controller) StartExperiment(experimentProcess *types.ExperimentProcess) error {
	return errors.Trace(ctrl.newTask(experimentProcess))
}

// DestroyExperiment destroys experiment
func (ctrl *Controller) DestroyExperiment(experimentID int64) error {
	ctrl.RLock()

	exp, exist := ctrl.experiments[experimentID]
	if !exist {
		ctrl.RUnlock()
		return errors.NotFoundf("experiment %d", experimentID)
	}

	ctrl.RUnlock()
	return errors.Trace(exp.Destroy())
}

// AnnotateExperiment annotates experiment
func (ctrl *Controller) AnnotateExperiment(experimentID int64, annotation string, author string) error {
	ctrl.RLock()

	exp, exist := ctrl.experiments[experimentID]
	if !exist {
		ctrl.RUnlock()
		return errors.NotFoundf("experiment %d", experimentID)
	}

	ctrl.RUnlock()

	exp.Annotate(annotation, author)
	return nil
}

// ReplaceCat replaces cat
func (ctrl *Controller) ReplaceCat(experimentID int64, config *types.Cat) error {
	ctrl.RLock()

	exp, exist := ctrl.experiments[experimentID]
	if !exist {
		ctrl.RUnlock()
		return errors.NotFoundf("experiment %d", experimentID)
	}

	ctrl.RUnlock()
	return errors.Trace(exp.ReplaceCat(config))
}

// StopTest stops test in experiment
func (ctrl *Controller) StopTest(experimentID int64, testName string) error {
	ctrl.RLock()

	exp, exist := ctrl.experiments[experimentID]
	if !exist {
		ctrl.RUnlock()
		return errors.NotFoundf("experiment %d", experimentID)
	}

	ctrl.RUnlock()
	return errors.Trace(exp.StopTest(testName))
}

// StartTest starts test in experiment
func (ctrl *Controller) StartTest(experimentID int64, testName string) error {
	ctrl.RLock()

	exp, exist := ctrl.experiments[experimentID]
	if !exist {
		ctrl.RUnlock()
		return errors.NotFoundf("experiment %d", experimentID)
	}

	ctrl.RUnlock()
	return errors.Trace(exp.StartTest(testName))
}

// RemoveTest removes test in experiment
func (ctrl *Controller) RemoveTest(experimentID int64, testName string) error {
	ctrl.RLock()

	exp, exist := ctrl.experiments[experimentID]
	if !exist {
		ctrl.RUnlock()
		return errors.NotFoundf("experiment %d", experimentID)
	}

	ctrl.RUnlock()
	return errors.Trace(exp.RemoveTest(testName))
}

// ListRunningExperiments lists running experiments
func (ctrl *Controller) ListRunningExperiments() types.ExperimentProcesses {
	ctrl.RLock()
	defer ctrl.RUnlock()
	var experiments types.ExperimentProcesses
	for _, experiment := range ctrl.experiments {
		experiments = append(experiments, experiment.GetProcess())
	}
	return experiments
}

// DestroyExperimentsByBoxID destroys experiments by box id
func (ctrl *Controller) DestroyExperimentsByBoxID(boxID int64) error {
	ctrl.RLock()
	defer ctrl.RUnlock()
	for _, experiment := range ctrl.experiments {
		if experiment.BoxID() != boxID {
			continue
		}
		err := experiment.Destroy()
		if err != nil {
			return errors.Trace(err)
		}
	}

	return nil
}

func (ctrl *Controller) handleExperimentStart(experimentProcess *types.ExperimentProcess) {
	ctrl.Lock()
	defer ctrl.Unlock()

	exp, err := experiment.NewExperiment(
		ctrl.ctx,
		experimentProcess,
		ctrl.storage,
		ctrl.catCtrl,
		ctrl.testCtrl,
		ctrl.notifiers,
		ctrl.operator)
	if err != nil {
		log.Errorf("create experiment : %+v / error : %v", experimentProcess, err)
		return
	}

	ctrl.experiments[experimentProcess.ID] = exp
	go func() {
		// TODO: If the experiment starts failed, we should send some message to user
		if err := exp.Start(); err != nil {
			log.Errorf("start experiment : %+v / error : %v", experimentProcess, err)
		}
	}()
}

func (ctrl *Controller) canRunNextGeneralExperiment() bool {
	var count int
	for _, e := range ctrl.experiments {
		if e.Type() == types.BoxTypeGeneral && (!e.IsFinished() || !e.IsDestroyed()) {
			count++
		}
	}

	managerSettings, exist := ctrl.storage.GetManagerSettings()
	if !exist {
		log.Error("manager settings not exist")
		return false
	}

	return count < managerSettings.GeneralRunningSizeLimit
}
