package core

import (
	"fmt"
	"github.com/guo-shaoge/tiflash-autoscale/pkg/meta"
	"go.uber.org/zap"
	"sync"
)

type basePool struct {
	poolName string

	mu      *sync.RWMutex
	podsMap meta.PodsMap
	// NOTE: only for WarmupPool
	newWarmupPodCV *sync.Cond
}

// NewBasePool return basePool
func newBasePool(poolName string) *basePool {
	res := basePool{
		poolName: poolName,
	}
	res.mu = &sync.RWMutex{}
	res.podsMap = make(meta.PodsMap)
	res.newWarmupPodCV = sync.NewCond(res.mu)
	return &res
}

// GetPoolName return pool name
func (b *basePool) GetPoolName() string {
	return b.poolName
}

// GetPodsCopy shallow copy podsMap
// called by PodMgr.UpdateMetricsByCopy(first copy pods, then update its metrics)
func (b *basePool) GetPodsCopy() meta.PodsMap {
	b.mu.RLock()
	defer b.mu.RUnlock()
	res := make(meta.PodsMap, len(b.podsMap))
	for k, v := range b.podsMap {
		res[k] = v
	}
	return res
}

func (b *basePool) GetPodsMapWithoutLock(filter meta.PodsFilter) meta.PodsMap {
	if filter == nil {
		return b.podsMap
	}

	var res meta.PodsMap
	for podName, podDesc := range b.podsMap {
		if filter(podDesc) {
			res[podName] = podDesc
		}
	}
	return res
}

// GetPodsNum return pods num
func (b *basePool) GetPodsNum(filter meta.PodsFilter) int {
	b.mu.RLock()
	defer b.mu.RUnlock()

	var cnt int
	for _, podDesc := range b.podsMap {
		if filter == nil || filter(podDesc) {
			cnt++
		}
	}
	return cnt
}

// RemovePodsByNum remove n pods.
// called for WarmupPool to remove pods.
func (b *basePool) RemovePodsByNum(num uint, filter meta.PodsFilter) []*meta.PodDesc {
	b.mu.Lock()
	defer b.mu.Unlock()

	return b.RemovePodsByNumWithoutLock(num, filter)
}

func (b *basePool) RemovePodsByNumWithoutLock(num uint, filter meta.PodsFilter) []*meta.PodDesc {
	res := make([]*meta.PodDesc, 0, num)

	for podName, podDesc := range b.podsMap {
		if len(res) >= int(num) {
			break
		}
		if filter == nil || filter(podDesc) {
			res = append(res, podDesc)
			delete(b.podsMap, podName)
		}
	}
	return res
}

// RemovePodsByFilter remove pods.
func (b *basePool) RemovePodsByFilter(filter meta.PodsFilter) []*meta.PodDesc {
	res := make([]*meta.PodDesc, 0, 5)

	b.mu.Lock()
	defer b.mu.Unlock()

	for podName, podDesc := range b.podsMap {
		if filter == nil || filter(podDesc) {
			res = append(res, podDesc)
			delete(b.podsMap, podName)
		}
	}
	return res
}

// RemovePodByName remove n named pods
// called by PodMgr.DelPodDesc(called by k8sMgr) to do real deletion.
func (b *basePool) RemovePodsByName(pods []string) int {
	var delCnt int

	b.mu.Lock()
	defer b.mu.Unlock()

	for _, podName := range pods {
		_, ok := b.podsMap[podName]
		if ok {
			delete(b.podsMap, podName)
			delCnt++
		}
	}
	return delCnt
}

// AddPods add pods into some pool
// called by
//  1. PodMgr.AddPodDesc(called by k8sMgr.watcher) to add pods to WarmupPool
//  2. PodMgr.podScannerLoop(to add pods to WarmupPool and UnassigningPool)
//  3. PodMgr.syncRemovePodsFromWarmupPool(to add pods to SharePool/VIPPool)
func (b *basePool) AddPods(pods []*meta.PodDesc) (addCnt uint, msg string) {
	b.mu.Lock()
	defer b.mu.Unlock()
	for _, podDesc := range pods {
		podName := podDesc.GetName()
		if _, ok := b.podsMap[podName]; ok {
			msg += fmt.Sprintf("%v;", podName)
			continue
		}
		b.podsMap[podName] = podDesc
		addCnt++
	}
	return addCnt, msg
}

// GetPodsByName get pod by name
// called by PodMgr.UpdatePodDesc (to update podDesc)
func (b *basePool) GetPodsByName(names []string) (res []*meta.PodDesc) {
	b.mu.RLock()
	defer b.mu.RUnlock()
	for _, name := range names {
		if podDesc, ok := b.podsMap[name]; ok {
			res = append(res, podDesc)
		}
	}
	return res
}

// scan all pods for SharePool/VIPPool, handle pod according to each status.
// called by PodMgr.podScannerLoop
// only for SharePool or VIPPool
func (b *basePool) scanUsablePool(logger *zap.Logger) (podsToUnassign meta.PodsMap, podsToWarmupPool meta.PodsMap) {
	podsToUnassign = make(meta.PodsMap)
	podsToWarmupPool = make(meta.PodsMap)
	b.mu.Lock()
	defer b.mu.Unlock()
	for podName, podDesc := range b.podsMap {
		status := podDesc.GetStatus()
		switch status {
		case meta.PodStatusRunning, meta.PodStatusSharing:
			// nop
		case meta.PodStatusReadyUnassign:
			logger.Info("got ReadyUnassign pod, will move to UnassigningPool",
				zap.String("pod", podName), zap.String("status", podDesc.GetStatusStr()))
			podsToUnassign[podName] = podDesc
			delete(b.podsMap, podName)
		case meta.PodStatusLogicalError:
			logger.Info("got LogicalError pod, will move to UnassigningPool",
				zap.String("pod", podName), zap.String("status", podDesc.GetStatusStr()))
			podsToUnassign[podName] = podDesc
			delete(b.podsMap, podName)
		case meta.PodStatusDeleted:
			logger.Info("got Deleted pod, will delete",
				zap.String("pod", podName), zap.String("status", podDesc.GetStatusStr()))
			delete(b.podsMap, podName)
		case meta.PodStatusPending:
			logger.Info("got Pending pod, move to WarmupPool",
				zap.String("pod", podName), zap.String("status", podDesc.GetStatusStr()))
			podsToWarmupPool[podName] = podDesc
			delete(b.podsMap, podName)
		case meta.PodStatusUnassigning, meta.PodStatusUnassigned, meta.PodStatusNeedDelete:
			// no idea how to handle, will be move to UnassigningPool next round.
			logger.Error("unexpected pods in SharePool/VIPPool, set to Error and move to UnassigningPool",
				zap.Any("pool", b.GetPoolName()),
				zap.Any("pod", podName),
				zap.Any("status", podDesc.GetStatusStr()))
			podsToUnassign[podName] = podDesc
			podDesc.ToLogicalError()
			delete(b.podsMap, podName)
		default:
			panic(fmt.Sprintf("unexpected pod status: %v", status))
		}
	}
	return podsToUnassign, podsToWarmupPool
}
