package core

import (
	"context"
	"strings"
	"sync"

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

// Manager represent schrodinger manager
type Manager struct {
	sync.RWMutex

	storage *storage.Storage

	experimentCtrl *experiment.Controller
	catCtrl        *cat.Controller
	boxCtrl        *box.Controller
	testCtrl       *test.Controller

	operator *operator.Operator
}

// NewManager creates new manager
func NewManager(store *storage.Storage) (*Manager, error) {
	log.Info("manager is running now!")

	mgr := &Manager{}

	notifiers, err := mgr.initNotifiers()
	if err != nil {
		return nil, errors.Trace(err)
	}

	operator, err := operator.NewOperator(config.GetGlobalConfig())
	if err != nil {
		return nil, errors.Trace(err)
	}

	mgr.operator = operator
	mgr.storage = store
	mgr.catCtrl = cat.NewController(mgr.storage, operator)
	mgr.testCtrl = test.NewController(mgr.storage, operator)
	mgr.experimentCtrl = experiment.NewController(
		mgr.storage,
		mgr.catCtrl,
		mgr.testCtrl,
		notifiers,
		operator)
	mgr.boxCtrl = box.NewController(mgr.storage, mgr.experimentCtrl)

	if err := mgr.initManagerSettings(); err != nil {
		return nil, errors.Trace(err)
	}

	if err := mgr.recover(); err != nil {
		return nil, errors.Trace(err)
	}

	return mgr, nil
}

func (mgr *Manager) initNotifiers() (map[string]notify.Notifier, error) {
	notifiers := make(map[string]notify.Notifier)

	conf := config.GetGlobalConfig().Notifiers

	if !util.IsEmpty(conf.Slack.Token) {
		slackNotifier, err := slack.New(&conf.Slack)
		if err != nil {
			return nil, errors.Trace(err)
		}

		notifiers[slack.NotifierName] = slackNotifier
	}

	return notifiers, nil
}

func (mgr *Manager) recover() error {
	log.Infof("start to recover boxes")
	boxes, _ := mgr.storage.ListBox(nil)
	for _, box := range boxes {
		if !box.IsOnline() {
			continue
		}
		log.Infof("start box %s(%d)", box.Name, box.ID)
		err := mgr.boxCtrl.SetBox(box)
		if err != nil {
			log.Errorf("start box %d error %v", box.ID, err)
		}
	}

	log.Infof("start to recover experiment")

	// catIDs used for not start cat twice
	var catIDs []int64

	var recoverExperiments []*types.ExperimentProcess

	// TODO: use sql to list
	experiments, _ := mgr.storage.ListExperimentProcess(nil)
	for _, experiment := range experiments {
		if experiment.IsEnd() {
			continue
		}

		if experiment.IsEnding() {
			//TODO: do ending task
			continue
		}

		if experiment.Cat != nil && experiment.IsStateNormal() {
			catIDs = append(catIDs, experiment.Cat.ID)
		}

		recoverExperiments = append(recoverExperiments, experiment)
	}

	err := mgr.experimentCtrl.Recover(recoverExperiments)
	if err != nil {
		log.Errorf("recover experiments error %v", err)
	}

	// TODO: just list catProcessses which not destroyed
	cats, _ := mgr.storage.ListCatProcess(nil)

	for _, cat := range cats {
		if int64InSlice(cat.ID, catIDs) {
			continue
		}

		if cat.IsDestroyed() {
			continue
		}

		go func(cat *types.CatProcess) {
			_, err := mgr.catCtrl.StartCat(context.Background(), cat)
			if err != nil && !strings.Contains(err.Error(), "already exist") {
				log.Errorf("start cat %s error %v", cat.Namespace, err)
			}
		}(cat)
	}

	for _, test := range mgr.storage.ListTestTemplate() {
		if !test.IsBuilding() {
			continue
		}
		err := mgr.setTestTemplate(test)
		if err != nil {
			log.Errorf("recover test error %v", err)
		}
	}

	return nil
}

func (mgr *Manager) initManagerSettings() error {
	_, exist := mgr.storage.GetManagerSettings()
	if exist {
		return nil
	}

	settings := &types.ManagerSettings{
		GeneralRunningSizeLimit: 18,
		BenchRunningSizeLimit:   2,
	}
	return errors.Trace(mgr.storage.SetManagerSettings(settings))
}

// SetGeneralExperimentRunningSizeLimit settings general experiment size limit
func (mgr *Manager) SetGeneralExperimentRunningSizeLimit(size int) error {
	settings, exist := mgr.storage.GetManagerSettings()
	if !exist {
		return errors.New("manager settings not found")
	}
	settings.GeneralRunningSizeLimit = size
	return errors.Trace(mgr.storage.SetManagerSettings(settings))
}

// GetGeneralExperimentRunningSizeLimit gets general experiment running size limit
func (mgr *Manager) GetGeneralExperimentRunningSizeLimit() (int, error) {
	settings, exist := mgr.storage.GetManagerSettings()
	if !exist {
		return 0, errors.New("manager settings not found")
	}
	return settings.GeneralRunningSizeLimit, nil
}

// SetBenchExperimentRunningSizeLimit settings bench experiment size limit
func (mgr *Manager) SetBenchExperimentRunningSizeLimit(size int) error {
	settings, exist := mgr.storage.GetManagerSettings()
	if !exist {
		return errors.New("manager settings not found")
	}
	settings.BenchRunningSizeLimit = size
	return errors.Trace(mgr.storage.SetManagerSettings(settings))
}

// GetBenchExperimentRunningSizeLimit gets bench experiment running size limit
func (mgr *Manager) GetBenchExperimentRunningSizeLimit() (int, error) {
	settings, exist := mgr.storage.GetManagerSettings()
	if !exist {
		return 0, errors.New("manager settings not found")
	}
	return settings.BenchRunningSizeLimit, nil
}
