package cat

import (
	"context"
	"sync"
	"time"

	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/schrodinger/config"
	"github.com/pingcap/schrodinger/manager/cat"
	"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

	cats map[int64]*cat.Cat

	operator cat.Operator
}

// NewController creates controller
func NewController(s *storage.Storage, operator cat.Operator) *Controller {
	ctx, shutdown := context.WithCancel(context.Background())
	ctrl := &Controller{
		ctx:      ctx,
		shutdown: shutdown,
		storage:  s,
		cats:     make(map[int64]*cat.Cat),
		operator: operator,
	}

	go ctrl.run()
	return ctrl
}

func (ctrl *Controller) run() {
	log.Info("cat controller start !")
	ctrl.wg.Add(1)
	defer func() {
		log.Info("cat 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.monitor()
		}
	}
}

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

	var deleteCatIDs []int64

	for id, cat := range ctrl.cats {
		if cat.IsDestroyed() {
			deleteCatIDs = append(deleteCatIDs, id)
		}
	}

	for _, id := range deleteCatIDs {
		log.Infof("remove cat %d from controller", id)
		delete(ctrl.cats, id)
	}
}

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

// StartCat starts cat
func (ctrl *Controller) StartCat(ctx context.Context, catProcess *types.CatProcess) (*cat.Cat, error) {
	ctrl.Lock()
	c, exist := ctrl.cats[catProcess.ID]
	if exist {
		ctrl.Unlock()
		return c, errors.AlreadyExistsf("cat %d", catProcess.ID)
	}

	c = cat.NewCat(ctx, catProcess, ctrl.storage, ctrl.operator)
	ctrl.cats[c.Identify()] = c
	ctrl.Unlock()

	return c, errors.Trace(c.Start())
}

// DestroyCat stops cat
func (ctrl *Controller) DestroyCat(catID int64) error {
	ctrl.RLock()

	cat, exist := ctrl.cats[catID]
	if !exist {
		ctrl.RUnlock()
		return errors.NotFoundf("cat id %d", catID)
	}
	ctrl.RUnlock()
	return errors.Trace(cat.Stop())
}

// UpgradeCat upgrades cat
func (ctrl *Controller) UpgradeCat(catID int64, config *types.Cat) error {
	ctrl.RLock()

	cat, exist := ctrl.cats[catID]
	if !exist {
		ctrl.RUnlock()
		return errors.NotFoundf("cat id %d", catID)
	}
	ctrl.RUnlock()
	return errors.Trace(cat.Upgrade(config))
}

// BackupCat backups cat physically
func (ctrl *Controller) BackupCat(catID int64, backupName string) error {
	ctrl.RLock()
	defer ctrl.RUnlock()
	cat, exist := ctrl.cats[catID]
	if !exist {
		return errors.NotFoundf("cat id %d", catID)
	}

	return errors.Trace(cat.Backup(backupName))
}

// ListCatBackups returns the all backups of the cat
func (ctrl *Controller) ListCatBackups(catID int64) ([]*types.TiDBClusterBackup, error) {
	ctrl.RLock()
	defer ctrl.RUnlock()
	cat, exist := ctrl.cats[catID]
	if !exist {
		return nil, errors.NotFoundf("cat id %d", catID)
	}

	return cat.ListBackups()
}

// RestoreCat restores cat with a physical backup
func (ctrl *Controller) RestoreCat(catID int64, backupName string) error {
	ctrl.RLock()
	defer ctrl.RUnlock()
	cat, exist := ctrl.cats[catID]
	if !exist {
		return errors.NotFoundf("cat id %d", catID)
	}

	return errors.Trace(cat.Restore(backupName))
}

// GetCatStatus returns the stage of cat
func (ctrl *Controller) GetCatStatus(catID int64) (types.CatStatus, error) {
	ctrl.RLock()
	defer ctrl.RUnlock()

	cat, exist := ctrl.cats[catID]
	if exist {
		return cat.Status(), nil
	}

	catProcess, exist := ctrl.storage.GetCatProcess(catID)
	if !exist {
		return types.CatStatus{}, errors.NotFoundf("cat id %d", catID)
	}

	return types.CatStatus{State: catProcess.State, Stage: catProcess.Stage}, nil
}
