<?php
namespace Api\PhpUtils\Redis;

use Api\PhpUtils\Http\Request;
use Api\PhpUtils\Cache\ApcuUtil;
use Api\PhpUtils\Log\FileLog;
use Redis;

class RedisUtil
{
    const PREFIX_CODIS_PROXY = '_cp:';
    const PREFIX_CODIS_PROXY_LOCK = '_cpl:';
    const PREFIX_SENTINEL_HOSTS = '_sh:';
    const PREFIX_SENTINEL_HOSTS_LOCK = '_shl:';

    const EXPIRE_AFTER_ONE_SECOND = 1;
    const EXPIRE_AFTER_FIVE_SECONDS = 5;
    const EXPIRE_AFTER_TEN_SECONDS = 10;
    const EXPIRE_AFTER_ONE_MINUTE = 60;
    const EXPIRE_AFTER_FIVE_MINUTES = 300;
    const EXPIRE_AFTER_TEN_MINUTES = 600;
    const EXPIRE_AFTER_HALF_HOUR = 1800;
    const EXPIRE_AFTER_ONE_HOUR = 3600;
    const EXPIRE_AFTER_SIX_HOURS = 21600;
    const EXPIRE_AFTER_TWELVE_HOURS = 43200;
    const EXPIRE_AFTER_ONE_DAY = 86400;
    const EXPIRE_AFTER_THREE_DAYS = 259200;
    const EXPIRE_AFTER_ONE_WEEK = 604800;
    const EXPIRE_AFTER_FIFTEEN_DAYS = 1296000;
    const EXPIRE_AFTER_ONE_MONTH = 2592000;

    private static $instances = [];
    private $serverName = '';
    private $serverRegion = '';
    private $master = true; // 哨兵模式/主从模式，默认使用主库
    private $serverList = []; // 服务列表（proxy，sentinel）
    private $connectServer = '';
    private $connectStatus = false;
    private $redis = null;

    private $mode = '';
    private $topom = '';
    private $address = ''; //sentinel and redis host:port
    private $fallback = '';
    private $password = '';
    private $timeout = 0.5; //float, value in seconds (optional, default is 0 meaning unlimited)
    private $readTimeout = 1.5; // float, value in seconds (optional, default is 0 meaning unlimited)
    private $retryInterval = 100; // int, value in milliseconds (optional)
    private $serializer = '';
    private $prefix = '';
    private $select = 0;
    private $config = [];
    
    public function getConnectServer()
    {
        return $this->connectServer;
    }

    public function getSserverList()
    {
        return $this->serverList;
    }

    /**
     * 获取实例
     *
     * @param string $serverName 服务名
     * @param array  $options 其他参数数组，优先级高于yaconf中的相同配置
     * @param string $options['serverRegion']  服务区域，不同数据中心
     * @param bool   $options['master']        哨兵模式/主从模式，默认true使用主库，若使用从库值为false
     * @param string $options['serializer']    序列化选项，包括 none|php|igbinary, msgpack自己实现msgpack_pack|msgpack_unpack, incr等方法只能使用none，set等方法使用none不能存Array或Object
     * @param boolean $master
     * @return mixed
     */
    public static function getInstance($serverName = '', array $options = []) 
    {
        if (empty($serverName)) {
            return null;
        }
        if (empty($options)) {
            $instancesKey = $serverName;
        } else {
            $instancesKey = $serverName . ':' . json_encode($options);
        }
        if (isset(self::$instances[$instancesKey])) {
            return self::$instances[$instancesKey];
        }
        return self::$instances[$instancesKey] = new Self($serverName, $options);
    }

    private function __construct($serverName = '', array $options = [])
    {
        $this->serverName = $serverName;
        // 暂时不自动匹配数据中心
        // $hostName = explode('.',gethostname());
        // if( end($hostName) == 'bj3' ){
        //     $this->serverRegion = 'wj';
        // } else {
        //     $this->serverRegion = 'sh';
        // }
        if (isset($options['serverRegion'])) {
            $this->serverRegion = $options['serverRegion'];
        }
        $this->master = $options['master'] ?? true;
        $this->serializer = $options['serializer'] ?? '';
        $this->initConfigure();
        $this->connect();
    }

    private function initConfigure() 
    {
        $this->config = config('redis', $this->serverName . 'Connect');
        if (!empty($this->config)) {
            $this->mode = $this->config['mode'] ?? 'codis';
            // codis topom
            if ($this->mode == 'codis' && isset($this->config['topom'])) {
                if ($this->serverRegion) {
                    $this->topom = $this->config['topom'][$this->serverRegion] ?? '';
                } else {
                    // 不传入数据中心，如果未配置region则直接使用，如果配置了region则默认为使用第一个
                    $this->topom = is_string($this->config['topom']) ? $this->config['topom'] : current($this->config['topom']);
                }
            }
            //sentinel
            if ($this->mode == 'sentinel' && isset($this->config['address'])) {
                if ($this->serverRegion) {
                    $this->address = $this->config['address'][$this->serverRegion] ?? '';
                } else {
                    // 不传入数据中心，如果未配置region则直接使用，如果配置了region则默认为使用第一个
                    $this->address = is_string($this->config['address']) ? $this->config['address'] : current($this->config['address']);
                }
            }
            //redis master-slave address or redis single address
            if ($this->mode == 'redis' && isset($this->config['address'])) {
                $masterOrSlave = $this->master ? 'master' : 'slave';
                if ($this->serverRegion) {
                    $this->address = $this->config['address'][$this->serverRegion][$masterOrSlave] ?? '';
                    if (empty($this->address) && isset($this->config['address'][$this->serverRegion])) {
                        // 单机，ini中设置了数据中心，但没有设置master/slave
                        $this->address = is_string($this->config['address'][$this->serverRegion]) ? $this->config['address'][$this->serverRegion] : '';
                    }
                } else {
                    // 不传入数据中心，ini中未配置region则直接使用
                    $this->address = $this->config['address'][$masterOrSlave] ?? '';
                    // 不传入数据中心，ini中如果配置了region，默认使用第一个
                    if (empty($this->address)) {
                        $region = current($this->config['address']);
                        // 兼容ini中未设置master/slave
                        $this->address = $region[$masterOrSlave] ?? (is_string($region) ? $region : '');
                    }
                    // 单机，ini中未配置了region，也未配置master/slave
                    if (empty($this->address)) {
                        $this->address = is_string($this->config['address']) ? $this->config['address'] : '';
                    }
                }
            }

            if (empty($this->serializer) && isset($this->config['serializer'])) {
                $this->serializer = $this->config['serializer'] ? strtolower($this->config['serializer']) : 'none';
            }
            $this->fallback = $this->config['fallback'] ?? '';
            $this->password = $this->config['password'] ?? '';
            $this->connectTimeout = $this->config['timeout'] ? intval($this->config['timeout']) : 1;
            $this->prefix = $this->config['prefix'] ?? '';
            $this->select = $this->config['select'] ?? 0;
        } else {
            throw new \Exception("redis config is empty");
        }
    }

    private function connect() 
    {
        if (!$this->mode) {
            return false;
        }
        switch($this->mode) {
            case 'codis':
                return $this->initCodis();
                break;
            case 'sentinel':
                return $this->initSentinel();
                break;
            case 'redis':
                return $this->initRedis();
                break;
            default:
                return false;
                break;
        }
    }

    private function initCodis() 
    {
        try {
            $this->initCodisConnect();
            $this->initOptions();
            return $this->connectStatus;
        }catch (\Exception $e){
            return false;
        }
    }

    private function initCodisConnect($retryTime = 1)
    {
        $codisProxy = $this->getCodisProxyFromApcu();
        if($codisProxy === false) {
            return false;
        }
        $this->serverList = $codisProxy;
        $proxyNum = count($this->serverList);
        $tempServerList = $this->serverList;

        $redis = new \Redis();
        // 随机选取一个地址，若连接失败，重新选择地址
        for ($i = 0; $i < $proxyNum; $i++) {
            $index = array_rand($tempServerList);
            $this->connectServer = $tempServerList[$index];
            list($host, $port) = explode(':', $this->connectServer);
            $connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
            if (1 || !$connector) {
                $j = 0;
                while($j < $retryTime){
                    $connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
                    $j++;
                    if($connector){
                        $this->redis = $redis;
                        $this->connectStatus = true;
                        break 2;
                    }
                }
                unset($tempServerList[$index]);
            }else {
                $this->redis = $redis;
                $this->connectStatus = true;
                break;
            }
        }
        return true;
    }

    private function initSentinel() 
    {
        try {
            $this->initSentinelConnect();
            $this->initOptions();
            return $this->connectStatus;
        }catch (\Exception $e){
            return false;
        }
    }

    private function initSentinelConnect($retryTime = 3)
    {
        $list = $this->getSentinelInfoFromApcu();
        if (!$list) {
            return false;
        }

        $server = [];
        if($this->master) {
            $server = $list['master'];
        } else {
            if(count($list['slave']) > 1) {
                $rand  = array_rand($list['slave']);
                $server = $list['slave'][$rand];
            } else {
                $server = current($list['slave']);
            }
        }
        $this->connectServer = $server[0] .':'. $server[1];

        $redis = new \Redis();
        $host = $server[0];
        $port = $server[1];
        $connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
        if (!$connector) {
            $i = 0;
            while($i < $retryTime){
                $connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
                $i++;
                if($connector){
                    $this->redis = $redis;
                    $this->connectStatus = true;
                    break;
                }
            }
        }else {
            $this->redis = $redis;
            $this->connectStatus = true;
        }
        return true;
    }

    /**
     * 初始化 redis
     * @return \Redis
     */
    public function initRedis()
    {
        try {
            $this->initRedisConnect();
            $this->initOptions();
            return $this->connectStatus;
        }catch (\Exception $e){
            return false;
        }
    }

    private function initRedisConnect($retryTime = 1)
    {
        if (!$this->address || !strstr($this->address, ":")) {
            return null;
        }
        if ($this->master) {
            $this->connectServer = $this->address;
            $this->serverList = [$this->address];
        } else {
            $this->serverList = $address = explode(',', $this->address);
            if(count($address) > 1) {
                $rand = array_rand($address);
                $this->connectServer = $address[$rand];
            } else {
                $this->connectServer = current($address);
            }
        }

        $redis = new \Redis();
        list($host, $port) = explode(':', $this->connectServer);
        $connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
        if (!$connector) {
            $i = 0;
            while($i < $retryTime){
                $connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
                $i++;
                if($connector){
                    $this->redis = $redis;
                    $this->connectStatus = true;
                    break;
                }
            }
        }else {
            $this->redis = $redis;
            $this->connectStatus = true;
        }
        return true;
    }

    private function initOptions()
    {
        if (! $this->connectStatus) {
            return false;
        }

        if ($this->password && !$this->redis->auth($this->password)) {
            $this->redis = null;
            $this->connectStatus = false;
            return false;
        }

        switch ($this->serializer) {
            case 'none':
                $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);	  // Don't serialize data
                break;
            case 'php':
                $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);	  // Use built-in serialize/unserialize
                break;
            case 'igbinary':
                $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY); // Use igBinary serialize/unserialize
                break;
            case 'msgpack':
                // Undefined class constant 'SERIALIZER_MSGPACK'
                // $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_MSGPACK);  // Use msgpack serialize/unserialize
                // break;
            default:
                $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);	  // Don't serialize data
                break;
        }
        
        $this->redis->setOption(Redis::OPT_READ_TIMEOUT, $this->readTimeout);
        
        if ($this->prefix) {
            $this->redis->setOption(Redis::OPT_PREFIX, $this->prefix);	// use custom prefix on all keys
        }
        
        $this->redis->select($this->select);	// use custom prefix on all keys
        return true;
    }
    
    /**
     * 从 topom 获取 proxy 列表
     */
    private function getTopomServer()
    {
        if (! $this->topom) {
            return false;
        }
        $url = $this->topom;
        $request = new Request();
        $res = $request->get($url);
        if(isset($res['response'])) {
            $res = $res['response'];
        }
        
        $serverList = [];
        if (!empty($res) && isset($res['stats']['proxy']['models']) && !empty($res['stats']['proxy']['models'])) {
            $servers = $res['stats']['proxy']['models'];
            foreach ($servers as $s) {
                $token = $s['token'];
                if (isset($res['stats']['proxy']['stats'][$token]['stats']['online']) && true === $res['stats']['proxy']['stats'][$token]['stats']['online']) {
                    $serverList[] = $s['proxy_addr'];
                }
            }
        }

        if (empty($serverList) && $this->fallback) {
            $serverList = explode(',', $this->fallback);
            FileLog::waring("topom-server-fallback", " get codis proxy server empty");
        }

        if ($serverList) {
            return $serverList;
        }
        return false;
    }

    /**
     * 使用SENTINEL slaves命令获取哨兵集群主从节点信息,返回的slave可能含有多个节点
     * 
     * @return   boolean|object 成功返回主从信息,失败返回false
     */
    private function getMasterSlaveList()
    {
        $redis = $this->getSentinelInstance();
        if(!$redis) {
            return false;
        }

        // $list = $redis->rawCommand('SENTINEL', 'slaves', "wemdia_redis"); // 自媒体哨兵集群名，测试使用
        $list = $redis->rawCommand('SENTINEL', 'slaves', $this->serverName);
        if( !is_array($list) || !isset($list[0]) ){
            return false;
        }
        foreach ($list as $key => $item)
        {
            $item = $this->formatSentinelReturn($item);
            if( !isset($item['ip']) || !isset($item['port']) || !isset($item['flags']) )
            {
                FileLog::error("sentinel return error ",  json_encode($item));
                continue;
            }
            if( !in_array($item['flags'], array('slave','master')) )
            {
                FileLog::error("sentinel flags error ", json_encode($item));
                continue;
            }
            if( $item['port'] <=0 )
            {
                FileLog::error("sentinel port error ", json_encode($item));
                continue;
            }
            $ret['slave'][] = array($item['ip'], $item['port']);
            $ret['master']  = array($item['master-host'], $item['master-port']);
        }
        if( empty($ret['master']) )
        {
            return false;
        }
        if( empty($ret['slave']) )
        {
            $ret['slave'][] = $ret['master'];
        }
        $ret['slave'] = array_values($ret['slave']);
        return $ret;
    }

    /**
     * 获取哨兵连接
     *
     * @return boolean|object
     */
    private function getSentinelInstance()
    {
        if (!$this->address) {
            return false;
        }
        $this->serverList = $address = explode(',', $this->address);
        $addrNum = count($address);

        $redis = new \Redis();
        // 随机选取一个地址，若连接失败，重新选择地址
        for ($i = 0; $i < $addrNum; $i++) {
            $index = array_rand($address);
            list($host, $port) = explode(':', $address[$index]);
            $connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
            if ($connector) {
                $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
                return $redis;
            }
            unset($address[$index]);
        }
        return false;
    }

    /**
     * 格式化哨兵返回信息
     *
     * @param array $item
     * @return array
     */
    private function formatSentinelReturn($item)
    {
        $ret = array();
        $count = count($item);
        for ($i=0; $i <$count; ){
            $ret[$item[$i]] = $item[$i+1];
            $i += 2;
        }
        return $ret;
    }

    /**
     * 从apcu获取codis proxy
     *
     * @return mixed 
     */
    private function getCodisProxyFromApcu() {
        $res = ApcuUtil::apcu_fetch(self::PREFIX_CODIS_PROXY, $this->serverName);
        if ($res !== false) {
            // get 
            if (! isset($res['data']) || ! isset($res['expire_at'])) {
                FileLog::waring('get bad value from apcu', $this->serverName . ' ' . self::PREFIX_CODIS_PROXY);
                return false;
            }
            if ($_SERVER['REQUEST_TIME'] >= $res['expire_at']) {
                // update 抢占式更新
                $this->setCodisProxyToApcu();
            }
            return $res['data'];
        } else {
            // set
            $data = $this->setCodisProxyToApcu();
            if ($data !== false) {
                return $data;
            } else {
                // 自旋 从apcu获取数据，休眠10~50毫秒 usleep(mt_rand(10000,50000))
                for ($i = 1; $i < 100; $i++) {
                    usleep(mt_rand(10000, 50000));
                    $result = ApcuUtil::apcu_fetch(self::PREFIX_CODIS_PROXY, $this->serverName);
                    if ($result !== false) {
                        return $result['data'] ?? false;
                    }
                    if ($i % 10 === 0) {
                        $data = $this->setCodisProxyToApcu();
                        if ($data !== false) {
                            return $data;
                        }
                    }
                }
                return false;
            }
        }
    }

    /**
     * 将codis proxy设置到apcu中，缓存10秒钟
     *
     * @return mixed
     */
    private function setCodisProxyToApcu() {
        if (ApcuUtil::apcu_add_one(self::PREFIX_CODIS_PROXY_LOCK, $this->serverName, 1, 2)) {
            // 抢到锁，处理业务
            $data = $this->getTopomServer();
            if ($data !== false) {
                $value = array(
                    'data' => $data,
                    'expire_at' => $_SERVER['REQUEST_TIME'] + 6
                );
                ApcuUtil::retry_apcu_store_one(self::PREFIX_CODIS_PROXY, $this->serverName, $value, 10);
                ApcuUtil::apcu_delete(self::PREFIX_CODIS_PROXY_LOCK, $this->serverName);
                return $data;
            }
        }
        return false;
    }

    /**
     * 从apcu 获取 sentinel master-slave list
     *
     * @return mixed 
     */
    private function getSentinelInfoFromApcu() {
        $res = ApcuUtil::apcu_fetch(self::PREFIX_SENTINEL_HOSTS, $this->serverName);
        if ($res !== false) {
            // get 
            if (! isset($res['data']) || ! isset($res['expire_at'])) {
                FileLog::waring('get bad value from apcu', $this->serverName . ' ' . self::PREFIX_SENTINEL_HOSTS);
                return false;
            }
            if ($_SERVER['REQUEST_TIME'] >= $res['expire_at']) {
                // update 抢占式更新
                $this->setSentinelInfoToApcu();
            }
            return $res['data'];
        } else {
            // set
            $data = $this->setSentinelInfoToApcu();
            if ($data !== false) {
                return $data;
            } else {
                // 自旋 从apcu获取数据，休眠10~50毫秒 usleep(mt_rand(10000,50000))
                for ($i = 1; $i < 100; $i++) {
                    usleep(mt_rand(10000, 50000));
                    $result = ApcuUtil::apcu_fetch(self::PREFIX_SENTINEL_HOSTS, $this->serverName);
                    if ($result !== false) {
                        return $result['data'] ?? false;
                    }
                    if ($i % 10 === 0) {
                        $data = $this->setSentinelInfoToApcu();
                        if ($data !== false) {
                            return $data;
                        }
                    }
                }
                return false;
            }
        }
    }

    /**
     * 将sentinel master-slave list 设置到apcu中，缓存10秒钟
     *
     * @return mixed
     */
    private function setSentinelInfoToApcu() {
        if (ApcuUtil::apcu_add_one(self::PREFIX_SENTINEL_HOSTS_LOCK, $this->serverName, 1, 2)) {
            // 抢到锁，处理业务
            $data = $this->getMasterSlaveList();
            if ($data !== false) {
                $value = array(
                    'data' => $data,
                    'expire_at' => $_SERVER['REQUEST_TIME'] + 6
                );
                ApcuUtil::retry_apcu_store_one(self::PREFIX_SENTINEL_HOSTS, $this->serverName, $value, 10);
                ApcuUtil::apcu_delete(self::PREFIX_SENTINEL_HOSTS_LOCK, $this->serverName);
                return $data;
            }
        }
        return false;
    }

    public function __call($method, array $args)
    {
        if (!$this->redis) {
            return false;
        }
        return call_user_func_array([$this->redis, $method], $args);
    }
}
