<?php

declare(strict_types=1);

namespace ProRank\SEO\Core\Performance;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Read-only diagnostics for the WordPress persistent object cache.
 *
 * Produces a single structured health report: whether a persistent object
 * cache is active, which backend, the state of the object-cache.php drop-in,
 * a real-config Redis connection probe, key Redis INFO metrics, and a list of
 * actionable flags.
 *
 * This class performs NO destructive actions. The only writes it makes are a
 * short-TTL set/get/delete on a dedicated probe key (the same pattern WordPress
 * core uses for cache self-tests).
 */
class ObjectCacheHealth {

    /** Latency (ms) above which a local cache round-trip is considered slow. */
    private const LATENCY_WARN_MS = 5.0;

    /** Hit ratio (%) below which the cache is considered underperforming. */
    private const HIT_RATIO_WARN = 80.0;

    /** Memory fragmentation ratio above which Redis is considered fragmented. */
    private const FRAGMENTATION_WARN = 1.5;

    /** Recognised drop-in signatures → human label. */
    private const KNOWN_DROPINS = [
        'Object Cache Pro'        => 'Object Cache Pro',
        'RedisCachePro'           => 'Object Cache Pro',
        'Redis Object Cache'      => 'Redis Object Cache (Till Krüss)',
        'WP_Redis'                => 'Redis Object Cache',
        'W3 Total Cache'          => 'W3 Total Cache',
        'w3-total-cache'          => 'W3 Total Cache',
        'LiteSpeed'               => 'LiteSpeed Cache',
        'Memcached'               => 'Memcached drop-in',
        'WP Rocket'               => 'WP Rocket',
        'Docket Cache'            => 'Docket Cache',
        'ProRank'                 => 'ProRank SEO',
    ];

    /**
     * Build the full health report.
     *
     * @return array
     */
    public function report(): array {
        $persistent       = function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache();
        $backend          = $this->detect_backend();
        $dropin           = $this->inspect_dropin();
        $redis_configured = $this->has_explicit_redis_config();
        $connection       = $this->probe_connection();
        // Collect Redis INFO whenever Redis is the active backend OR explicit
        // Redis config exists (so "configured but inactive" is still diagnosable).
        $redis = ($backend === 'redis' || $redis_configured) ? $this->collect_redis_info() : [];

        $flags = $this->evaluate_flags($persistent, $backend, $dropin, $connection, $redis, $redis_configured);

        $status = 'good';
        foreach ($flags as $flag) {
            if ($flag['severity'] === 'critical' || $flag['severity'] === 'high') {
                $status = 'critical';
                break;
            }
            if ($flag['severity'] === 'medium') {
                $status = 'warning';
            }
        }

        return [
            'generated_at'     => current_time('mysql'),
            'status'           => $status,
            'persistent_cache' => $persistent,
            'redis_configured' => $redis_configured,
            'backend'          => $backend,
            'backend_label'    => $this->backend_label($backend),
            'dropin'           => $dropin,
            'connection'       => $connection,
            'redis'            => $redis,
            'flags'            => $flags,
        ];
    }

    /**
     * Detect the active cache backend.
     *
     * @return string redis|memcached|apcu|litespeed|array|none|unknown
     */
    public function detect_backend(): string {
        if (!function_exists('wp_using_ext_object_cache') || !wp_using_ext_object_cache()) {
            return 'none';
        }

        $obj = $GLOBALS['wp_object_cache'] ?? null;

        // The ProRank drop-in's class is always WP_Object_Cache (private
        // $backend), so class-name sniffing cannot distinguish Redis from a
        // degraded array fallback. Ask it directly when available.
        if (is_object($obj) && method_exists($obj, 'prorank_backend')) {
            $b = (string) $obj->prorank_backend();
            if (in_array($b, array('redis', 'memcached', 'array'), true)) {
                return $b;
            }
        }

        $class = is_object($obj) ? get_class($obj) : '';

        if (stripos($class, 'redis') !== false || defined('WP_REDIS_VERSION')) {
            return 'redis';
        }
        if (stripos($class, 'memcached') !== false) {
            return 'memcached';
        }
        if (stripos($class, 'memcache') !== false) {
            return 'memcache';
        }
        if (stripos($class, 'apcu') !== false || stripos($class, 'apc') !== false) {
            return 'apcu';
        }
        if (defined('LSCWP_V') || stripos($class, 'litespeed') !== false) {
            return 'litespeed';
        }

        return 'unknown';
    }

    public function backend_label(string $backend): string {
        $map = [
            'redis'     => 'Redis',
            'memcached' => 'Memcached',
            'memcache'  => 'Memcache',
            'apcu'      => 'APCu',
            'litespeed' => 'LiteSpeed',
            'array'     => 'In-memory (non-persistent)',
            'none'      => 'None (database)',
            'unknown'   => 'Unknown',
        ];
        return $map[$backend] ?? ucfirst($backend);
    }

    /**
     * Inspect the wp-content/object-cache.php drop-in.
     *
     * @return array{state:string, exists:bool, owner:string, path:string, readable:bool}
     */
    /**
     * Ownership/version facts from the drop-in manager (single source of
     * truth for the ProRank marker + bundled-vs-installed version).
     *
     * @return array{is_prorank_owned:bool, dropin_version:?int, bundled_version:int, update_available:bool}
     */
    private function ownership_facts(): array {
        $defaults = [
            'is_prorank_owned' => false,
            'dropin_version'   => null,
            'bundled_version'  => 0,
            'update_available' => false,
        ];
        if (!class_exists('\\ProRank\\SEO\\Core\\Performance\\ObjectCache\\ObjectCacheDropinManager')) {
            $file = __DIR__ . '/ObjectCache/ObjectCacheDropinManager.php';
            if (is_readable($file)) {
                require_once $file;
            }
        }
        if (!class_exists('\\ProRank\\SEO\\Core\\Performance\\ObjectCache\\ObjectCacheDropinManager')) {
            return $defaults;
        }
        try {
            $m = new \ProRank\SEO\Core\Performance\ObjectCache\ObjectCacheDropinManager();
            $i = $m->inspect();
            return [
                'is_prorank_owned' => (bool) ($i['is_prorank_owned'] ?? false),
                'dropin_version'   => $i['installed_version'] ?? null,
                'bundled_version'  => (int) ($i['bundled_version'] ?? 0),
                'update_available' => (bool) ($i['update_available'] ?? false),
            ];
        } catch (\Throwable $e) {
            return $defaults;
        }
    }

    public function inspect_dropin(): array {
        $path = (defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR : ABSPATH . 'wp-content') . '/object-cache.php';
        $own  = $this->ownership_facts();

        if (!file_exists($path)) {
            return array_merge([
                'state'    => 'missing',
                'exists'   => false,
                'owner'    => '',
                'path'     => $path,
                'readable' => false,
            ], $own);
        }

        $readable = is_readable($path);
        $contents = $readable ? (string) @file_get_contents($path) : '';

        $owner = '';
        foreach (self::KNOWN_DROPINS as $needle => $label) {
            if ($contents !== '' && stripos($contents, $needle) !== false) {
                $owner = $label;
                break;
            }
        }

        // A drop-in is valid if it either defines wp_cache_init() inline OR is a
        // loader-style stub that includes its real implementation (Object Cache
        // Pro, Docket Cache and others ship a thin stub that require()s api.php).
        // Only a non-PHP / unreadable file, or an unrecognised file that does
        // neither, is treated as broken.
        $defines_init = $contents !== '' && preg_match('/function\s+wp_cache_init\s*\(/i', $contents) === 1;
        $php_open     = $contents !== '' && strpos($contents, '<?php') !== false;
        // Any require/include statement makes this a loader-style stub. We do
        // NOT require the include target to be quoted/parenthesised right after
        // the keyword — patterns like `include_once __DIR__ . "/loader.php";`
        // are common and legitimate.
        $is_loader    = $contents !== '' && preg_match(
            '/\b(require|require_once|include|include_once)\b/i',
            $contents
        ) === 1;

        if (!$readable || $contents === '') {
            $state = 'unreadable';
        } elseif (!$php_open) {
            $state = 'invalid';
        } elseif ($defines_init) {
            // Concrete drop-in: known signature => valid, otherwise foreign.
            $state = ($owner !== '') ? 'valid' : 'foreign';
        } elseif ($owner !== '' && $is_loader) {
            // Recognised loader stub (e.g. Object Cache Pro) — valid but the
            // real implementation lives in an included file we don't inspect.
            $state = 'valid_unverified';
        } elseif ($owner !== '') {
            // Recognised owner, no inline init, no obvious loader pattern —
            // still trust the signature rather than calling it broken.
            $state = 'valid_unverified';
        } elseif ($is_loader) {
            // Unknown owner but clearly a loader — not broken, just unrecognised.
            $state = 'foreign';
        } else {
            $state = 'invalid';
        }

        return array_merge([
            'state'    => $state,
            'exists'   => true,
            'owner'    => $owner !== '' ? $owner : 'Unknown',
            'path'     => $path,
            'readable' => $readable,
        ], $own);
    }

    /**
     * Resolve the real Redis connection parameters from every known source.
     *
     * @return array
     */
    public function resolve_redis_config(): array {
        $config = [
            'scheme'   => 'tcp',
            'host'     => '127.0.0.1',
            'port'     => 6379,
            'username' => null,
            'password' => null,
            'database' => 0,
            'path'     => null,
            'timeout'  => 1.0,
            'source'   => 'default',
        ];

        // Object Cache Pro / array-style config.
        if (defined('WP_REDIS_CONFIG')) {
            $wpc = constant('WP_REDIS_CONFIG');
            if (is_array($wpc)) {
                $config['source'] = 'WP_REDIS_CONFIG';
                foreach (['scheme', 'host', 'port', 'username', 'password', 'database', 'path', 'timeout'] as $k) {
                    if (isset($wpc[$k]) && $wpc[$k] !== '') {
                        $config[$k] = $wpc[$k];
                    }
                }
                // Object Cache Pro nests auth under 'username'/'password' or a
                // combined 'parameters' array depending on version.
                if (isset($wpc['parameters']) && is_array($wpc['parameters'])) {
                    foreach (['host', 'port', 'username', 'password', 'database', 'path', 'timeout'] as $k) {
                        if (isset($wpc['parameters'][$k]) && $wpc['parameters'][$k] !== '') {
                            $config[$k] = $wpc['parameters'][$k];
                        }
                    }
                }
            }
        }

        // Individual constants (Redis Object Cache plugin convention).
        $const_map = [
            'WP_REDIS_SCHEME'   => 'scheme',
            'WP_REDIS_HOST'     => 'host',
            'WP_REDIS_PORT'     => 'port',
            'WP_REDIS_USERNAME' => 'username',
            'WP_REDIS_PASSWORD' => 'password',
            'WP_REDIS_DATABASE' => 'database',
            'WP_REDIS_PATH'     => 'path',
            'WP_REDIS_TIMEOUT'  => 'timeout',
        ];
        foreach ($const_map as $const => $key) {
            if (defined($const)) {
                $val = constant($const);
                if ($val !== '' && $val !== null) {
                    $config[$key] = $val;
                    if ($config['source'] === 'default') {
                        $config['source'] = 'WP_REDIS_* constants';
                    }
                }
            }
        }

        // Host-provided global (Pantheon, some managed hosts).
        if (isset($GLOBALS['redis_server']) && is_array($GLOBALS['redis_server'])) {
            $rs = $GLOBALS['redis_server'];
            $config['source'] = '$redis_server global';
            if (!empty($rs['host'])) {
                $config['host'] = $rs['host'];
            }
            if (!empty($rs['port'])) {
                $config['port'] = $rs['port'];
            }
            if (!empty($rs['auth'])) {
                $config['password'] = $rs['auth'];
            }
        }

        // Unix socket path takes precedence over host/port when set.
        if (!empty($config['path'])) {
            $config['scheme'] = 'unix';
        }

        // Normalise TLS scheme aliases.
        $scheme = strtolower((string) $config['scheme']);
        if (in_array($scheme, ['tls', 'rediss', 'ssl'], true)) {
            $config['scheme'] = 'tls';
        }

        $config['port'] = (int) $config['port'];
        $config['database'] = (int) $config['database'];
        $config['timeout'] = (float) $config['timeout'];

        return $config;
    }

    /**
     * Whether an explicit Redis configuration exists (regardless of whether the
     * object cache is currently active). Lets us probe "Redis configured but
     * not active" — the most actionable failure case.
     *
     * @return bool
     */
    public function has_explicit_redis_config(): bool {
        if (defined('WP_REDIS_CONFIG') && is_array(constant('WP_REDIS_CONFIG'))) {
            return true;
        }
        foreach (['WP_REDIS_HOST', 'WP_REDIS_PATH', 'WP_REDIS_PASSWORD', 'WP_REDIS_PORT', 'WP_REDIS_SCHEME'] as $const) {
            if (defined($const) && constant($const) !== '' && constant($const) !== null) {
                return true;
            }
        }
        if (isset($GLOBALS['redis_server']) && is_array($GLOBALS['redis_server']) && !empty($GLOBALS['redis_server']['host'])) {
            return true;
        }
        return false;
    }

    /**
     * Probe the cache connection. For Redis, connect with the real resolved
     * config; for any backend, time a round-trip via the WP cache API.
     *
     * @return array
     */
    public function probe_connection(): array {
        $result = [
            'tested'     => false,
            'connected'  => false,
            'latency_ms' => null,
            'method'     => '',
            'error'      => '',
            'config'     => [],
        ];

        $backend = $this->detect_backend();

        // Probe Redis directly when it is the active backend OR when explicit
        // Redis config exists (so a configured-but-not-loaded Redis is tested).
        if (($backend === 'redis' || $this->has_explicit_redis_config()) && class_exists('Redis')) {
            $cfg = $this->resolve_redis_config();
            // Never expose the password in the report.
            $result['config'] = [
                'scheme'   => $cfg['scheme'],
                'host'     => $cfg['scheme'] === 'unix' ? null : $cfg['host'],
                'port'     => $cfg['scheme'] === 'unix' ? null : $cfg['port'],
                'path'     => $cfg['path'],
                'database' => $cfg['database'],
                'source'   => $cfg['source'],
                'username' => $cfg['username'] !== null && $cfg['username'] !== '' ? (string) $cfg['username'] : null,
                'auth'     => $cfg['password'] !== null && $cfg['password'] !== '',
                'tls'      => $cfg['scheme'] === 'tls',
            ];
            $result['method'] = 'redis-direct';
            $result['tested'] = true;

            try {
                $redis = new \Redis();
                $start = microtime(true);

                if ($cfg['scheme'] === 'unix' && !empty($cfg['path'])) {
                    $ok = @$redis->connect((string) $cfg['path']);
                } else {
                    // phpredis accepts a tls:// prefix on the host for TLS.
                    $host = (string) $cfg['host'];
                    if ($cfg['scheme'] === 'tls' && strpos($host, 'tls://') !== 0 && strpos($host, 'rediss://') !== 0) {
                        $host = 'tls://' . $host;
                    }
                    $ok = @$redis->connect($host, (int) $cfg['port'], min(2.0, (float) $cfg['timeout']));
                }

                if (!$ok) {
                    $result['error'] = 'Redis connect() failed';
                    return $result;
                }

                // Strict auth check — ACL username (Redis 6+) or password only.
                if ($cfg['password'] !== null && $cfg['password'] !== '') {
                    if ($cfg['username'] !== null && $cfg['username'] !== '') {
                        $auth_ok = @$redis->auth([(string) $cfg['username'], (string) $cfg['password']]);
                    } else {
                        $auth_ok = @$redis->auth((string) $cfg['password']);
                    }
                    if ($auth_ok !== true) {
                        $result['error'] = 'Redis authentication failed';
                        @$redis->close();
                        return $result;
                    }
                }

                // Strict select check.
                if ((int) $cfg['database'] > 0) {
                    $select_ok = @$redis->select((int) $cfg['database']);
                    if ($select_ok !== true) {
                        $result['error'] = 'Redis SELECT of database ' . (int) $cfg['database'] . ' failed';
                        @$redis->close();
                        return $result;
                    }
                }

                // Strict ping check — phpredis returns true or the string '+PONG'/'PONG'
                // depending on version; anything else is a failure.
                $pong = @$redis->ping();
                $ping_ok = ($pong === true)
                    || (is_string($pong) && strcasecmp(trim($pong, " \t\n\r\0\x0B+"), 'PONG') === 0);

                $result['latency_ms'] = round((microtime(true) - $start) * 1000, 2);
                $result['connected']  = $ping_ok;
                if (!$ping_ok) {
                    $result['error'] = 'Redis PING did not return PONG';
                }
                @$redis->close();
            } catch (\Throwable $e) {
                $result['error'] = $e->getMessage();
                $result['connected'] = false;
            }

            return $result;
        }

        // Generic round-trip via the WP cache API (works for any backend).
        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
            $result['method'] = 'wp-cache-api';
            $result['tested'] = true;
            $key = 'prorank_oc_probe_' . wp_generate_password(8, false);
            $start = microtime(true);
            wp_cache_set($key, 'ok', 'prorank-health', 10);
            $val = wp_cache_get($key, 'prorank-health');
            wp_cache_delete($key, 'prorank-health');
            $result['latency_ms'] = round((microtime(true) - $start) * 1000, 2);
            $result['connected'] = ($val === 'ok');
            if (!$result['connected']) {
                $result['error'] = 'Cache round-trip did not return the stored value';
            }
        }

        return $result;
    }

    /**
     * Collect read-only Redis INFO metrics via a direct connection.
     *
     * @return array
     */
    public function collect_redis_info(): array {
        $info = [];
        if (!class_exists('Redis')) {
            return $info;
        }

        $cfg = $this->resolve_redis_config();

        try {
            $redis = new \Redis();
            if ($cfg['scheme'] === 'unix' && !empty($cfg['path'])) {
                $ok = @$redis->connect((string) $cfg['path']);
            } else {
                $host = (string) $cfg['host'];
                if ($cfg['scheme'] === 'tls' && strpos($host, 'tls://') !== 0 && strpos($host, 'rediss://') !== 0) {
                    $host = 'tls://' . $host;
                }
                $ok = @$redis->connect($host, (int) $cfg['port'], min(2.0, (float) $cfg['timeout']));
            }
            if (!$ok) {
                return $info;
            }
            if ($cfg['password'] !== null && $cfg['password'] !== '') {
                if ($cfg['username'] !== null && $cfg['username'] !== '') {
                    $auth_ok = @$redis->auth([(string) $cfg['username'], (string) $cfg['password']]);
                } else {
                    $auth_ok = @$redis->auth((string) $cfg['password']);
                }
                if ($auth_ok !== true) {
                    @$redis->close();
                    return $info;
                }
            }
            if ((int) $cfg['database'] > 0 && @$redis->select((int) $cfg['database']) !== true) {
                @$redis->close();
                return $info;
            }

            $raw = @$redis->info();
            if (is_array($raw)) {
                $hits   = (int) ($raw['keyspace_hits'] ?? 0);
                $misses = (int) ($raw['keyspace_misses'] ?? 0);
                $total  = $hits + $misses;

                $info = [
                    'version'                  => $raw['redis_version'] ?? null,
                    'mode'                     => $raw['redis_mode'] ?? 'standalone',
                    'uptime_seconds'           => (int) ($raw['uptime_in_seconds'] ?? 0),
                    'used_memory'              => (int) ($raw['used_memory'] ?? 0),
                    'used_memory_human'        => $raw['used_memory_human'] ?? null,
                    'maxmemory'                => (int) ($raw['maxmemory'] ?? 0),
                    'maxmemory_human'          => $raw['maxmemory_human'] ?? null,
                    'maxmemory_policy'         => $raw['maxmemory_policy'] ?? 'unknown',
                    'mem_fragmentation_ratio'  => (float) ($raw['mem_fragmentation_ratio'] ?? 0),
                    'keyspace_hits'            => $hits,
                    'keyspace_misses'          => $misses,
                    'hit_ratio'                => $total > 0 ? round(($hits / $total) * 100, 2) : null,
                    'evicted_keys'             => (int) ($raw['evicted_keys'] ?? 0),
                    'expired_keys'             => (int) ($raw['expired_keys'] ?? 0),
                    'rejected_connections'     => (int) ($raw['rejected_connections'] ?? 0),
                    'connected_clients'        => (int) ($raw['connected_clients'] ?? 0),
                    'instantaneous_ops_per_sec'=> (int) ($raw['instantaneous_ops_per_sec'] ?? 0),
                ];
            }
            @$redis->close();
        } catch (\Throwable $e) {
            $info['error'] = $e->getMessage();
        }

        return $info;
    }

    /**
     * Turn the collected state into actionable flags.
     *
     * @return array<int, array{code:string, severity:string, message:string, recommendation:string}>
     */
    private function evaluate_flags(bool $persistent, string $backend, array $dropin, array $connection, array $redis, bool $redis_configured = false): array {
        $flags = [];

        // The ProRank drop-in is installed but it fell back to its in-process
        // array cache (Redis/Memcached unreachable or missing). wp_using_ext_
        // object_cache() can still report true, so without this the "no
        // persistent cache" problem would be silently masked.
        if (!empty($dropin['is_prorank_owned']) && $backend === 'array') {
            $flags[] = [
                'code'           => 'prorank_dropin_array_fallback',
                'severity'       => 'high',
                'message'        => 'The ProRank object-cache drop-in is installed but has fallen back to a non-persistent in-memory cache (Redis/Memcached is unreachable or unavailable). There is effectively NO persistent object cache.',
                'recommendation' => 'Fix the Redis/Memcached connection (service running, correct host/port/socket/credentials) or remove the ProRank drop-in. Until then every request re-runs DB queries.',
            ];
        }

        // Redis is configured but the PHP Redis client is missing — the
        // connection genuinely cannot be tested (distinct from "tested and
        // failed"). phpredis OR Relay satisfies the client requirement.
        $redis_client_available = class_exists('Redis') || class_exists('Relay\\Relay');
        if ($redis_configured && !$redis_client_available) {
            $flags[] = [
                'code'           => 'redis_client_unavailable',
                'severity'       => 'high',
                'message'        => 'Redis is configured in wp-config.php but no PHP Redis client (phpredis or Relay) is installed, so the connection could not be tested.',
                'recommendation' => 'Install the PHP "redis" (phpredis) extension on the server. Without it WordPress cannot use Redis for object caching even though it is configured.',
            ];
        }

        if (!$persistent && $redis_configured) {
            // The most actionable failure: Redis is configured in wp-config.php
            // but the object cache is not active (drop-in missing or failed).
            $conn_tested = !empty($connection['tested']);
            $conn_ok     = !empty($connection['connected']);

            if (!$conn_tested) {
                // Could not test (no client, or no testable transport).
                $detail = $redis_client_available
                    ? ' and the connection could not be tested.'
                    : ' and the connection could not be tested because the PHP Redis client is missing.';
                $reco = $redis_client_available
                    ? 'Install the object-cache.php drop-in via a Redis object cache plugin, then re-check.'
                    : 'Install the PHP Redis client (phpredis), then install the object-cache.php drop-in.';
            } elseif ($conn_ok) {
                $detail = ' even though Redis is reachable. The object-cache.php drop-in is likely missing or failed to load.';
                $reco   = 'Install/repair the object-cache.php drop-in (via your Redis object cache plugin) so WordPress actually uses the reachable Redis server.';
            } else {
                $detail = ' and the Redis connection test failed' . (!empty($connection['error']) ? ': ' . $connection['error'] : '.');
                $reco   = 'Verify the Redis service is running and the configured host/port/socket/credentials are correct, then install the object cache drop-in.';
            }

            $flags[] = [
                'code'           => 'redis_configured_but_inactive',
                'severity'       => 'high',
                'message'        => 'Redis is configured in wp-config.php but WordPress is NOT using a persistent object cache' . $detail,
                'recommendation' => $reco,
            ];
        } elseif (!$persistent) {
            $flags[] = [
                'code'           => 'no_persistent_object_cache',
                'severity'       => 'high',
                'message'        => 'No persistent object cache is active. Every page load re-runs database queries that could be cached.',
                'recommendation' => 'Install and configure a persistent object cache (Redis recommended) if your host supports it. This is the single biggest server-side TTFB improvement for logged-in, cart and AJAX traffic.',
            ];
        }

        if (($dropin['state'] ?? '') === 'foreign') {
            $flags[] = [
                'code'           => 'foreign_dropin',
                'severity'       => 'medium',
                'message'        => 'An object-cache.php drop-in is present but its origin is not recognised (' . ($dropin['owner'] ?? 'Unknown') . ').',
                'recommendation' => 'Confirm this drop-in is intentional and maintained. Unrecognised drop-ins cannot be health-checked or safely managed by ProRank.',
            ];
        }
        if (in_array(($dropin['state'] ?? ''), ['invalid', 'unreadable'], true)) {
            $flags[] = [
                'code'           => 'invalid_dropin',
                'severity'       => 'high',
                'message'        => 'The object-cache.php drop-in is present but appears invalid or unreadable (it does not define wp_cache_init()).',
                'recommendation' => 'A broken drop-in can disable caching site-wide or cause errors. Replace or remove it via your object cache plugin.',
            ];
        }

        if ($backend === 'redis' && !empty($connection['tested']) && empty($connection['connected'])) {
            $flags[] = [
                'code'           => 'connection_failure',
                'severity'       => 'critical',
                'message'        => 'Redis is configured but the connection test failed' . ($connection['error'] !== '' ? ': ' . $connection['error'] : '.'),
                'recommendation' => 'Verify the Redis service is running and the host/port/socket/password in wp-config.php are correct. Until this is fixed the object cache is effectively disabled.',
            ];
        }

        if ($connection['latency_ms'] !== null && $connection['latency_ms'] > self::LATENCY_WARN_MS) {
            $flags[] = [
                'code'           => 'high_latency',
                'severity'       => 'medium',
                'message'        => sprintf('Cache round-trip latency is high (%.2f ms).', $connection['latency_ms']),
                'recommendation' => 'High latency erodes the benefit of object caching. Prefer a Unix socket or same-host Redis over a remote TCP connection, and check server load.',
            ];
        }

        if (!empty($redis)) {
            $policy = $redis['maxmemory_policy'] ?? 'unknown';
            $maxmem = (int) ($redis['maxmemory'] ?? 0);

            if ($maxmem > 0 && $policy === 'noeviction') {
                $flags[] = [
                    'code'           => 'noeviction_policy',
                    'severity'       => 'high',
                    'message'        => 'Redis has a memory limit but maxmemory-policy is "noeviction" — once full, cache writes will fail with OOM errors.',
                    'recommendation' => 'Set maxmemory-policy to "allkeys-lru" (or "allkeys-lfu") so Redis evicts old keys instead of rejecting writes.',
                ];
            }

            if ((int) ($redis['evicted_keys'] ?? 0) > 0) {
                $flags[] = [
                    'code'           => 'evictions_present',
                    'severity'       => 'medium',
                    'message'        => sprintf('Redis has evicted %d key(s) — the cache may be too small for the working set.', (int) $redis['evicted_keys']),
                    'recommendation' => 'Increase the Redis maxmemory allocation, or reduce cache churn. Frequent evictions lower the hit ratio.',
                ];
            }

            if ((int) ($redis['rejected_connections'] ?? 0) > 0) {
                $flags[] = [
                    'code'           => 'rejected_connections',
                    'severity'       => 'medium',
                    'message'        => sprintf('Redis has rejected %d connection(s) — the maxclients limit may be too low.', (int) $redis['rejected_connections']),
                    'recommendation' => 'Raise the Redis "maxclients" setting or reduce connection churn (use persistent connections).',
                ];
            }

            if ($redis['hit_ratio'] !== null && ($redis['keyspace_hits'] + $redis['keyspace_misses']) > 1000 && $redis['hit_ratio'] < self::HIT_RATIO_WARN) {
                $flags[] = [
                    'code'           => 'low_hit_ratio',
                    'severity'       => 'medium',
                    'message'        => sprintf('Redis keyspace hit ratio is low (%.1f%%).', $redis['hit_ratio']),
                    'recommendation' => 'A low hit ratio suggests short TTLs, frequent flushes, or an undersized cache. Review cache invalidation and memory allocation.',
                ];
            }

            if ((float) ($redis['mem_fragmentation_ratio'] ?? 0) > self::FRAGMENTATION_WARN) {
                $flags[] = [
                    'code'           => 'high_fragmentation',
                    'severity'       => 'low',
                    'message'        => sprintf('Redis memory fragmentation ratio is high (%.2f).', (float) $redis['mem_fragmentation_ratio']),
                    'recommendation' => 'High fragmentation wastes RAM. Consider enabling activedefrag in Redis, or restarting Redis during a maintenance window.',
                ];
            }
        }

        return $flags;
    }
}
