package experiment

import (
	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/schrodinger/pkg/queue"
	"github.com/pingcap/schrodinger/pkg/types"
)

var pendingSizeLimit int32 = 50

// task is use for start experiment
type task struct {
	experiment *types.ExperimentProcess

	position int
}

func (t *task) Key() interface{} {
	return t.experiment.BoxID
}

func (t *task) SetPosition(pos int) {
	t.position = pos
}

// Recover recovers controller
func (ctrl *Controller) Recover(experiments []*types.ExperimentProcess) error {
	for _, experiment := range experiments {
		if experiment.IsPending() {
			continue
		}

		ctrl.handleExperimentStart(experiment)
	}

	for _, experiment := range experiments {
		if !experiment.IsPending() {
			continue
		}

		experimentType, err := ctrl.getExperimentType(experiment)
		if err != nil {
			continue
		}

		switch experimentType {
		// TODO: remove bench from schrodinger
		case types.BoxTypeBench, types.BoxTypeGeneral:
			err = ctrl.pending.Push(&task{
				experiment: experiment,
			})
		}
		if err != nil {
			return errors.Trace(err)
		}
	}

	return nil
}

// need to think about recover
func (ctrl *Controller) newTask(experiment *types.ExperimentProcess) error {
	box, exist := ctrl.storage.GetBox(experiment.BoxID)
	if !exist {
		log.Errorf("[experiment %s] can not found box %d", experiment.Name, experiment.BoxID)
		return errors.Errorf("[experiment %s] can not found box %d", experiment.Name, experiment.BoxID)
	}

	var (
		err  error
		item queue.Item
	)

	switch box.Misc.Type {
	// TODO: remove bench
	case types.BoxTypeBench, types.BoxTypeGeneral:
		log.Debugf("[experiment %s] add to general queue, box %d", experiment.Name, experiment.BoxID)
		item, err = ctrl.pending.Remove(experiment.BoxID)
		if err == nil {
			t := item.(*task)
			ctrl.storage.OmitExperimentProcess(t.experiment.ID)
		}
		err = ctrl.pending.Push(&task{
			experiment: experiment,
		})
	default:
		log.Errorf("[experiment %s] box type %s is not supported", experiment.Name, box.Misc.Type)
		return errors.NotSupportedf("[experiment %s] box type %s", experiment.Name, box.Misc.Type)
	}

	return errors.Trace(err)
}

func (ctrl *Controller) listPendingExperiment() []*types.ExperimentProcess {
	items := ctrl.pending.All()
	experiments := make([]*types.ExperimentProcess, len(items))

	for _, item := range items {
		t := item.(*task)
		experiments = append(experiments, t.experiment)
	}

	return experiments
}

// CancelExperiment cancels experiment
func (ctrl *Controller) CancelExperiment(boxID int64) error {
	var (
		err  error
		item queue.Item
	)

	omitExperimet := func() error {
		t := item.(*task)
		return errors.Trace(ctrl.storage.OmitExperimentProcess(t.experiment.ID))
	}

	item, err = ctrl.pending.Remove(boxID)
	if err == nil {
		return omitExperimet()
	}

	return errors.Trace(err)
}

func (ctrl *Controller) scheduleTask() {
	log.Info("schedule general experiment")
	if ctrl.canRunNextGeneralExperiment() {
		ctrl.scheduleTaskAction(ctrl.pending)
	} else {
		log.Debugf("do not have enough resource, there are %d general experiments to run", ctrl.pending.Size())
	}
}

func (ctrl *Controller) scheduleTaskAction(pendingTasks queue.Queue) {
	item, err := pendingTasks.Pop()
	if err != nil {
		if !errors.IsNotValid(err) {
			log.Errorf("fail to pop task from pending error %v", err)
		}
		return
	}

	t := item.(*task)
	if ctrl.detectSameBoxRunningExperiment(t.experiment) {
		err = pendingTasks.Push(item)
		if err != nil {
			log.Errorf("fail to add task to pending queue back error %v", err)
		}
		return
	}

	ctrl.handleExperimentStart(t.experiment)
}

func (ctrl *Controller) detectSameBoxRunningExperiment(experiment *types.ExperimentProcess) bool {
	for _, exp := range ctrl.experiments {
		if exp.BoxID() == experiment.BoxID {
			return true
		}
	}
	return false
}

func (ctrl *Controller) getExperimentType(experiment *types.ExperimentProcess) (types.BoxType, error) {
	box, exist := ctrl.storage.GetBox(experiment.BoxID)
	if !exist {
		log.Errorf("[experiment %s] can not found box %d", experiment.Name, experiment.BoxID)
		return "", errors.Errorf("[experiment %s] can not found box %d", experiment.Name, experiment.BoxID)
	}
	return box.Misc.Type, nil
}
