<?php

declare(strict_types=1);

class Awardit_Magento_Redis {
    const DSN_QUERY_KEYS = [
        "persistent" => null,
        "prefix" => null,
        // default 0 (ie. infinite) so we provide another default
        "connectTimeout" => "2.5", // s
        "readTimeout" => "2.5", // s
        "retryInterval" => "5", // ms
    ];
    /**
     * List over non-string parameters, all have to be string in config for
     * magento to not fail.
     */
    const DSN_TYPES = [
        "port" => "int",
        "connectTimeout" => "float", // s
        "retryInterval" => "int", // ms
        "readTimeout" => "float", // s
        "persistent" => "string",
    ];

    /**
     * @param Array<string, mixed> $queryKeys List of query keys with key being
     * the key and value being the default. Null will not provide a default
     */
    public static function parseDsn(string $dsn, array $queryKeys): array {
        $auth = null;
        $query = [];
        $dsn = parse_url($dsn);

        if( ! $dsn) {
            throw new Exception("Malformed Redis DSN");
        }

        if( ! array_key_exists("host", $dsn)) {
            throw new Exception("Missing host in Redis DSN");
        }

        parse_str($dsn["query"] ?? "", $query);

        $config = [
            "host" => $dsn["host"],
            // Custom
            "prefix" => trim($dsn["path"] ?? "", " /"),
        ];

        if( ! empty($dsn["port"])) {
            $config["port"] = $dsn["port"];
        }

        if( ! empty($dsn["user"])) {
            $config["auth"] = [$dsn["user"], $dsn["pass"] ?? ""];
        }

        foreach(array_merge(self::DSN_QUERY_KEYS, $queryKeys) as $key => $default) {
            if( ! empty($query[$key])) {
                $config[$key] = $query[$key];
            }
            else if($default !== null) {
                $config[$key] = $default;
            }
        }

        return $config;
    }

    /**
     * Casts configuration options to proper types, magento will have them all
     * stored as strings.
     *
     * @param Array<string, mixed> $config
     * @param Array<string, 'int'|'string'|'float'> $types
     */
    public static function castConfigOptions(array $config, array $types): array {
        foreach(array_merge(self::DSN_TYPES, $types) as $key => $type) {
            if(array_key_exists($key, $config)) {
                switch($type) {
                    case "int":
                        $config[$key] = (int)$config[$key];
                        break;
                    case "float":
                        $config[$key] = (float)$config[$key];
                        break;
                }
            }
        }

        return $config;
    }

    /**
     * @param array{
     *   host:string,
     *   port?:int,
     *   connectTimeout?:float,
     *   retryInterval?:int,
     *   readTimeout?:float,
     *   auth?:mixed,
     *   persistent?:string,
     * } $config
     */
    public static function connect(array $config, string $prefix): Redis {
        // TODO: Use config as first argument once we have phpredis
        // version >= 6 and skip connect/auth
        $redis = new Redis();

        if(array_key_exists("persistent", $config)) {
            $persistentId = $config["persistent"];

            /**
             * @psalm-suppress TooManyArguments The port or read-timeout is missing.
             */
            if( ! $redis->pconnect(
                $config["host"],
                $config["port"] ?? 6379,
                $config["connectTimeout"] ?? 5.0, // timeout, s
                $persistentId,
                $config["retryInterval"] ?? 5, // ms
                $config["readTimeout"] ?? 5.0, // s
            )) {
                throw new RuntimeException(sprintf(
                    "Failed to connect using a persistent connection '%s' to Redis: %s",
                    $persistentId,
                    $redis->getLastError() ?: "unknown error",
                ));
            }
        }
        else {
            if( ! $redis->connect(
                $config["host"],
                $config["port"] ?? 6379,
                $config["connectTimeout"] ?? 5.0, // timeout, s
                null, // reserved
                $config["retryInterval"] ?? 5, // ms
                $config["readTimeout"] ?? 5.0, // s
            )) {
                throw new RuntimeException(sprintf(
                    "Failed to connect to Redis: %s",
                    $redis->getLastError() ?: "unknown error",
                ));
            }
        }

        if(array_key_exists("auth", $config)) {
            if( ! $redis->auth($config["auth"])) {
                throw new RuntimeException("Failed to authenticate with Redis server");
            }
        }

        if( ! empty($prefix) && ! $redis->setOption(Redis::OPT_PREFIX, $prefix.":")) {
            throw new RuntimeException("Failed to set Redis key prefix");
        }

        if( ! $redis->setOption(Redis::OPT_SCAN, Redis::SCAN_PREFIX)) {
            throw new RuntimeException("Failed to set Redis scan key prefix");
        }

        if( ! $redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY)) {
            throw new RuntimeException("Failed to set Redis scan retry behavior");
        }

        return $redis;
    }

    public static function compress(string $data): string {
        // TODO: How to configure this?
        // FIXME: Implement
        // TODO: Needs a lower bound for when it will no longer compress

        return $data;
    }

    public static function uncompress(string $compressed): string {
        // FIXME: Implement
        return $compressed;
    }
}
