<?php

declare(strict_types=1);

namespace Mautic\CoreBundle\Helper;

use Mautic\CoreBundle\Predis\Command\Unlink;
use Mautic\CoreBundle\Predis\Replication\MasterOnlyStrategy;
use Mautic\CoreBundle\Predis\Replication\StrategyConfig;
use Predis\Client;
use Predis\Cluster\ClusterStrategy;
use Predis\Connection\Cluster\PredisCluster;
use Predis\Connection\Cluster\RedisCluster;
use Predis\Connection\Replication\SentinelReplication;

/**
 * Helper functions for simpler operations with arrays.
 */
class PRedisConnectionHelper
{
    /**
     * Transform the redis url config key into an array if needed
     * If possible also resolve the configured redis hostname to multiple IP addresses.
     *
     * @param mixed $configuredUrls a string or an array of redis endpoints to connect to
     */
    public static function getRedisEndpoints($configuredUrls): iterable
    {
        if (is_iterable($configuredUrls)) {
            // assume arrays are already in the correct format
            return $configuredUrls;
        }
        $parsed = parse_url($configuredUrls);
        if (!$parsed) {
            return [$configuredUrls];
        }

        // resolve hostnames ahead of time to support dns records with multiple ip addresses
        // we need to provide each one to predis separately or it will just use a single one
        $resolvedArray = gethostbynamel($parsed['host']);
        if (!$resolvedArray) {
            return [$configuredUrls];
        }

        // this will return an array of associative arrays which is supported by Predis
        return array_map(function ($i) use ($parsed) {
            $parsed['host'] = $i;

            return $parsed;
        }, $resolvedArray);
    }

    /**
     * Transform the redis mautic config to an options array consumable by PRedis.
     *
     * @param array $redisConfiguration mautic's redis configuration
     */
    public static function makeRedisOptions(array $redisConfiguration, string $prefix = ''): array
    {
        $redisOptions = [];

        if ($prefix) {
            $redisOptions['prefix'] = $prefix;
        }

        if (!empty($redisConfiguration['replication'])) {
            $redisOptions['replication'] = $redisConfiguration['replication'];
            $redisOptions['service']     = $redisConfiguration['service'] ?? 'mymaster';
        }

        if (!empty($redisConfiguration['password'])) {
            $redisOptions['parameters'] = ['password' => $redisConfiguration['password']];
        }

        foreach (['cluster', 'scheme', 'ssl'] as $key) {
            if (isset($redisConfiguration[$key])) {
                $redisOptions[$key] = $redisConfiguration[$key];
            }
        }

        return $redisOptions;
    }

    /**
     * @param mixed[] $endpoints
     * @param mixed[] $inputOptions
     */
    public static function createClient(array $endpoints, array $inputOptions): Client
    {
        $replication = $inputOptions['replication'] ?? null;

        if ('sentinel' === $replication) {
            $inputOptions['aggregate'] = fn ($sentinels, $options): SentinelReplication => new SentinelReplication(
                $options->service,
                $sentinels,
                $options->connections,
                new MasterOnlyStrategy(StrategyConfig::fromArray($inputOptions))
            );
        }

        $inputOptions['commands'] = array_merge(
            $inputOptions['commands'] ?? [],
            [Unlink::ID => Unlink::class]
        );

        // Convert single-endpoint array to string to avoid Predis 3 aggregate connection error
        // This is to maintain compatibility with Predis 3 which expects a string for single endpoint
        // or an array of endpoints for multiple connections.
        if (1 === count($endpoints) && is_string(reset($endpoints))) {
            $endpoints = reset($endpoints);
        }

        $client  = new Client($endpoints, $inputOptions);

        $connection = $client->getConnection();

        if ($connection instanceof RedisCluster || $connection instanceof PredisCluster) {
            $clusterStrategy = $connection->getClusterStrategy();

            if ($clusterStrategy instanceof ClusterStrategy && !in_array(Unlink::ID, $clusterStrategy->getSupportedCommands())) {
                $clusterStrategy->setCommandHandler(Unlink::ID, [$clusterStrategy, 'getKeyFromAllArguments']);
            }
        }

        return $client;
    }
}
