package service

import (
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/algorithm"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/algorithm/eas"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/algorithm/response"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/context"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/log"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/module"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/recconf"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/service/feature"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/service/rank"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/sort"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/utils"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec/utils/ast"
)

var generalRanks = make(map[string]*GeneralRank)

// GetGeneralRank return the GeneralRank by the context
// If not found, return nil
func GetGeneralRank(context *context.RecommendContext) *GeneralRank {
	scene := context.GetParameter("scene").(string)
	// find rank config
	var rankConfig recconf.GeneralRankConfig
	found := false
	if context.ExperimentResult != nil {
		rankconf := context.ExperimentResult.GetExperimentParams().Get("generalRankConf", "")
		if rankconf != "" {
			d, _ := json.Marshal(rankconf)
			if err := json.Unmarshal(d, &rankConfig); err == nil {
				found = true
			}
		}
	}
	if !found {
		if rankConfigs, ok := recconf.Config.GeneralRankConfs[scene]; ok {
			rankConfig = rankConfigs
			found = true
		}
	}

	// not find any generalRankconf, return  nil
	if !found {
		return nil
	}
	d, _ := json.Marshal(rankConfig)
	id := scene + "#" + utils.Md5(string(d))
	if r, ok := generalRanks[scene]; ok && id == r.id {
		return r
	}

	generalRank, err := NewGeneralRankWithConfig(scene, rankConfig)
	if err != nil {
		log.Error(fmt.Sprintf("create generalRank error, err:%v", err))
		return nil
	}

	generalRank.id = id
	generalRanks[scene] = generalRank

	return generalRank
}

type GeneralRank struct {
	featureService *FeatureService
	retainNum      int
	batchCount     int
	id             string
	config         recconf.GeneralRankConfig
	sort           *sort.ItemRankScoreSort
	scene          string
}

func NewGeneralRankWithConfig(scene string, config recconf.GeneralRankConfig) (*GeneralRank, error) {
	preRank := GeneralRank{
		featureService: &FeatureService{
			featureSceneMap:      make(map[string][]*feature.Feature),
			featureSceneAsyncMap: make(map[string]bool),
			featureFuncMap:       make(map[string]FeatureFunc),
			loadFeatureFuncMap:   make(map[string]LoadFeatureFunc),
		},
		retainNum:  config.RetainNum,
		batchCount: config.BatchCount,
		config:     config,
		sort:       sort.NewItemRankScoreSort(),
		scene:      scene,
	}

	if preRank.batchCount == 0 {
		preRank.batchCount = 100
	}

	if preRank.retainNum < 0 {
		return nil, fmt.Errorf("retainNum's value is error, val:%d", preRank.retainNum)
	}
	var features []*feature.Feature
	for _, conf := range config.FeatureLoadConfs {
		f := feature.LoadWithConfig(conf)
		features = append(features, f)
	}

	preRank.featureService.featureSceneAsyncMap[scene] = true
	preRank.featureService.featureSceneMap[scene] = features

	return &preRank, nil
}

// Rank GeneralRank for items , return the items for Rank
// 1. first load user features
// 2. construct data use AlgoDataGenerator, if processor is easyrec, create easyrecRequest
// 3. use goroutines to invoke eas module
// 4. sort the items by score and return the count by the retainNum
func (r *GeneralRank) Rank(user *module.User, items []*module.Item, context *context.RecommendContext) (ret []*module.Item) {

	start := time.Now()

	// get user feature by the FeatureService
	r.featureService.LoadFeaturesForGeneralRank(user, items, context)
	if context.Debug {
		fmt.Println(user)
	}

	algoDataList := make([]rank.IAlgoData, 0)
	i := 0
	algoGenerator := rank.CreateAlgoDataGenerator(r.config.RankConf.Processor)
	var userFeatures map[string]interface{}

	if r.config.RankConf.Processor == eas.Eas_Processor_EASYREC {
		userFeatures = user.MakeUserFeatures2()
	} else {
		userFeatures = user.MakeUserFeatures()
	}

	emptyFeatures := make(map[string]interface{}, 0)
	for _, item := range items {
		if r.config.RankConf.Processor == eas.Eas_Processor_EASYREC {
			algoGenerator.AddFeatures(item, emptyFeatures, userFeatures)
		} else {
			features := item.GetFeatures()
			algoGenerator.AddFeatures(item, features, userFeatures)
		}

		i++
		if i%r.batchCount == 0 {
			algoData := algoGenerator.GeneratorAlgoData()
			algoDataList = append(algoDataList, algoData)
		}
	}

	if algoGenerator.HasFeatures() {
		algoData := algoGenerator.GeneratorAlgoData()
		algoDataList = append(algoDataList, algoData)
	}

	if len(algoDataList) == 0 {
		return
	}

	requestCh := make(chan rank.IAlgoData, len(algoDataList))
	responseCh := make(chan rank.IAlgoData, len(algoDataList))
	defer close(requestCh)
	defer close(responseCh)

	for _, data := range algoDataList {
		requestCh <- data
	}

	rankConfig := r.config.RankConf

	gCount := len(algoDataList)
	for i := 0; i < gCount; i++ {
		go func() {
			algoData := <-requestCh

			var wg sync.WaitGroup
			for _, algoName := range rankConfig.RankAlgoList {
				wg.Add(1)
				go func(algo string) {
					defer wg.Done()
					algoResponses, err := algorithm.Run(algo, algoData.GetFeatures())
					if err != nil {
						log.Error(fmt.Sprintf("requestId=%s\terror=run algorithm error(%v)", context.RecommendId, err))
						algoData.SetError(err)
					} else {
						if result, ok := algoResponses.([]response.AlgoResponse); ok {
							algoData.SetAlgoResult(algo, result)
						}
					}
				}(algoName)
			}
			wg.Wait()
			responseCh <- algoData
		}()
	}

	for i := 0; i < gCount; i++ {
		algoData := <-responseCh
		if algoData.Error() == nil && algoData.GetAlgoResult() != nil {
			for name, algoResult := range algoData.GetAlgoResult() {
				itemList := algoData.GetItems()
				for j := 0; j < len(algoResult) && j < len(itemList); j++ {
					if algoResult[j].GetModuleType() {
						scoreMap := algoResult[j].GetScoreMap()
						for k, v := range scoreMap {
							itemList[j].AddAlgoScore(name+"_"+k, v)
						}
					} else {
						itemList[j].AddAlgoScore(name, algoResult[j].GetScore())
					}
				}
			}
		}

		if rankConfig.RankScore != "" {
			itemList := algoData.GetItems()
			for k, _ := range itemList {
				itemList[k].Score = ast.ExprSourceResult(rankConfig.RankScore, itemList[k])
			}
		}
	}

	// item sort
	sortData := sort.SortData{Data: items, Context: context}

	r.sort.Sort(&sortData)
	ret = sortData.Data.([]*module.Item)

	if r.retainNum > 0 && len(ret) > r.retainNum {
		ret = ret[:r.retainNum]
	}

	log.Info(fmt.Sprintf("requestId=%s\tmodule=GeneralRank\tcost=%d", context.RecommendId, utils.CostTime(start)))
	return
}

func LoadWithConfig(config *recconf.RecommendConfig) {

	for sceneName, rankConfig := range config.GeneralRankConfs {
		if generalRank, err := NewGeneralRankWithConfig(sceneName, rankConfig); err == nil {
			d, _ := json.Marshal(rankConfig)
			generalRank.id = sceneName + "#" + utils.Md5(string(d))
			generalRanks[sceneName] = generalRank
		} else {
			panic(err)
		}
	}
}
