package com.tzld.piaoquan.recommend.server.framework.recaller;


import com.google.common.base.Stopwatch;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.tzld.piaoquan.recommend.server.common.base.RankItem;
import com.tzld.piaoquan.recommend.server.framework.candidiate.*;
import com.tzld.piaoquan.recommend.server.framework.common.User;
import com.tzld.piaoquan.recommend.server.framework.recaller.provider.QueueProvider;
import com.tzld.piaoquan.recommend.server.gen.recommend.RecommendRequest;
import com.tzld.piaoquan.recommend.server.util.CommonCollectionUtils;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.stream.Collectors;


public class BaseRecaller<Video> {

    private static final Logger LOGGER = LoggerFactory.getLogger(BaseRecaller.class);
    private static final long DEFAULT_QUEUE_LOAD_TIMEOUT = 1500; // ms
    private static final long DEFAULT_PARALLEL_FILTER_TIMEOUT = 1500; // ms

    private static final String FILTER_CONF = "filter_config.conf"; // ms
    private static final ExecutorService filterExecutorService = new ThreadPoolExecutor(128, 128,
            0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1000), r -> new Thread(r, "filterExecutorService"),
            new ThreadPoolExecutor.CallerRunsPolicy());

    private static final ExecutorService fetchQueueExecutorService = new ThreadPoolExecutor(128, 128,
            0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1000), r -> new Thread(r, "fetchQueueExecutorService"),
            new ThreadPoolExecutor.CallerRunsPolicy());
    private final QueueProvider<Video> queueProvider;

    private static final FilterConfig filterConfig;

    private final long QUEUE_LOAD_TIMEOUT;

    static {
        filterConfig = new FilterConfig(FILTER_CONF);
    }

    public BaseRecaller(QueueProvider<Video> queueProvider) {
        this(queueProvider, DEFAULT_QUEUE_LOAD_TIMEOUT);
    }

    public BaseRecaller(QueueProvider<Video> queueProvider,
                        long queueLoadTimeout) {
        this.queueProvider = queueProvider;
        this.QUEUE_LOAD_TIMEOUT = queueLoadTimeout;
    }


    public BaseRecaller(QueueProvider<Video> queueProvider,
                        FilterConfig filterConfig,
                        long queueLoadTimeout) {
        this.queueProvider = queueProvider;
        this.QUEUE_LOAD_TIMEOUT = queueLoadTimeout;
    }


    public String extractItemId(Entry<Video> entry) {
        return entry.id;
    }


    public boolean isValidItem(Video item) {
        return item != null;
    }

    /**
     * 把queue中的entry, 补充candidiate信息，放入RankItem中
     *
     * @param entries
     * @param candidate
     * @return
     */
    private List<RankItem> toHits(final Iterable<Entry<Video>> entries, final Candidate candidate) {

        List<RankItem> result = new ArrayList<RankItem>();
        for (Entry entry : entries) {
            RankItem item = new RankItem();
            item.setId(extractItemId(entry));
            item.putRankerScore("L3Score", (Double) entry.scores.get("ordering"));
            item.setQueue(candidate.getMergeQueueName()); // merge queue

            CandidateInfo candidateInfo = new CandidateInfo();
            candidateInfo.setCandidateQueueName(candidate.getCandidateKey());
            candidateInfo.setCandidate(candidate);

            item.setCandidateInfo(candidateInfo);
            item.addToCandidateInfoList(candidateInfo);
            result.add(item);
        }
        return result;
    }


    // 读取redis中的数据放入queue中
    public Map<Candidate, Queue<Video>> loadQueues(List<Candidate> candidates) {
        // update queueName
        Iterable<Candidate> updateCandidates = FluentIterable.from(candidates).transform(candidate -> {
            try {
                long ttl = QueueName.DEFAULT_LOCAL_CACHE_TTL;
                candidate.setCandidateQueueName(QueueName.fromString(candidate.getCandidateKey(), ttl));
            } catch (Exception e) {
                candidate.setCandidateQueueName(null);
                LOGGER.error("error parse QueueName [{}]", candidate.getCandidateKey());
            }
            return candidate;
        });

        // parse queues
        Iterable<QueueName> queueNames = FluentIterable.from(updateCandidates).transform(candidate -> candidate.getCandidateQueueName());

        // parallel load queues, redis 或者缓存获取index
        Map<QueueName, Queue<Video>> queues = Maps.newConcurrentMap();
        try {
            // 格式
            queues = queueProvider.loads(Lists.newArrayList(queueNames), QUEUE_LOAD_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            LOGGER.error("load queue occur error [{}]", ExceptionUtils.getFullStackTrace(e));
        }

        // parse candidate map
        Map<Candidate, Queue<Video>> candidateQueueMap = Maps.newConcurrentMap();
        for (Candidate candidate : updateCandidates) {
            QueueName name = candidate.getCandidateQueueName();
            Queue<Video> entries = queues.get(name);
            if (queues.containsKey(name) && entries != null) {
                candidateQueueMap.put(candidate, entries);
            }
        }
        return candidateQueueMap;
    }


    /**
     * recall
     *
     * @param requestData
     * @param user
     * @param recallCandidates
     * @return
     */
    public List<RankItem> recalling(final RecommendRequest requestData, final User user, List<Candidate> recallCandidates) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        stopwatch.reset().start();
        // load from redis
        List<Callable<Map<Candidate, Queue<Video>>>> fetchQueueCalls = Lists.newArrayList();
        fetchQueueCalls.add(() -> {
            boolean isFromRedis = true;
            return obtainQueue(recallCandidates, requestData, user, isFromRedis);
        });

        // 多线程执行load
        List<Future<Map<Candidate, Queue<Video>>>> fetchQueueFutures = null;
        try {
            fetchQueueFutures = fetchQueueExecutorService.invokeAll(fetchQueueCalls, DEFAULT_QUEUE_LOAD_TIMEOUT,
                    TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            LOGGER.error("[fetch queue error] inter fail: {}", ExceptionUtils.getStackTrace(e));
        } catch (Exception e) {
            LOGGER.error("[fetch queue error] ex", ExceptionUtils.getStackTrace(e));
        }

        Map<Candidate, Queue<Video>> candidateQueueMap = Maps.newHashMap();
        if (CollectionUtils.isNotEmpty(fetchQueueFutures)) {
            for (Future<Map<Candidate, Queue<Video>>> future : fetchQueueFutures) {
                if (future.isDone() && !future.isCancelled()) {
                    Map<Candidate, Queue<Video>> result = null;
                    try {
                        result = future.get();
                    } catch (InterruptedException e) {
                        LOGGER.error("[fetch queue error] InterruptedException {}", ExceptionUtils.getStackTrace(e));
                    } catch (ExecutionException e) {
                        LOGGER.error("[fetch queue error] ex {}", ExceptionUtils.getStackTrace(e));
                    }
                    if (result != null) {
                        candidateQueueMap.putAll(result);
                    }
                }
            }
        }

        stopwatch.reset().start();
        List<RankItem> result = convertToRankItem(candidateQueueMap, requestData, user);

        return result;
    }


    // 转成RankItem
    // 同时给Filter预留处理
    private List<RankItem> convertToRankItem(Map<Candidate, Queue<Video>> candidateQueueMap,
                                             RecommendRequest requestData,
                                             User user) {


        final FilterPipeline<Video> recallFilter = new FilterPipeline<>(filterConfig, requestData, user);

        final List<Callable<List<RankItem>>> callables = new ArrayList<>();
        for (final Map.Entry<Candidate, Queue<Video>> entry : candidateQueueMap.entrySet()) {
            callables.add(() -> {
                List<RankItem> candidateHits = new ArrayList<>();
                final Candidate candidate = entry.getKey();
                List<Entry<Video>> entriesValue = CommonCollectionUtils.iterableToList(entry.getValue());
                try {
                    final Map<Video, Entry<Video>> entryMap = new HashMap<>();
                    List<Video> entriesList = entriesValue.stream().map(e -> {
                        entryMap.put(e.item, e);
                        return e.item;
                    }).collect(Collectors.toList());
                    List<Video> entriesAfterFilter = recallFilter.doFilter(candidate, entriesList);
                    // 1. filter  TODO 待后续增加自定义filter
                    if (CollectionUtils.isNotEmpty(entriesAfterFilter)) {
                        List<Entry<Video>> entries = entriesAfterFilter.stream()
                                .limit(candidate.getCandidateNum())
                                .map(entryMap::get)
                                .collect(Collectors.toList());
                        // 2. toHits
                        candidateHits.addAll(toHits(entries, candidate));
                    }

                } catch (Exception e) {
                    LOGGER.error("recall filter queue occur error, queue [{}], error: [{}]", candidate.toString(), ExceptionUtils.getFullStackTrace(e));
                }
                return candidateHits;
            });
        }

        Map<String, RankItem> hits = new HashMap<>();
        try {
            List<Future<List<RankItem>>> futures = filterExecutorService.invokeAll(callables, DEFAULT_PARALLEL_FILTER_TIMEOUT, TimeUnit.MILLISECONDS);
            for (Future<List<RankItem>> future : futures) {
                try {
                    if (future.isDone() && !future.isCancelled() && future.get() != null) {
                        List<RankItem> part = future.get();
                        // add to result
                        for (RankItem item : part) {
                            // merge candidate Info
                            if (hits.containsKey(item.getId())) {
                                hits.get(item.getId()).addToCandidateInfoList(item.getCandidateInfo());
                            } else {
                                hits.put(item.getId(), item);
                            }
                        }
                        LOGGER.debug("collect items from filter task, userid: [{}], item size [{}]", "", part.size());
                    } else {
                        LOGGER.error("parallel recall filter Canceled {} ", "");
                    }
                } catch (Exception e) {
                    LOGGER.error("parallel recall filter occur error, uid: [{}], Exception [{}]",
                            "", ExceptionUtils.getFullStackTrace(e));
                }
            }
        } catch (Exception e) {
            LOGGER.error("parallel recall filter occur error, uid: [{}], Exception [{}]",
                    "", ExceptionUtils.getFullStackTrace(e));
        }

        List<RankItem> result = new ArrayList<>(hits.values());

        return result;
    }


    private Map<Candidate, Queue<Video>> obtainQueue(List<Candidate> refactorCandidates, RecommendRequest requestData, User user, boolean isFromRedis) {
        return loadQueues(refactorCandidates);
    }

}
