package com.crm.rely.backend.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.crm.rely.backend.core.constant.Constants;
import com.crm.rely.backend.core.entity.login.InfoEntity;
import com.crm.rely.backend.exception.ServiceException;
import com.crm.rely.backend.service.RedisService;
import com.crm.rely.backend.util.UUIDUtil;
import lombok.extern.log4j.Log4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author houn
 */
@Log4j
@Service
public class RedisServiceImpl implements RedisService {

    private static final String LOCK_MSG = "OK";
    private static final Long UNLOCK_MSG = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final String LOCK_PREFIX = "lock_";
    private static final int SLEEP_TIME = 100;
    /**
     * time millisecond
     */
    private static final int TIME = 1000;
    private Logger logger = LoggerFactory.getLogger(RedisServiceImpl.class);
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    @Value("${spring.redis.type:1}")
    private int type;
    @Value("${spring.redis.PREFIX:}")
    private String PREFIX;
    /**
     * lua unlock script
     */
    @Autowired
    private RedisScript<Long> redisUnlockScript;
    /**
     * lua lock
     */
    @Autowired
    private RedisScript<Long> redisLockScript;

    @Override
    public <T> boolean save(String key, T t) throws ServiceException {

        return save(key, t, -1);
    }

    @Override
    public <T> boolean save(String key, T t, long time) throws ServiceException {

        String objectStr = JSON.toJSONString(t, SerializerFeature.WriteMapNullValue);

        return saveObject(key, objectStr, time);
    }

    @Override
    public <T> boolean saveObject(String key, T t) throws ServiceException {

        return saveObject(key, t, -1);
    }

    @Override
    public <T> boolean saveObject(String key, T t, long time) throws ServiceException {

        if (!isConnect()) {
            return false;
        }

//        if (time < 0) {
//
//            redisTemplate.opsForValue().set(key, t);
//        } else {
//            redisTemplate.opsForValue().set(key, t, time, TimeUnit.MILLISECONDS);
//        }
        if (time < 0) {

            redisTemplate.opsForValue().set(PREFIX + key, t);
        } else {
            redisTemplate.opsForValue().set(PREFIX + key, t, time, TimeUnit.MILLISECONDS);
        }


        return true;
    }

    @Override
    public <T> T getObject(String key) throws ServiceException {

        if (!isConnect()) {
            return null;
        }

//        if (hasKey(key)) {
//            return (T) redisTemplate.opsForValue().get(key);
//        }


        if (hasKey(key)) {
            return (T) redisTemplate.opsForValue().get(PREFIX + key);
        }

        return null;
    }

    @Override
    public boolean hasKey(String key) throws ServiceException {

        if (!isConnect()) {
            return false;
        }


        return redisTemplate.hasKey(PREFIX + key);

//        return redisTemplate.hasKey(key);
    }

    @Override
    public <T> T getEntity(String key, Class<T> clazz) throws ServiceException {

        if (!isConnect()) {
            return null;
        }

//        if (hasKey(key)) {
//            String redisStr = this.getString(key);
//            return JSON.parseObject(redisStr, clazz);
//        }


        if (hasKey(key)) {
            String redisStr = getString(key);
            return JSON.parseObject(redisStr, clazz);
        }

        return null;
    }

    @Override
    public <T> List<T> getList(String key, Class<T> clazz) throws ServiceException {

        if (!isConnect()) {
            return null;
        }

//        if (hasKey(key)) {
//            String redisStr = this.getString(key);
//            return JSON.parseArray(redisStr, clazz);
//        }

        if (hasKey(key)) {
            String redisStr = getString(key);
            return JSON.parseArray(redisStr, clazz);
        }

        return null;
    }

    @Override
    public boolean remove(String key) throws ServiceException {

        if (!isConnect()) {
            return false;
        }

        Set<Object> keys = redisTemplate.keys(PREFIX + key);

        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }

        return true;
    }

    @Override
    public boolean remove(String... keys) throws ServiceException {

        List<String> keyArrays = new ArrayList<>(keys.length);
        for (String key : keys) {
            keyArrays.add(key);
        }

        return remove(keyArrays);
    }

    @Override
    public boolean remove(Collection<String> keys) throws ServiceException {
        if (keys == null || keys.size() <= 0) {
            return true;
        }
        if (!isConnect()) {
            return false;
        }
        Collection kCollection = new ArrayList<>(keys.size());
        for (String key : keys) {

//            kCollection.add(PREFIX + key);

            Set<Object> setKeys = redisTemplate.keys(PREFIX + key);
            kCollection.add(setKeys);
        }

        redisTemplate.delete(kCollection);

        return true;
    }

    @Override
    public String getString(String key) throws ServiceException {

        if (!isConnect()) {
            return null;
        }

        if (hasKey(key)) {
            String redisStr = (String) redisTemplate.opsForValue().get(PREFIX + key);
            if (redisStr == null) {
                return "";
            } else {
                return redisStr;
            }
        }


        return null;
    }

    @Override
    public boolean isConnect() throws ServiceException {

        boolean isConnect = false;
        int count = 0;
        while (count < 3 && !isConnect) {
            try {

                List<RedisClientInfo> redisClientInfos = redisTemplate.getClientList();

                if (redisClientInfos != null && redisClientInfos.size() > 0) {
                    isConnect = true;
                }
            } catch (RedisConnectionFailureException e) {

                logger.error(e.getMessage());
            }
        }

        return isConnect;
    }


    /**
     * 一直获取锁 知道获取到为止
     *
     * @param key     锁头
     * @param request 锁内容 建议直接uuid
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean lock(String key, String request) throws InterruptedException {

        try {
            for (; ; ) {

                if (lock(10 * TIME, key, request)) {
                    return true;
                }

                Thread.sleep(SLEEP_TIME);
            }
        } catch (RuntimeException e) {
            logger.error(e.getMessage());
            return false;
        }
    }

    /**
     * 加锁 重复获取blockTime时间 原理为 blockTime -= SLEEP_TIME 每次 Thread.sleep(SLEEP_TIME)时间
     * 这里SLEEP_TIME为100毫秒即blockTime/SLEEP_TIME的次数
     *
     * @param key
     * @param request
     * @param blockTime 尝试时间
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean lock(String key, String request, int blockTime) throws InterruptedException {

        try {
            while (blockTime >= 0) {

                if (lock(10 * TIME, key, request)) {
                    return true;
                }

                blockTime -= SLEEP_TIME;

                Thread.sleep(SLEEP_TIME);
            }
        } catch (RuntimeException e) {
            logger.error(e.getMessage());
            return false;
        }
        return false;
    }

    private boolean lock(int expireTime, String key, String request) {
        List keys = Collections.singletonList(PREFIX + LOCK_PREFIX + key);

        Long result = redisTemplate.execute(redisLockScript, keys, request, expireTime);
        if (UNLOCK_MSG.equals(result)) {
            return true;
        } else {
            return false;
        }

    }

    /**
     * 加锁 只获取一次
     *
     * @param key
     * @param request
     * @return
     */
    @Override
    public boolean tryLock(String key, String request) {
        return tryLock(key, request, 10 * TIME);
    }

    /**
     * 加锁 只获取一次 但自定义过期时间
     *
     * @param key
     * @param request
     * @param expireTime 加锁时间
     * @return
     */
    @Override
    public boolean tryLock(String key, String request, int expireTime) {

        return lock(expireTime, key, request);
    }

    @Override
    public boolean tryLock(String key, Runnable statement) {

        String co = UUIDUtil.getUUID();
        try {
            if (tryLock(key, co, 10 * 60 * 1000)) {
                statement.run();
                return true;
            }
            return false;
        } finally {
            unlock(key, co);

        }
    }

    @Override
    public void tryLock(String key, Runnable statement, String exceptionMsg) throws ServiceException {
        if (!tryLock(key, statement)) {
            throw ServiceException.exception(exceptionMsg);
        }
    }

    @Override
    public void tryLock(InfoEntity infoEntity, HttpServletRequest httpServletRequest, Runnable statement,
                        String exceptionMsg) throws ServiceException {
        String uri = httpServletRequest.getRequestURI();
        String key = httpServletRequest.getHeader(Constants.ACCESS_TOKEN) + infoEntity.getId() + uri;
        tryLock(key, statement, exceptionMsg);
    }

    @Override
    public boolean unlock(String key, String request) {
        List keys = Collections.singletonList(PREFIX + LOCK_PREFIX + key);

        Long result = redisTemplate.execute(redisUnlockScript, keys, request);
        if (UNLOCK_MSG.equals(result)) {
            return true;
        } else {
            return false;
        }
    }

    @Override
    public <T> void saveList(String key, List<T> list) throws Exception {
        if (!isConnect()) {
            return;
        }
        String listString = JSON.toJSONString(list);

        if (redisTemplate.hasKey(PREFIX + key)) {
            redisTemplate.delete(PREFIX + key);
        }

        redisTemplate.opsForValue().set(PREFIX + key, listString);
    }

}
