<?php

namespace traits;

use classes\redis;
use Exception;
use Predis\Client as PredisClient;

trait redis_t
{
    /**
     * The Redis scheme
     * @var string
     */
    protected string $redis_scheme = 'tcp';
    /**
     * The Redis host
     * @var string
     */
    protected string $redis_host = '';
    /**
     * The Redis port
     * @var int
     */
    protected int $redis_port = 6379;
    /**
     * The Redis password
     * @var string
     */
    protected string $redis_password = '';
    /**
     * The Redis database
     * @var int
     */
    protected int $redis_database = 0;
    /**
     * The object types that can be stored in the Redis server, this is used to prevent unauthorized objects from being stored
     * @var array
     */
    private array $object_types = [
        'order', // The order objects (orders table in the database)
    ];
    /**
     * Get the Redis client
     * @var PredisClient
     */
    private PredisClient $redis;

    /**
     * Does the key exist in the Redis server
     * @param string $key
     * @return bool
     */
    public function exists(string $key): bool
    {
        if (!self::is_connected()) {
            self::connect();
        }
        return $this->redis->exists($key);
    }

    /**
     * Get the Redis client
     * @return PredisClient
     * @throws Exception
     */
    public function get_client(): PredisClient
    {
        if (!self::is_connected()) {
            self::connect();
        }
        return $this->redis;
    }

    /**
     * Check if the Redis server is connected
     * @return bool
     */
    public function is_connected(): bool
    {
        return isset($this->redis);
    }

    /**
     * Connect to the Redis server
     * @throws Exception
     */
    public function connect(): self
    {

        $this->redis = new PredisClient([
            'scheme' => $this->redis_scheme,
            'host' => $this->redis_host,
            'port' => $this->redis_port,
            'password' => $this->redis_password,
            'database' => $this->redis_database
        ]);

        // Check if the connection was successful
        if (!self::is_connected()) {
            throw new Exception('Could not connect to the Redis server');
        }

        return $this;
    }

    /**
     * Add a pre-defined object to the Redis server
     * @param string $object_type
     * @param string $key
     * @param string $value
     * @param int $expiration
     * @return redis|redis_t
     * @throws Exception
     */
    public function add_object(string $object_type, string $key, string $value, int $expiration = 60): self
    {
        // Check if the object type is valid
        if (!self::is_valid_object_type($object_type)) {
            throw new Exception('Invalid object type');
        }
        // Set the object in the Redis server
        $this->setEx($object_type . ':' . $key, $value, $expiration);
        return $this;
    }

    /**
     * Check if the object type is valid
     * @param string $key
     * @return bool
     */
    private function is_valid_object_type(string $key): bool
    {
        return in_array(self::get_object_type($key), $this->object_types);
    }

    /**
     * Get the object type
     * @param string $key
     * @return string
     */
    private function get_object_type(string $key): string
    {
        return explode(':', $key)[0];
    }

    /**
     * Set the value in the Redis server with an expiration time
     * @param string $key
     * @param string $value
     * @param int $expiration The expiration time in seconds
     * @return redis|redis_t
     */
    public function setEx(string $key, string $value, int $expiration = 60): self
    {
        if (!self::is_connected()) {
            self::connect();
        }
        $this->redis->setex($key, $expiration, $value);
        return $this;
    }

    /**
     * Delete a pre-defined object from the Redis server
     * @param string $object_type
     * @param string $key
     * @return redis|redis_t
     * @throws Exception
     */
    public function delete_object(string $object_type, string $key): self
    {
        // Check if the object type is valid
        if (!self::is_valid_object_type($object_type)) {
            throw new Exception('Invalid object type');
        }
        // Delete the object from the Redis server
        $this->delete($object_type . ':' . $key);
        return $this;
    }

    /**
     * Delete the value from the Redis server
     * @param string $key
     * @return redis|redis_t
     */
    public function delete(string $key): self
    {
        if (!self::is_connected()) {
            self::connect();
        }
        $this->redis->del($key);
        return $this;
    }

    /**
     * Get a pre-defined object from the Redis server
     * @param string $object_type
     * @param string $key
     * @return string|null
     * @throws Exception
     */
    public function get_object(string $object_type, string $key): string|null
    {
        // Check if the object type is valid
        if (!self::is_valid_object_type($object_type)) {
            throw new Exception('Invalid object type');
        }
        return $this->get($object_type . ':' . $key);
    }

    /**
     * Get the value from the Redis server
     * @param string $key
     * @return string|null
     */
    public function get(string $key): string|null
    {
        if (!self::is_connected()) {
            self::connect();
        }
        return $this->redis->get($key);
    }

    /**
     * Disconnect from the Redis server when the object is destroyed
     */
    public function __destruct()
    {
        if (self::is_connected()) {
            self::disconnect();
        }
    }

    /**
     * Disconnect from the Redis server
     */
    public function disconnect(): void
    {
        $this->redis->disconnect();
    }

    /**
     * Set array key in Redis
     * @param string $key
     * @param array $value
     * @return redis|redis_t
     */
    public function set_array(string $key, array $value): self
    {
        $this->set($key, json_encode($value));
        return $this;
    }

    /**
     * Set the value in the Redis server
     * @param string $key
     * @param string $value
     * @return redis|redis_t
     */
    public function set(string $key, string $value): self
    {
        if (!self::is_connected()) {
            self::connect();
        }
        $this->redis->set($key, $value);
        return $this;
    }

    /**
     * Clear keys from Redis
     * @param string|array $keys
     * @return redis|redis_t
     */
    public function clear_keys(array|string $keys): self
    {
        if (!is_array($keys)) {
            // Remove all keys matching the pattern
            $keys = $this->get_keys($keys);
        }
        foreach ( $keys as $key ) {
            $this->delete($key);
        }
        return $this;
    }

    /**
     * Get keys from Redis
     * @param string $key The key to search for (supports wildcards)
     * @return array
     */
    public function get_keys(string $key): array
    {
        // Get all keys from the cache
        return $this->redis->keys($key);
    }

    /**
     * Get all keys from the Redis server
     * @return array
     */
    public function keys(): array
    {
        return $this->redis->keys('*');
    }

    /**
     * Get arrays from Redis
     * @param string $key The key to search for (supports wildcards)
     * @return array|null
     */
    public function get_arrays(string $key): array|null
    {
        $keys = $this->get_keys($key);
        $arrays = [];
        foreach ( $keys as $key ) {
            $arrays[] = $this->get_array($key);
        }
        return $arrays;
    }

    /**
     * Get array key from Redis
     * @param string $key
     * @return array|null
     */
    public function get_array(string $key): array|null
    {
        // Get the array from the cache
        $value = $this->get($key);
        // Check if the value is not null
        if ($value === null) {
            return null;
        }
        // Check if the value is not a valid JSON
        if (!self::is_json($value)) {
            return null;
        }
        return json_decode($value, true);
    }

    /**
     * Check if the value is a valid JSON
     * @param string $value
     * @return bool
     */
    private function is_json(string $value): bool
    {
        json_decode($value);
        return json_last_error() === JSON_ERROR_NONE;
    }

    /**
     * Get the count of matching keys
     * @param string $key The key to search for (supports wildcards)
     * @return int
     */
    public function count_keys(string $key): int
    {
        return count($this->get_keys($key));
    }
}