<?php

/**
 * Credis_Sentinel
 *
 * Implements the Sentinel API as mentioned on http://redis.io/topics/sentinel.
 * Sentinel is aware of master and slave nodes in a cluster and returns instances of Credis_Client accordingly.
 *
 * @author Thijs Feryn <thijs@feryn.eu>
 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
 * @package Credis_Sentinel
 */
class Credis_Sentinel
{
    /**
     * Contains a client that connects to a Sentinel node.
     * Sentinel uses the same protocol as Redis which makes using Credis_Client convenient.
     * @var Credis_Client
     */
    protected $_client;

    /**
     * Contains an active instance of Credis_Cluster per master pool
     * @deprecated no longer used
     * @var array
     */
    protected $_cluster = array();

    /**
     * Contains an active instance of Credis_Client representing a master
     * @var array
     */
    protected $_master = array();

    /**
     * Contains an array Credis_Client objects representing all slaves per master pool
     * @var array
     */
    protected $_slaves = array();

    /**
     * Use the phpredis extension or the standalone implementation
     * @var bool
     * @deprecated
     */
    protected $_standAlone = false;

    /**
     * Store the AUTH password used by Credis_Client instances
     * @var string
     */
    protected $_password = '';
    /**
     * Store the AUTH username used by Credis_Client instances (Redis v6+)
     * @var string
     */
    protected $_username = '';
    /**
     * @var null|float
     */
    protected $_timeout;
    /**
     * @var string
     */
    protected $_persistent;
    /**
     * @var int
     */
    protected $_db;
    /**
     * @var string|null
     */
    protected $_replicaCmd = null;
    /**
     * @var string|null
     */
    protected $_redisVersion = null;

    /**
     * Connect with a Sentinel node. Sentinel will do the master and slave discovery
     *
     * @param Credis_Client $client
     * @param string $password (deprecated - use setClientPassword)
     * @throws CredisException
     */
    public function __construct(Credis_Client $client, $password = null, $username = null)
    {
        $client->forceStandalone(); // SENTINEL command not currently supported by phpredis
        $this->_client = $client;
        $this->_password = $password;
        $this->_username = $username;
        $this->_timeout = null;
        $this->_persistent = '';
        $this->_db = 0;
    }

    /**
     * Clean up client on destruct
     */
    public function __destruct()
    {
        $this->_client->close();
    }

    /**
     * @param float $timeout
     * @return $this
     */
    public function setClientTimeout($timeout)
    {
        $this->_timeout = $timeout;
        return $this;
    }

    /**
     * @param string $persistent
     * @return $this
     */
    public function setClientPersistent($persistent)
    {
        $this->_persistent = $persistent;
        return $this;
    }

    /**
     * @param int $db
     * @return $this
     */
    public function setClientDatabase($db)
    {
        $this->_db = $db;
        return $this;
    }

    /**
     * @param null|string $password
     * @return $this
     */
    public function setClientPassword($password)
    {
        $this->_password = $password;
        return $this;
    }

    /**
     * @param null|string $username
     * @return $this
     */
    public function setClientUsername($username)
    {
        $this->_username = $username;
        return $this;
    }

    /**
     * @param null|string $replicaCmd
     * @return $this
     */
    public function setReplicaCommand($replicaCmd)
    {
        $this->_replicaCmd = $replicaCmd;
        return $this;
    }

    public function detectRedisVersion()
    {
        if ($this->_redisVersion !== null && $this->_replicaCmd !== null) {
            return;
        }
        $serverInfo = $this->info('server');
        $this->_redisVersion = $serverInfo['redis_version'];
        // Redis v7+ renames the replica command to 'replicas' instead of 'slaves'
        $this->_replicaCmd = version_compare($this->_redisVersion, '7.0.0', '>=') ? 'replicas' : 'slaves';
    }

    /**
     * @return Credis_Sentinel
     * @deprecated
     */
    public function forceStandalone()
    {
        $this->_standAlone = true;
        return $this;
    }

    /**
     * Discover the master node automatically and return an instance of Credis_Client that connects to the master
     *
     * @param string $name
     * @return Credis_Client
     * @throws CredisException
     */
    public function createMasterClient($name)
    {
        $master = $this->getMasterAddressByName($name);
        if (!isset($master[0]) || !isset($master[1])) {
            throw new CredisException('Master not found');
        }
        return new Credis_Client($master[0], $master[1], $this->_timeout, $this->_persistent, $this->_db, $this->_password, $this->_username);
    }

    /**
     * If a Credis_Client object exists for a master, return it. Otherwise create one and return it
     * @param string $name
     * @return Credis_Client
     */
    public function getMasterClient($name)
    {
        if (!isset($this->_master[$name])) {
            $this->_master[$name] = $this->createMasterClient($name);
        }
        return $this->_master[$name];
    }

    /**
     * Discover the slave nodes automatically and return an array of Credis_Client objects
     *
     * @param string $name
     * @return Credis_Client[]
     * @throws CredisException
     */
    public function createSlaveClients($name)
    {
        $slaves = $this->slaves($name);
        $workingSlaves = [];
        foreach ($slaves as $slaveRaw) {
            $replica = [];
            for ($i = 0; $i < count($slaveRaw) - 1; $i += 2) {
                $replica[$slaveRaw[$i]] = $slaveRaw[$i + 1];
            }

            $flags = $replica['flags'] ?? null;
            if ($flags === null) {
                throw new CredisException('Can\' retrieve slave status');
            }

            if (($replica['master-link-status'] ?? '') === 'err') {
                continue;
            }

            $flags = explode(',', $flags);
            if (!array_intersect($flags, ['s_down', 'o_down', 'disconnected'])) {
                $workingSlaves[] = new Credis_Client($replica['ip'], $replica['port'], $this->_timeout, $this->_persistent, $this->_db, $this->_password, $this->_username);
            }
        }
        return $workingSlaves;
    }

    /**
     * If an array of Credis_Client objects exist for a set of slaves, return them. Otherwise create and return them
     * @param string $name
     * @return Credis_Client[]
     */
    public function getSlaveClients($name)
    {
        if (!isset($this->_slaves[$name])) {
            $this->_slaves[$name] = $this->createSlaveClients($name);
        }
        return $this->_slaves[$name];
    }

    /**
     * Catch-all method
     * @param string $name
     * @param array $args
     * @return mixed
     */
    public function __call($name, $args)
    {
        array_unshift($args, $name);
        return call_user_func(array($this->_client, 'sentinel'), $args);
    }

    /**
     * get information block for the sentinel instance
     *
     * @param string|NUll $section
     *
     * @return array
     */
    public function info($section = null)
    {
        if ($section) {
            return $this->_client->info($section);
        }
        return $this->_client->info();
    }

    /**
     * Return information about all registered master servers
     * @return mixed
     */
    public function masters()
    {
        return $this->_client->sentinel('masters');
    }

    /**
     * Return all information for slaves that are associated with a single master
     * @param string $name
     * @return mixed
     */
    public function slaves($name)
    {
        if ($this->_replicaCmd === null) {
            $this->detectRedisVersion();
        }
        return $this->_client->sentinel($this->_replicaCmd, $name);
    }

    /**
     * Get the information for a specific master
     * @param string $name
     * @return mixed
     */
    public function master($name)
    {
        return $this->_client->sentinel('master', $name);
    }

    /**
     * Get the hostname and port for a specific master
     * @param string $name
     * @return mixed
     */
    public function getMasterAddressByName($name)
    {
        return $this->_client->sentinel('get-master-addr-by-name', $name);
    }

    /**
     * Check if the Sentinel is still responding
     * @return string|Credis_Client
     */
    public function ping()
    {
        return $this->_client->ping();
    }

    /**
     * Perform an auto-failover which will re-elect another master and make the current master a slave
     * @param string $name
     * @return mixed
     */
    public function failover($name)
    {
        return $this->_client->sentinel('failover', $name);
    }

    /**
     * @return string
     */
    public function getHost()
    {
        return $this->_client->getHost();
    }

    /**
     * @return int
     */
    public function getPort()
    {
        return $this->_client->getPort();
    }
}
