package strategy

import (
	"fmt"
	"math"
	"reflect"
	"time"

	"github.com/samber/lo"
	"gitlink.org.cn/cloudream/common/pkgs/bitmap"
	"gitlink.org.cn/cloudream/common/utils/math2"
	"gitlink.org.cn/cloudream/common/utils/sort2"
	"gitlink.org.cn/cloudream/jcs-pub/client/internal/metacache"
	"gitlink.org.cn/cloudream/jcs-pub/client/types"
	"gitlink.org.cn/cloudream/jcs-pub/common/consts"
	cortypes "gitlink.org.cn/cloudream/jcs-pub/coordinator/types"
)

type Request struct {
	Detail       types.ObjectDetail
	Range        math2.Range
	DestHub      cortypes.HubID      // 可以为0。此字段不为0时，DestLocation字段无意义。
	DestLocation cortypes.LocationID // 可以为0
}

type Strategy interface {
	GetDetail() types.ObjectDetail
}

// 直接下载完整对象
type DirectStrategy struct {
	Detail    types.ObjectDetail
	UserSpace types.UserSpaceDetail
}

func (s *DirectStrategy) GetDetail() types.ObjectDetail {
	return s.Detail
}

// 从指定对象重建对象
type ECReconstructStrategy struct {
	Detail     types.ObjectDetail
	Redundancy types.ECRedundancy
	Blocks     []types.ObjectBlock
	UserSpaces []types.UserSpaceDetail
}

func (s *ECReconstructStrategy) GetDetail() types.ObjectDetail {
	return s.Detail
}

type LRCReconstructStrategy struct {
	Detail     types.ObjectDetail
	Redundancy types.LRCRedundancy
	Blocks     []types.ObjectBlock
	Spaces     []types.UserSpaceDetail
}

func (s *LRCReconstructStrategy) GetDetail() types.ObjectDetail {
	return s.Detail
}

type Selector struct {
	cfg          Config
	storageMeta  *metacache.UserSpaceMeta
	hubMeta      *metacache.HubMeta
	connectivity *metacache.Connectivity
}

func NewSelector(cfg Config, storageMeta *metacache.UserSpaceMeta, hubMeta *metacache.HubMeta, connectivity *metacache.Connectivity) *Selector {
	return &Selector{
		cfg:          cfg,
		storageMeta:  storageMeta,
		hubMeta:      hubMeta,
		connectivity: connectivity,
	}
}

func (s *Selector) Select(req Request) (Strategy, error) {
	req2 := request2{
		Detail:       req.Detail,
		Range:        req.Range,
		DestLocation: req.DestLocation,
	}

	if req.DestHub != 0 {
		req2.DestHub = s.hubMeta.Get(req.DestHub)
	}

	switch red := req.Detail.Object.Redundancy.(type) {
	case *types.NoneRedundancy:
		return s.selectForNoneOrRep(req2)

	case *types.RepRedundancy:
		return s.selectForNoneOrRep(req2)

	case *types.ECRedundancy:
		return s.selectForEC(req2, *red)

	case *types.LRCRedundancy:
		return s.selectForLRC(req2, *red)
	}

	return nil, fmt.Errorf("unsupported redundancy type: %v of object %v", reflect.TypeOf(req.Detail.Object.Redundancy), req.Detail.Object.ObjectID)
}

type downloadSpaceInfo struct {
	Space        types.UserSpaceDetail
	ObjectPinned bool
	Blocks       []types.ObjectBlock
	Distance     float64
}

type downloadBlock struct {
	Space types.UserSpaceDetail
	Block types.ObjectBlock
}

type request2 struct {
	Detail       types.ObjectDetail
	Range        math2.Range
	DestHub      *cortypes.Hub
	DestLocation cortypes.LocationID
}

func (s *Selector) selectForNoneOrRep(req request2) (Strategy, error) {
	sortedStgs := s.sortDownloadStorages(req)
	if len(sortedStgs) == 0 {
		return nil, fmt.Errorf("no storage available for download")
	}

	_, blks := s.getMinReadingBlockSolution(sortedStgs, 1)
	if len(blks) == 0 {
		return nil, fmt.Errorf("no block available for download")
	}

	return &DirectStrategy{
		Detail:    req.Detail,
		UserSpace: sortedStgs[0].Space,
	}, nil
}

func (s *Selector) selectForEC(req request2, red types.ECRedundancy) (Strategy, error) {
	sortedStgs := s.sortDownloadStorages(req)
	if len(sortedStgs) == 0 {
		return nil, fmt.Errorf("no storage available for download")
	}

	bsc, blocks := s.getMinReadingBlockSolution(sortedStgs, red.K)
	osc, stg := s.getMinReadingObjectSolution(sortedStgs, red.K)

	if bsc < osc {
		bs := make([]types.ObjectBlock, len(blocks))
		ss := make([]types.UserSpaceDetail, len(blocks))
		for i, b := range blocks {
			bs[i] = b.Block
			ss[i] = b.Space
		}

		return &ECReconstructStrategy{
			Detail:     req.Detail,
			Redundancy: red,
			Blocks:     bs,
			UserSpaces: ss,
		}, nil
	}

	// bsc >= osc，如果osc是MaxFloat64，那么bsc也一定是，也就意味着没有足够块来恢复文件
	if osc == math.MaxFloat64 {
		return nil, fmt.Errorf("no enough blocks to reconstruct the object %v , want %d, get only %d", req.Detail.Object.ObjectID, red.K, len(blocks))
	}

	return &DirectStrategy{
		Detail:    req.Detail,
		UserSpace: stg,
	}, nil
}

func (s *Selector) selectForLRC(req request2, red types.LRCRedundancy) (Strategy, error) {
	sortedStgs := s.sortDownloadStorages(req)
	if len(sortedStgs) == 0 {
		return nil, fmt.Errorf("no storage available for download")
	}

	var blocks []downloadBlock
	selectedBlkIdx := make(map[int]bool)
	for _, stg := range sortedStgs {
		for _, b := range stg.Blocks {
			if b.Index >= red.M() || selectedBlkIdx[b.Index] {
				continue
			}
			blocks = append(blocks, downloadBlock{
				Space: stg.Space,
				Block: b,
			})
			selectedBlkIdx[b.Index] = true
		}
	}
	if len(blocks) < red.K {
		return nil, fmt.Errorf("not enough blocks to download lrc object")
	}

	bs := make([]types.ObjectBlock, len(blocks))
	ss := make([]types.UserSpaceDetail, len(blocks))
	for i, b := range blocks {
		bs[i] = b.Block
		ss[i] = b.Space
	}

	return &LRCReconstructStrategy{
		Detail:     req.Detail,
		Redundancy: red,
		Blocks:     bs,
		Spaces:     ss,
	}, nil
}

func (s *Selector) sortDownloadStorages(req request2) []*downloadSpaceInfo {
	var spaceIDs []types.UserSpaceID
	for _, id := range req.Detail.PinnedAt {
		if !lo.Contains(spaceIDs, id) {
			spaceIDs = append(spaceIDs, id)
		}
	}
	for _, b := range req.Detail.Blocks {
		if !lo.Contains(spaceIDs, b.UserSpaceID) {
			spaceIDs = append(spaceIDs, b.UserSpaceID)
		}
	}

	downloadSpaceMap := make(map[types.UserSpaceID]*downloadSpaceInfo)
	for _, id := range req.Detail.PinnedAt {
		storage, ok := downloadSpaceMap[id]
		if !ok {
			mod := s.storageMeta.Get(id)
			if mod == nil || mod.MasterHub == nil {
				continue
			}

			storage = &downloadSpaceInfo{
				Space:        *mod,
				ObjectPinned: true,
				Distance:     s.getStorageDistance(req, *mod),
			}
			downloadSpaceMap[id] = storage
		}

		storage.ObjectPinned = true
	}

	for _, b := range req.Detail.Blocks {
		space, ok := downloadSpaceMap[b.UserSpaceID]
		if !ok {
			mod := s.storageMeta.Get(b.UserSpaceID)
			if mod == nil || mod.MasterHub == nil {
				continue
			}

			space = &downloadSpaceInfo{
				Space:    *mod,
				Distance: s.getStorageDistance(req, *mod),
			}
			downloadSpaceMap[b.UserSpaceID] = space
		}

		space.Blocks = append(space.Blocks, b)
	}

	return sort2.Sort(lo.Values(downloadSpaceMap), func(left, right *downloadSpaceInfo) int {
		return sort2.Cmp(left.Distance, right.Distance)
	})
}

func (s *Selector) getStorageDistance(req request2, src types.UserSpaceDetail) float64 {
	if req.DestHub != nil {
		if src.MasterHub.HubID == req.DestHub.HubID {
			return consts.StorageDistanceSameStorage
		}

		if src.MasterHub.LocationID == req.DestHub.LocationID {
			return consts.StorageDistanceSameLocation
		}

		latency := s.connectivity.Get(src.MasterHub.HubID, req.DestHub.HubID)
		if latency == nil || *latency > time.Duration(float64(time.Millisecond)*s.cfg.HighLatencyHubMs) {
			return consts.HubDistanceHighLatencyHub
		}

		return consts.StorageDistanceOther
	}

	if req.DestLocation != 0 {
		if src.MasterHub.LocationID == req.DestLocation {
			return consts.StorageDistanceSameLocation
		}
	}

	return consts.StorageDistanceOther
}

func (s *Selector) getMinReadingBlockSolution(sortedStgs []*downloadSpaceInfo, k int) (float64, []downloadBlock) {
	gotBlocksMap := bitmap.Bitmap64(0)
	var gotBlocks []downloadBlock
	dist := float64(0.0)
	for _, n := range sortedStgs {
		for _, b := range n.Blocks {
			if !gotBlocksMap.Get(b.Index) {
				gotBlocks = append(gotBlocks, downloadBlock{
					Space: n.Space,
					Block: b,
				})
				gotBlocksMap.Set(b.Index, true)
				dist += n.Distance
			}

			if len(gotBlocks) >= k {
				return dist, gotBlocks
			}
		}
	}

	return math.MaxFloat64, gotBlocks
}

func (s *Selector) getMinReadingObjectSolution(sortedStgs []*downloadSpaceInfo, k int) (float64, types.UserSpaceDetail) {
	dist := math.MaxFloat64
	var downloadSpace types.UserSpaceDetail
	for _, n := range sortedStgs {
		if n.ObjectPinned && float64(k)*n.Distance < dist {
			dist = float64(k) * n.Distance
			stg := n.Space
			downloadSpace = stg
		}
	}

	return dist, downloadSpace
}
