package com.crm.gateway.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.crm.gateway.exception.ServiceException;
import com.crm.gateway.service.RedisService;
import com.crm.gateway.util.ScriptUtil;
import com.crm.gateway.util.UUIDUtil;
import com.crm.rely.backend.core.constant.Constants;
import com.crm.rely.backend.core.constant.RedisToolsConstants;
import com.crm.rely.backend.core.entity.login.InfoEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

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

    private Logger logger = LoggerFactory.getLogger(RedisServiceImpl.class);

    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;

    @Value("${spring.redis.type:1}")
    private int type;

    /**
     * time millisecond
     */
    private static final int TIME = 1000;

    /**
     * lua script
     */
    private String script;

    private RedisServiceImpl() {
        buildScript();
    }

    @Resource
    private RedisTemplate redisTemplate;

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

        return this.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);
//        }
        try {
            if (time < 0) {

                redisTemplate.opsForValue().set(key, t);
            } else {
                redisTemplate.opsForValue().set(key, t, time, TimeUnit.MILLISECONDS);
            }
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }

        return true;
    }

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

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

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

        try {
            if (hasKey(key)) {
                return (T) redisTemplate.opsForValue().get(key);
            }
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
        return null;
    }

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

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

        try {
            return redisTemplate.hasKey(key);
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
//        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);
//        }

        try {
            if (hasKey(key)) {
                String redisStr = this.getString(key);
                return JSON.parseObject(redisStr, clazz);
            }
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }

        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);
//        }
        try {
            if (hasKey(key)) {
                String redisStr = this.getString(key);
                return JSON.parseArray(redisStr, clazz);
            }
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
        return null;
    }

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

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

//        if (hasKey(key)) {
//            redisTemplate.delete(key);
//        }
        try {
            redisTemplate.delete(key);
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }

        return true;
    }

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

        if (!isConnect()) {
            return false;
        }
//        for (String key : keys) {
//            redisTemplate.delete(key);
//        }
        try {
            for (String key : keys) {
                redisTemplate.delete(key);
            }
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }

        return true;
    }

    @Override
    public boolean remove(Collection<String> keys) throws ServiceException {

        redisTemplate.delete(keys);

        return true;
    }

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

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

//        if (hasKey(key)) {
//            String redisStr = (String) redisTemplate.opsForValue().get(key);
//            if (redisStr == null) {
//                return "";
//            } else {
//                return redisStr;
//            }
//        }
        try {
            if (hasKey(key)) {
                String redisStr = (String) redisTemplate.opsForValue().get(key);
                if (redisStr == null) {
                    return "";
                } else {
                    return redisStr;
                }
            }
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }

        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;
    }

    /**
     * get Redis connection
     *
     * @return
     */
    private Object getConnection() {
        Object connection;
        if (type == RedisToolsConstants.SINGLE) {
            RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection();
            connection = redisConnection.getNativeConnection();
        } else {
            RedisClusterConnection clusterConnection = redisTemplate.getConnectionFactory().getClusterConnection();
            connection = clusterConnection.getNativeConnection();
        }
        return connection;
    }

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

                if (lock(connection, 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 {

        //get connection
        Object connection = getConnection();
        try {
            while (blockTime >= 0) {

                if (lock(connection, 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(Object connection, String key, String request) {

        return lock(connection, key, request, 10 * TIME);
    }

    /**
     * 加锁
     *
     * @param connection
     * @param key
     * @param request
     * @param expireTime 过期时间 默认为10 * TIME  这里为TIME为1000 即10秒
     * @return
     */
    private boolean lock(Object connection, String key, String request, int expireTime) {


        try {
            String result;

            if (connection instanceof Jedis) {
                //加锁 LOCK_PREFIX + key
                result = ((Jedis) connection).set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
                ((Jedis) connection).close();
            } else {
                result = ((JedisCluster) connection).set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
                try {
                    ((JedisCluster) connection).close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (LOCK_MSG.equals(result)) {
                return true;
            }
            return false;
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
    }

    /**
     * 加锁 只获取一次
     *
     * @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) {
        //get connection
        Object connection = getConnection();

        return lock(connection, key, request, expireTime);
    }

    @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) {
        try {
            //get connection
            Object connection = getConnection();
            //lua script

            Object result = null;
            if (connection instanceof Jedis) {
                result = ((Jedis) connection).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
                ((Jedis) connection).close();
            } else if (connection instanceof JedisCluster) {
                result = ((JedisCluster) connection).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
                try {
                    ((JedisCluster) connection).close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                //throw new RuntimeException("instance is error") ;
                return false;
            }

            if (UNLOCK_MSG.equals(result)) {
                return true;
            } else {
                return false;
            }
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }

    }

    /**
     * read lua script
     */
    private void buildScript() {
        script = ScriptUtil.getScript("lua/lock.lua");
    }

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

            redisTemplate.opsForValue().set(key, listString);
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
//        if (redisTemplate.hasKey(key)) {
//            redisTemplate.delete(key);
//        }
//
//        redisTemplate.opsForValue().set(key, listString);
    }

}
