package cn.axzo.msg.center.message.service.todo.cache;

import cn.axzo.msg.center.dal.mapper.MessageBaseTemplateMapper;
import cn.axzo.msg.center.dal.mapper.TodoMapper;
import cn.axzo.msg.center.inside.notices.config.PendingMessageBizConfig;
import cn.axzo.msg.center.message.domain.param.MessageGroupNodeStatisticParam;
import cn.axzo.msg.center.service.pending.response.PendingMessageStatisticResponseV2;
import com.alibaba.fastjson.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.security.SecureRandom;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * @author yanglin
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class NodeStatCache {

    private final MessageBaseTemplateMapper messageBaseTemplateMapper;
    private final TodoMapper todoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final PendingMessageBizConfig cfg;
    private final PeriodCacheMetric cacheMetric = new PeriodCacheMetric("node-stat", 1, TimeUnit.HOURS);

    public PendingMessageStatisticResponseV2 getCacheResponseOrReload(
            MessageGroupNodeStatisticParam request, Supplier<PendingMessageStatisticResponseV2> loader) {
        if (!cfg.isNodeStatCacheOn()) {
            return loader.get();
        }
        Collection<String> nodeCodes = request.getGroupNodeCodes();
        if (CollectionUtils.isNotEmpty(nodeCodes) && nodeCodes.size() > cfg.getNodeStatCacheMaxRequestNodeCodeSize()) {
            log.info("超过了允许缓存的最大请求节点编码数量，不使用缓存, 直接从数据库获取. request={}", request);
            return loader.get();
        }
        Date templateLatestUpdateTime = messageBaseTemplateMapper.getTemplateLatestUpdateTime();
        Date todoLatestUpdateTime = todoMapper.getTodoLatestUpdateTime(request.getPersonId());
        NodeStatCacheValue cacheValue = getCacheValue(request);
        boolean reloaded = false;
        if (cacheValue == null || cacheValue.shouldReload(templateLatestUpdateTime, todoLatestUpdateTime)) {
            cacheMetric.incrMissCount();
            log.info("reloading node stat data, request={}", request);
            PendingMessageStatisticResponseV2 response = loader.get();
            cacheValue = new NodeStatCacheValue();
            cacheValue.setResponse(response);
            cacheValue.setTemplateLatestUpdateTime(templateLatestUpdateTime);
            cacheValue.setTodoLatestUpdateTime(todoLatestUpdateTime);
            setCacheValue(request, cacheValue);
            reloaded = true;
        }
        cacheMetric.incrHitCount();
        PendingMessageStatisticResponseV2 copy = new PendingMessageStatisticResponseV2();
        log.info("use cached node stat data, request={}", request);
        BeanUtils.copyProperties(cacheValue.getResponse(), copy);
        copy.setCacheReloaded(reloaded);
        return copy;
    }

    // !! cache helper

    private NodeStatCacheValue getCacheValue(MessageGroupNodeStatisticParam request) {
        String redisKey = buildCacheKey(request);
        String json = stringRedisTemplate.opsForValue().get(redisKey);
        if (StringUtils.isBlank(json)) {
            return null;
        }
        try {
            return JSON.parseObject(json, NodeStatCacheValue.class);
        } catch (Exception e) {
            // 人为改了缓存, 而且没有改对
            log.warn("fail to parse cache value, will reload. request={}, cacheValue={}", request, json, e);
            return null;
        }
    }

    private void setCacheValue(MessageGroupNodeStatisticParam request, NodeStatCacheValue cacheValue) {
        log.info("set cache value for request. request={}", request);
        cacheValue.getResponse().setCacheReloaded(null);
        long secondsDelta = TimeUnit.HOURS.toSeconds(cfg.getNodeStatCacheDataExpireHours());
        // add some random delta seconds, range: 0-600 seconds
        secondsDelta += new SecureRandom().nextInt(10 * 60);

        String redisKey = buildCacheKey(request);
        String redisValue = JSON.toJSONString(cacheValue);
        stringRedisTemplate.opsForValue().set(redisKey, redisValue, secondsDelta, TimeUnit.SECONDS);
    }

    private static String buildCacheKey(MessageGroupNodeStatisticParam request) {
        StringBuffer buf = new StringBuffer();
        buf.append("msg-center:node-stat:v3");
        Consumer<Object> appender = value -> {
            if (value != null) {
                buf.append(":").append(value);
            }
        };
        appender.accept(request.getPersonId());
        appender.accept(request.getOuId());
        appender.accept(request.getTerminalType());
        Collection<String> nodeCodes = request.getGroupNodeCodes();
        if (CollectionUtils.isNotEmpty(nodeCodes)) {
            // 排序避免编码顺序和重复编码的影响
            nodeCodes.stream()
                    .filter(StringUtils::isNotBlank)
                    .distinct()
                    .sorted()
                    .forEach(appender);
        }
        List<Long> workspaceIds = request.getWorkspaceIds();
        if (CollectionUtils.isNotEmpty(workspaceIds)) {
            workspaceIds.stream()
                    .filter(Objects::nonNull)
                    .distinct()
                    .sorted()
                    .forEach(appender);
        }
        return buf.toString();
    }

}