package core

import (
	"strings"

	"github.com/juju/errors"
	"github.com/pingcap/schrodinger/pkg/storage"
	"github.com/pingcap/schrodinger/pkg/types"
)

// UpdateBox updates box
func (mgr *Manager) UpdateBox(box *types.Box) error {
	oldBox, exist := mgr.storage.GetBox(box.ID)
	if !exist {
		return errors.NotFoundf("box %d", box.ID)
	}

	if !box.WithoutCat {
		box.SetNodeLabelsForUpdate(oldBox)
	}

	if err := mgr.storage.UpdateBox(box); err != nil {
		return errors.Trace(err)
	}

	return errors.Trace(mgr.boxCtrl.SetBox(box))
}

// CreateBox creates box
func (mgr *Manager) CreateBox(box *types.Box) error {
	_, exist := mgr.storage.GetBoxByName(box.Name)
	if exist {
		return errors.AlreadyExistsf("box name %s", box.Name)
	}

	if len(box.State) == 0 {
		box.State = types.BoxStateOnline
	}

	if !box.WithoutCat {
		box.SetNodeLabelsForUpdate(nil)
	}

	if err := mgr.storage.CreateBox(box); err != nil {
		return errors.Trace(err)
	}

	return errors.Trace(mgr.boxCtrl.SetBox(box))
}

// GetBoxByName returns the specified box by name
func (mgr *Manager) GetBoxByName(name string) (*types.Box, error) {
	box, exist := mgr.storage.GetBoxByName(name)
	if !exist {
		return nil, errors.NotFoundf("box %s", name)
	}

	return box, nil
}

// GetBoxesByTag returns the specified boxes by tag
func (mgr *Manager) GetBoxesByTag(tag string) types.Boxes {
	return mgr.storage.GetBoxesByTag(tag)
}

// GetBox returns the specified box by name
func (mgr *Manager) GetBox(boxID int64) (*types.Box, error) {
	box, exist := mgr.storage.GetBox(boxID)
	if !exist {
		return nil, errors.NotFoundf("box %d", boxID)
	}

	return box, nil
}

// DeleteBox deletes box
func (mgr *Manager) DeleteBox(boxID int64) error {
	_, exist := mgr.storage.GetBox(boxID)
	if !exist {
		return errors.NotFoundf("box id %d", boxID)
	}

	err := mgr.boxCtrl.StopBox(boxID)
	if err != nil {
		return errors.Trace(err)
	}

	err = mgr.experimentCtrl.CancelExperiment(boxID)
	if err != nil && !strings.Contains(err.Error(), "already removed") {
		return errors.Trace(err)
	}

	err = mgr.experimentCtrl.DestroyExperimentsByBoxID(boxID)
	if err != nil {
		return errors.Trace(err)
	}

	return mgr.storage.DeleteBox(boxID)
}

// StopBox stops box
func (mgr *Manager) StopBox(boxID int64) error {
	err := mgr.boxCtrl.StopBox(boxID)
	if err != nil {
		return errors.Trace(err)
	}

	return mgr.storage.StopBox(boxID)
}

// RecoverBox recovers box
func (mgr *Manager) RecoverBox(boxID int64) error {
	box, exist := mgr.storage.GetBox(boxID)
	if !exist {
		return errors.NotFoundf("box %d", boxID)
	}

	box.State = types.BoxStateOnline

	err := mgr.storage.UpdateBox(box)
	if err != nil {
		return errors.Trace(err)
	}

	return errors.Trace(mgr.boxCtrl.SetBox(box))
}

// BoxWithSummary represents box with summary
type BoxWithSummary struct {
	*types.Box
	Summary *Summary `json:"summary"`
}

// Summary is box's experiments summary
type Summary struct {
	Experiments        []*ExperimentBrief `json:"experiments"`
	ResourceOccupation bool               `json:"resource_occupation"`
	TestInOrder        bool               `json:"test_in_order"`
	HasPending         bool               `json:"has_pending"`
}

// ExperimentBrief represents expriment brief infomation
type ExperimentBrief struct {
	ID         int64                 `json:"id"`
	Ctime      string                `json:"ctime"`
	State      types.ExperimentState `json:"status"`
	Stage      types.ExperimentStage `json:"stage"`
	CatState   types.CatState        `json:"cat_status"`
	Author     string                `json:"author"`
	Annotation string                `json:"annotation"`
	Destroyed  bool                  `json:"destroyed"`
}

// ExperimentBriefs is an array of ExperimentBrief
type ExperimentBriefs []*ExperimentBrief

// implement sort.Interface
func (experimentBriefs ExperimentBriefs) Len() int { return len(experimentBriefs) }
func (experimentBriefs ExperimentBriefs) Swap(i, j int) {
	experimentBriefs[i], experimentBriefs[j] = experimentBriefs[j], experimentBriefs[i]
}
func (experimentBriefs ExperimentBriefs) Less(i, j int) bool {
	return experimentBriefs[i].Ctime < experimentBriefs[j].Ctime
}

// GetBoxWithSummaryPagination lists box with summary
func (mgr *Manager) GetBoxWithSummaryPagination(
	boxCond *storage.BoxConditions,
	experimentCond *storage.ExperimentProcessConditions) ([]*BoxWithSummary, int64) {

	boxes, total := mgr.storage.ListBox(boxCond)
	experimentSummary := make(map[int64]ExperimentBriefs)
	resourceOccupation := make(map[int64]bool)
	hasPending := make(map[int64]bool)

	var boxIds []int64
	for _, box := range boxes {
		boxIds = append(boxIds, box.ID)
	}
	experimentCond.Box = boxIds

	var experiments types.ExperimentProcesses
	if len(boxIds) == 0 {
		experiments = types.ExperimentProcesses{}
	} else {
		experiments, _ = mgr.storage.ListExperimentProcess(experimentCond)
	}

	for _, experiment := range experiments {
		boxID := experiment.BoxID
		brief := getBrief(experiment)
		experimentSummary[boxID] = append(experimentSummary[boxID], brief)
		if !experiment.IsPending() && !experiment.IsDestroyed() {
			resourceOccupation[boxID] = true
		}

		if experiment.IsPending() {
			hasPending[boxID] = true
		}
	}

	var boxWithSummaries []*BoxWithSummary

	for _, box := range boxes {
		boxWithSummary := &BoxWithSummary{
			Box:     box,
			Summary: getBoxSummary(box, experimentSummary, resourceOccupation, hasPending),
		}
		boxWithSummaries = append(boxWithSummaries, boxWithSummary)
	}
	return boxWithSummaries, total
}

// CreateImmediatelyBox create immediately box
func (mgr *Manager) CreateImmediatelyBox(boxID int64) error {
	return errors.Trace(mgr.boxCtrl.Trigger(boxID))
}
