package com.brilliance.isc.counter.service.impl;

import com.brilliance.isc.counter.config.CounterConfig;
import com.brilliance.isc.counter.config.SegmentConfig;
import com.brilliance.isc.counter.exception.CounterException;
import com.brilliance.isc.counter.mapper.CounterMapperOfCou;
import com.brilliance.isc.counter.service.CounterService;
import com.brilliance.isc.counter.service.SegmentService;
import lombok.AllArgsConstructor;
import org.redisson.api.RScript;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec;
import org.redisson.client.codec.StringCodec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

@Service("segmentCounter")
public class SegmentCounter implements CounterService {

    private Logger log = LoggerFactory.getLogger(this.getClass());
    private static String COUMAP = couPrefix + "MAP";
    private static String maxKey = "%s:max";
    private static String curKey = "%s:cur";
    private static String updkey = "%s:upd";
    private static String script =
            "local mapKey = KEYS[1];\n" +
                    "local maxKey = KEYS[2];\n" +
                    "local curKey = KEYS[3];\n" +
                    "local updKey = KEYS[4];\n" +
                    "local maxArg = tonumber(ARGV[1]);\n" +
                    "local curArg = tonumber(ARGV[2]);\n" +
                    "local timeout = tonumber(ARGV[3]);\n" +
                    "\n" +
                    "--更新COU缓存\n" +
                    "if maxArg > 0 then\n" +
                    "  redis.call('HMSET', mapKey, maxKey, maxArg, curKey, curArg + 1);\n" +
                    "  redis.call('DEL',updKey);\n" +
                    "  return curArg;\n" +
                    "end\n" +
                    "--cou存在且缓冲区仍有空余\n" +
                    "local cur = tonumber(redis.call('HGET',mapKey,curKey));\n" +
                    "if cur and cur < tonumber(redis.call('HGET',mapKey,maxKey)) then\n" +
                    "  redis.call('HSET',mapKey,curKey,cur+1);\n" +
                    "  return cur;" +
                    "end\n" +
                    "--C0U不存在或者C0U缓存已使用完\n" +
                    "if tonumber(redis.call('GET',updKey)) == -2 then\n" +
                    "  return -2;\n" +
                    "end\n" +
                    "redis.call('SETEX',updKey,timeout,-2);\n" +
                    "if cur then\n" +
                    "  return -1;\n" +
                    "end\n" +
                    "return -3;";

    private static String curScript = "local cur=tonumber(redis.call('HGET','" + COUMAP + "','" + curKey + "'));return cur;";

    @Autowired
    private RedissonClient redissonClient;
    private Codec codec = new StringCodec();

    @Autowired
    @Qualifier("dbCounter")
    private CounterService dbCounter;
    @Autowired
    private CounterConfig counterConfig;
    @Autowired
    private SegmentConfig segmentConfig;
    @Autowired
    private SegmentService segmentService;

    @Autowired
    private CounterMapperOfCou counterMapper;

    @Autowired
    @Qualifier("jacksonRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    public Long internalGen(String key) throws CounterException {
        long timeout = segmentConfig.getBuffUpdTimeout();
        int buffLen = segmentConfig.getBuffLen();
        List<Object> keys = Arrays.asList(COUMAP,
                String.format(maxKey, key),
                String.format(curKey, key),
                String.format(updkey, key));
        Object[] argvGet = new Object[]{-1L, -1L, timeout};
        Long result = this.eval(key, keys, argvGet);

        //-2:其他线程开始更新号段缓冲，当前线程自旋等待 自旋最长时间为timeout
        Long timeoutMillis = System.currentTimeMillis() + timeout * 1000;
        while (result == -2 && System.currentTimeMillis() <= timeoutMillis) {
            for (int i = 0; i < 2000; i++) {
            }
            result = this.eval(key, keys, argvGet);
        }
        //超时后其他线程仍未完成号段更新，自动失败
        if (result == -2)
            throw new CounterException("获取COU超时[" + key + "]请稍后重试!");

        //大于0: 缓冲区号段可用，此时结果即为新cou值
        if (result > 0)
            return result;

        // -1：redis中存在号段但缓冲已用完
        // -3：redis中不存在对应号段
        Segment buf = segmentService.updateSegment(result == -3, key, counterConfig.getStart(), segmentConfig.getBuffLen());
        if ((result = this.eval(key, keys, buf.max, buf.cur, timeout)) > 0) {
            return result;
        }
        throw new CounterException("获取counter异常!");
    }

    public Long evalCur(String key) {
        Long id = redissonClient.getScript(codec).eval(
                RScript.Mode.READ_ONLY,
                String.format(curScript, key),
                RScript.ReturnType.INTEGER);
        log.info("eval cur {}:{}", key, id);
        return id;
    }

    private Long eval(String key, List<Object> keys, Object... argv) {
        Long id = redissonClient.getScript(codec).eval(
                RScript.Mode.READ_WRITE,
                String.valueOf(script),
                RScript.ReturnType.INTEGER,
                keys, argv);
        log.info("eval{} {}:{}", argv, key, id);
        return id;
    }

    @Override
    public long idGen(String key) {
        //允许部分使用号段缓冲，极端情况下要求连续不跳号的走数据库直接更新。
        if (segmentConfig.segmentAllowed(key)) {
            return internalGen(key);
        }
        return dbCounter.idGen(key);
    }

    @AllArgsConstructor
    public static class Segment {
        public long cur;
        public long max;
    }



    /**
     * 将Redis缓存中的COU落表
     * 因为希望落表与清理redis是同一个事务，因此设置一个独立事务
     */
    @Transactional(rollbackFor = {RuntimeException.class, Exception.class},propagation = Propagation.REQUIRES_NEW)
    public void updateCouFromRedis(){
        try {
            log.info("开始将Redis COU落表");
            String couKey = COUMAP;
            Map<Object, Object> couMap = redisTemplate.opsForHash().entries(couKey);
            if(couMap == null){
                log.info("没有读取到Redis COU");
                return;
            }
            log.info("COU数量:{}",couMap.size());
            couMap.forEach((key, value) -> {
                String keyStr = (String) key;
                if (keyStr.endsWith(":cur")) {
                    keyStr = keyStr.substring(0, keyStr.length() - 4);
                    log.info("更新COU,key:{},value:{}",keyStr,value);
                    long valueVal = 0L;

                    if(value instanceof Integer){
                        valueVal = (Integer)value;
                    }else{
                        valueVal = (Long)value;
                    }
                    counterMapper.updateCounterDirect(keyStr, valueVal);
                }
            });
            log.info("清理Redis COU缓存");
            redisTemplate.delete(couKey);
            log.info("清理Redis COU完成");
        }catch (Exception e){
            log.error("落回COU出错{}", e);
        }
    }
}
