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


import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Iterables;
import com.tzld.piaoquan.recommend.server.framework.candidiate.Queue;
import com.tzld.piaoquan.recommend.server.framework.candidiate.QueueName;
import com.tzld.piaoquan.recommend.server.framework.utils.FixedThreadPoolHelper;
import com.tzld.piaoquan.recommend.server.framework.utils.RedisSmartClient;
import com.tzld.piaoquan.recommend.server.model.Video;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

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


public class RedisBackedQueue implements QueueProvider<Video> {
    private static final Logger logger = LoggerFactory.getLogger(RedisBackedQueue.class);
    private static final int DEFAULT_TTL = 7 * 24 * 60 * 60;
    private static final long CACHE_MAXIMUMSIZE = 50 * 10000; // default 50w
    private static final long CACHE_TIMEOUT_MS = 5 * 60 * 1000L;

    private static final ExecutorService executorService = Executors.newFixedThreadPool(64);

    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    private final ExecutorService queueRefreshExecutorService = new FixedThreadPoolHelper(4, "queueRefresh").getThreadPoolExecutor();

    // 不能为static
    private final BlockingQueue<QueueName> asyncRefreshQueue = new LinkedBlockingQueue<QueueName>(50000);
    private final RedisSmartClient client;
    private final LoadingCache<QueueName, Pair<Long, Queue<Video>>> cache;

    /**
     * 实例化 RedisBackedQueue, 并提供基于Guava Cache的本地缓存管理工作
     *
     * @param client         redis-cluster 连接池
     * @param cacheTimeOutMs 缓存有效期, 开启缓存的情况下,默认缓存5min,以写时间为基准
     */
    public RedisBackedQueue(RedisSmartClient client, final long cacheTimeOutMs) {
        this.client = client;

        cache = CacheBuilder.newBuilder()
                .expireAfterWrite((long) (cacheTimeOutMs * 1.3), TimeUnit.MILLISECONDS)
                .maximumSize(CACHE_MAXIMUMSIZE)
                .build(new RedisBackedQueueWithoutMetaCacheLoader(client));

        // protect key
        for (int i = 0; i < 4; i++) {
            queueRefreshExecutorService.execute(new CacheRefreshRunnable());
        }
    }

    public RedisBackedQueue(RedisSmartClient client) {
        this(client, CACHE_TIMEOUT_MS);
    }

    public RedisSmartClient getClient() {
        return client;
    }


    // redis读取队列内内容
    public Index get(QueueName name) throws Exception {
        String valueJson = client.get(name.toString());
        Index index = new Index(name.toString(), valueJson);
        return index;
    }



    /**
     * 加载指定索引, 并指定索引本地缓存的TTL
     * @param name QueueName
     * @return
     * @throws ExecutionException
     */
    public Queue<Video> load(QueueName name) throws ExecutionException {

        Pair<Long, Queue<Video>> cachedQueue = cache.get(name);
        if (cachedQueue == null) {
            // 清理本地缓存中当前key的数据
            cache.invalidate(name);
            logger.debug("invalidate queue [{}]", name);
            return new Queue<Video>(name.toString());
        }
        // update key refresh time
        if (cachedQueue.getKey() == Long.MIN_VALUE) {
            cache.put(name, Pair.of(System.currentTimeMillis() + name.getTTL(), cachedQueue.getValue()));
        }

        if (name.getTTL() == 0) {
            cache.invalidate(name);
            logger.debug("invalidate queue [{}]", name);
        }

        // check and refresh
        if (name.getTTL() > 0 && System.currentTimeMillis() > cachedQueue.getKey()) {
            boolean isok = asyncRefreshQueue.offer(name);
            if (logger.isDebugEnabled()) {
                logger.debug("offer async queue name [{}], result [{}]", name, isok);
            }
            if (asyncRefreshQueue.size() > CACHE_MAXIMUMSIZE) {
                logger.warn("index backgroud blocking queue length [{}], it is danger", asyncRefreshQueue.size());
            }
            if (!isok) {
                logger.error("index backgroud offer blocking queue error, queuename [{}], with queue lenght [{}]", name, asyncRefreshQueue.size());
            }
        }

        return cachedQueue.getValue();
    }

    public Map<QueueName, Queue<Video>> loads(List<QueueName> names) throws ExecutionException {
        return this.loads(names, 200, TimeUnit.MILLISECONDS);
    }

    public Map<QueueName, Queue<Video>> loads(List<QueueName> names, long timeout, TimeUnit timeUnit) throws ExecutionException {

        final Iterable<List<QueueName>> namesIterable = Iterables.partition(names, 1);
        final List<Callable<Map<QueueName, Queue<Video>>>> callables =
                new ArrayList<Callable<Map<QueueName, Queue<Video>>>>();
        for (final List<QueueName> queueNames : namesIterable) {
            callables.add(() -> {
                Map<QueueName, Queue<Video>> result = new HashMap<>();
                for (final QueueName name : queueNames) {
                    try {
                        Queue queue = load(name);
                        result.put(name, queue);
                    } catch (Exception e) {
                        logger.error("load queue error [{}] [{}]", name, ExceptionUtils.getFullStackTrace(e));
                    }
                }
                return result;
            });
        }

        Map<QueueName, Queue<Video>> loadQueue = new ConcurrentHashMap<QueueName, Queue<Video>>();
        try {
            List<Future<Map<QueueName, Queue<Video>>>> futures = executorService.invokeAll(callables);
            for (Future<Map<QueueName, Queue<Video>>> future : futures) {
                if (future.isDone() && !future.isCancelled()) {
                    try {
                        Map<QueueName, Queue<Video>> ret = future.get();
                        loadQueue.putAll(ret);
                    } catch (ExecutionException ee) {
                        logger.error("Failed to execute load a queue partition for {}", ExceptionUtils.getFullStackTrace(ee));
                    }
                }
            }
        } catch (InterruptedException ie) {
            logger.error("Interrupted when waiting for parallel queue load finish for {}", ExceptionUtils.getFullStackTrace(ie));
        }
        return loadQueue;
    }



    // 使用Guava Cache 的异步刷新接口, 异步刷新到期的key
    public class CacheRefreshRunnable implements Runnable {

        @Override
        public void run() {
            while (true) {
                QueueName queueName = null;
                try {
                    queueName = asyncRefreshQueue.take(); // take async item, blocked when no item
                    final long startTime = System.currentTimeMillis();
                    if (logger.isDebugEnabled()) {
                        logger.debug("async refresh thread get queuename [{}]", queueName); // get itemid
                    }

                    Pair<Long, Queue<Video>> entry = cache.get(queueName);
                    // check refresh
                    if (startTime > entry.getKey()) {
                        cache.refresh(queueName);
                    }

                    long endTime = System.currentTimeMillis();
                    if (logger.isDebugEnabled()) {
                        logger.debug("parallel refresh queue [{}] spenttime [{}]", queueName, endTime - startTime);
                    }
                } catch (Exception e) {
                    logger.error("schedule refresh queue error [{}], exception [{}]", queueName, ExceptionUtils.getFullStackTrace(e));
                }
            }
        }
    }
}
