package com.tzld.piaoquan.recommend.server.service.score;

import com.tzld.piaoquan.recommend.feature.domain.video.base.UserFeature;
import com.tzld.piaoquan.recommend.server.common.base.RankItem;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;


@Slf4j
public class ScorerPipeline {
    public static final int corePoolSize = 128;
    public static final int SCORE_TIME_OUT = 400;
    public static final Logger LOGGER = LoggerFactory.getLogger(ScorerPipeline.class);
    public static final ExecutorService executorService = Executors.newFixedThreadPool(corePoolSize);

    public List<AbstractScorer> scorers;

    public ScorerPipeline(List<AbstractScorer> scorers) {
        this.scorers = scorers;
    }

    /**
     * scoring
     *
     * @return
     */
    public List<RankItem> scoring(final ScoreParam param,
                                  final UserFeature userFeature,
                                  final List<RankItem> rankItems) {

        if (CollectionUtils.isEmpty(scorers)) {
            log.error("scorers is empty");
            return rankItems;
        }
        List<RankItem> items = rankItems;

        for (final AbstractScorer scorer : scorers) {
            if (!scorer.isEnable()) {
                continue;
            }

            final int beforeSize = rankItems.size();
            final long startTime = System.currentTimeMillis();

            String fullScorerName = scorer.getScorerConfigInfo().getScorerName();
            String[] scorerNames = fullScorerName.split("\\.");
            final String scorerName = scorerNames.length > 0 ? scorerNames[scorerNames.length - 1] : fullScorerName;

            final List<RankItem> scoreRankerItems = items;
            Callable<List<RankItem>> callable = () -> scorer.scoring(param, userFeature, scoreRankerItems);

            // execute score use thread to protected score worst time
            List<RankItem> scoredItems = new ArrayList<RankItem>();
            try {
                List<Future<List<RankItem>>> futures = executorService.invokeAll(Arrays.asList(callable), SCORE_TIME_OUT, TimeUnit.MILLISECONDS);
                for (Future<List<RankItem>> future : futures) {
                    try {
                        if (future.isDone() && !future.isCancelled() && future.get() != null) {
                            scoredItems.addAll(future.get());
                        } else {
                            future.cancel(true);
                            LOGGER.error("score task is cancelled, scorename [{}] fail items [{}]",
                                    new Object[]{scorerName, scoreRankerItems.size()});
                        }
                    } catch (Exception e) {
                        LOGGER.error("thread pool exception scorename [{}], exception [{}]",
                                new Object[]{scorerName, ExceptionUtils.getFullStackTrace(e)});
                    }
                }
            } catch (Exception e) {
                LOGGER.error("thread pool exception uid [{}] scorename [{}], exception [{}]",
                        new Object[]{scorerName, ExceptionUtils.getFullStackTrace(e)});
            }

            //  变更item
            if (CollectionUtils.isNotEmpty(scoreRankerItems)) {
                items = scoreRankerItems;
            } else {
                items = new ArrayList<>(items);
            }

            int position = 0;
            for (RankItem item : items) {
                item.getRankerIndex().put(scorerName, position++);
                item.getRankerScore().put(scorerName, item.getScore());
            }

            //
            long spentTime = System.currentTimeMillis() - startTime;
            LOGGER.debug("after scorer [{}], spentTime [{}], before size [{}], remaining size [{}]",
                    new Object[]{scorerName, spentTime, beforeSize, scoreRankerItems.size()});
        }

        int position = 0;
        for (RankItem item : items) {
            item.getRankerIndex().put("finalScore", position++);
            item.getRankerScore().put("finalScore", item.getScore());
        }

        return items;
    }

    public List<RankItem> scoring(final Map<String, Float> sceneFeatureMap,
                                  final Map<String, Float> userFeatureMap,
                                  final List<RankItem> rankItems) {

        if (CollectionUtils.isEmpty(scorers)) {
            log.error("scorers is empty");
            return rankItems;
        }
        List<RankItem> items = rankItems;

        for (final AbstractScorer scorer : scorers) {
            if (!scorer.isEnable()) {
                continue;
            }

            final int beforeSize = rankItems.size();
            final long startTime = System.currentTimeMillis();

            String fullScorerName = scorer.getScorerConfigInfo().getScorerName();
            String[] scorerNames = fullScorerName.split("\\.");
            final String scorerName = scorerNames.length > 0 ? scorerNames[scorerNames.length - 1] : fullScorerName;

            final List<RankItem> scoreRankerItems = items;
            Callable<List<RankItem>> callable = () -> scorer.scoring(sceneFeatureMap, userFeatureMap, scoreRankerItems);

            // execute score use thread to protected score worst time
            List<RankItem> scoredItems = new ArrayList<RankItem>();
            try {
                List<Future<List<RankItem>>> futures = executorService.invokeAll(Arrays.asList(callable), SCORE_TIME_OUT, TimeUnit.MILLISECONDS);
                for (Future<List<RankItem>> future : futures) {
                    try {
                        if (future.isDone() && !future.isCancelled() && future.get() != null) {
                            scoredItems.addAll(future.get());
                        } else {
                            future.cancel(true);
                            LOGGER.error("score task is cancelled, scorename [{}] fail items [{}]",
                                    new Object[]{scorerName, scoreRankerItems.size()});
                        }
                    } catch (Exception e) {
                        LOGGER.error("thread pool exception scorename [{}], exception [{}]",
                                new Object[]{scorerName, ExceptionUtils.getFullStackTrace(e)});
                    }
                }
            } catch (Exception e) {
                LOGGER.error("thread pool exception uid [{}] scorename [{}], exception [{}]",
                        new Object[]{scorerName, ExceptionUtils.getFullStackTrace(e)});
            }

            //  变更item
            if (CollectionUtils.isNotEmpty(scoreRankerItems)) {
                items = scoreRankerItems;
            } else {
                items = new ArrayList<>(items);
            }

            int position = 0;
            for (RankItem item : items) {
                item.getRankerIndex().put(scorerName, position++);
                if (scorerName.equals("finalScore")) {
                    item.getRankerScore().put(scorerName, item.getScore());
                }
            }

            //
            long spentTime = System.currentTimeMillis() - startTime;
            LOGGER.debug("after scorer [{}], spentTime [{}], before size [{}], remaining size [{}]",
                    new Object[]{scorerName, spentTime, beforeSize, scoreRankerItems.size()});
        }

        int position = 0;
        for (RankItem item : items) {
            item.getRankerIndex().put("finalScore", position++);
            item.getRankerScore().put("finalScore", item.getScore());
        }

        return items;
    }

    public List<RankItem> scoring(final Map<String, Float> sceneFeatureMap,
                                  final Map<String, Float> userFeatureMap,
                                  final Map<String, Float> norUserFeatureMap,
                                  final List<RankItem> rankItems) {

        if (CollectionUtils.isEmpty(scorers)) {
            log.error("scorers is empty");
            return rankItems;
        }
        List<RankItem> items = rankItems;

        Map<String, Float> userFeatMap;
        for (final AbstractScorer scorer : scorers) {
            if (!scorer.isEnable()) {
                continue;
            }
            userFeatMap = userFeatureMap;
            if (scorer.scorerConfigInfo.getModelPath().contains("_nor.tar.gz")) {
                userFeatMap = norUserFeatureMap;
            }

            final int beforeSize = rankItems.size();
            final long startTime = System.currentTimeMillis();

            String fullScorerName = scorer.getScorerConfigInfo().getScorerName();
            String[] scorerNames = fullScorerName.split("\\.");
            final String scorerName = scorerNames.length > 0 ? scorerNames[scorerNames.length - 1] : fullScorerName;

            final List<RankItem> scoreRankerItems = items;
            final Map<String, Float> tmpUserFeatMap = userFeatMap;
            Callable<List<RankItem>> callable = () -> scorer.scoring(sceneFeatureMap, tmpUserFeatMap, scoreRankerItems);

            // execute score use thread to protected score worst time
            List<RankItem> scoredItems = new ArrayList<RankItem>();
            try {
                List<Future<List<RankItem>>> futures = executorService.invokeAll(Arrays.asList(callable), SCORE_TIME_OUT, TimeUnit.MILLISECONDS);
                for (Future<List<RankItem>> future : futures) {
                    try {
                        if (future.isDone() && !future.isCancelled() && future.get() != null) {
                            scoredItems.addAll(future.get());
                        } else {
                            future.cancel(true);
                            LOGGER.error("score task is cancelled, scorename [{}] fail items [{}]",
                                    new Object[]{scorerName, scoreRankerItems.size()});
                        }
                    } catch (Exception e) {
                        LOGGER.error("thread pool exception scorename [{}], exception [{}]",
                                new Object[]{scorerName, ExceptionUtils.getFullStackTrace(e)});
                    }
                }
            } catch (Exception e) {
                LOGGER.error("thread pool exception uid [{}] scorename [{}], exception [{}]",
                        new Object[]{scorerName, ExceptionUtils.getFullStackTrace(e)});
            }

            //  变更item
            if (CollectionUtils.isNotEmpty(scoreRankerItems)) {
                items = scoreRankerItems;
            } else {
                items = new ArrayList<>(items);
            }

            int position = 0;
            for (RankItem item : items) {
                item.getRankerIndex().put(scorerName, position++);
                if (scorerName.equals("finalScore")) {
                    item.getRankerScore().put(scorerName, item.getScore());
                }
            }

            //
            long spentTime = System.currentTimeMillis() - startTime;
            LOGGER.debug("after scorer [{}], spentTime [{}], before size [{}], remaining size [{}]",
                    new Object[]{scorerName, spentTime, beforeSize, scoreRankerItems.size()});
        }

        int position = 0;
        for (RankItem item : items) {
            item.getRankerIndex().put("finalScore", position++);
            item.getRankerScore().put("finalScore", item.getScore());
        }

        return items;
    }
}
