<?php
/* ProRank SEO Object Cache drop-in | owner:prorank | v:1 | do-not-edit */
/**
 * ProRank SEO — persistent object cache drop-in (clean-room).
 *
 * STANDALONE: this file is copied verbatim to wp-content/object-cache.php and
 * loads before plugins. It MUST NOT reference ProRank autoloaders, namespaces,
 * module classes, REST controllers or any plugin file. If the ProRank plugin
 * is deactivated this drop-in still works (or fails open to a request-local
 * array cache). It never calls Redis FLUSHDB/FLUSHALL.
 *
 * @package ProRankObjectCacheDropin
 */

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

if ( ! defined( 'PRORANK_OC_DROPIN_VERSION' ) ) {
    define( 'PRORANK_OC_DROPIN_VERSION', 1 );
}

// Defensive: object-cache.php is loaded exactly once by core. If something
// has already defined the object-cache API (core's class, another drop-in,
// or an accidental double-include via a file scanner), bail rather than
// fatally redeclaring. Never break the site.
if ( class_exists( 'WP_Object_Cache', false ) || function_exists( 'wp_cache_init' ) ) {
    return;
}

/**
 * Object cache backed by Redis (phpredis) → Memcached → request-local array.
 *
 * Fail-open: any backend error degrades to the in-memory array for that
 * request; the site never white-screens.
 */
class WP_Object_Cache {

    /** @var array Request-local cache: cache[group][key] = value */
    private $cache = array();

    /** @var array Groups never written to a persistent backend. */
    private $non_persistent_groups = array();

    /** @var array Groups shared across a multisite network (no blog id). */
    private $global_groups = array();

    /** @var string 'redis' | 'memcached' | 'array' */
    private $backend = 'array';

    /** @var \Redis|null */
    private $redis = null;

    /** @var \Memcached|null */
    private $mc = null;

    /** @var string Stable key prefix (salt + site/global context). */
    private $salt = '';

    /** @var int Current blog id for multisite key scoping. */
    private $blog_id = 0;

    /** @var bool */
    private $multisite = false;

    /** @var array Per-request memo of generation counters. */
    private $gen_cache = array();

    /** @var int Hits/misses (Query Monitor / health visibility). */
    public $cache_hits = 0;
    public $cache_misses = 0;

    /** Sentinel returned by the backend layer to mean "not found". */
    private static $MISS;

    /** Sentinel: the backend errored mid-request (distinct from a legit
     *  negative such as add() NX-miss). Callers fail open to request-local. */
    private static $BACKEND_ERR;

    public function __construct() {
        if ( self::$MISS === null ) {
            self::$MISS = new stdClass();
        }
        if ( self::$BACKEND_ERR === null ) {
            self::$BACKEND_ERR = new stdClass();
        }

        $this->multisite = ( function_exists( 'is_multisite' ) && is_multisite() );
        $this->blog_id   = ( function_exists( 'get_current_blog_id' ) ? (int) get_current_blog_id() : 1 );

        $salt = defined( 'WP_CACHE_KEY_SALT' ) ? (string) WP_CACHE_KEY_SALT : '';
        if ( $salt === '' && isset( $GLOBALS['table_prefix'] ) ) {
            $salt = (string) $GLOBALS['table_prefix'];
        }
        $this->salt = $salt;

        $disabled = ( defined( 'WP_REDIS_DISABLED' ) && WP_REDIS_DISABLED )
                 || ( defined( 'PRORANK_OBJECT_CACHE_DISABLED' ) && PRORANK_OBJECT_CACHE_DISABLED );

        if ( $disabled ) {
            $this->backend = 'array';
            return;
        }

        try {
            if ( $this->connect_redis() ) {
                $this->backend = 'redis';
            } elseif ( $this->connect_memcached() ) {
                $this->backend = 'memcached';
            } else {
                $this->backend = 'array';
            }
        } catch ( \Throwable $e ) {
            $this->backend = 'array';
            $this->log( 'init failed: ' . $e->getMessage() );
        }
    }

    /* ------------------------------------------------------------------ *
     * Connection (single attempt, short timeout, no retry storms)
     * ------------------------------------------------------------------ */

    private function read_redis_config() {
        $c = array(
            'scheme'   => 'tcp',
            'host'     => '127.0.0.1',
            'port'     => 6379,
            'path'     => null,
            'username' => null,
            'password' => null,
            'database' => 0,
            'timeout'  => 1.0,
        );

        if ( defined( 'WP_REDIS_CONFIG' ) && is_array( WP_REDIS_CONFIG ) ) {
            $wpc = WP_REDIS_CONFIG;
            foreach ( array( 'scheme', 'host', 'port', 'path', 'username', 'password', 'database', 'timeout' ) as $k ) {
                if ( isset( $wpc[ $k ] ) && $wpc[ $k ] !== '' ) {
                    $c[ $k ] = $wpc[ $k ];
                }
            }
            // Object Cache Pro-style nested parameters (must match the health
            // resolver, otherwise the drop-in passes health but fails live).
            if ( isset( $wpc['parameters'] ) && is_array( $wpc['parameters'] ) ) {
                foreach ( array( 'host', 'port', 'path', 'username', 'password', 'database', 'timeout' ) as $k ) {
                    if ( isset( $wpc['parameters'][ $k ] ) && $wpc['parameters'][ $k ] !== '' ) {
                        $c[ $k ] = $wpc['parameters'][ $k ];
                    }
                }
            }
        }
        $map = array(
            'WP_REDIS_SCHEME'   => 'scheme',
            'WP_REDIS_HOST'     => 'host',
            'WP_REDIS_PORT'     => 'port',
            'WP_REDIS_PATH'     => 'path',
            'WP_REDIS_USERNAME' => 'username',
            'WP_REDIS_PASSWORD' => 'password',
            'WP_REDIS_DATABASE' => 'database',
            'WP_REDIS_TIMEOUT'  => 'timeout',
        );
        foreach ( $map as $const => $key ) {
            if ( defined( $const ) && constant( $const ) !== '' && constant( $const ) !== null ) {
                $c[ $key ] = constant( $const );
            }
        }
        if ( isset( $GLOBALS['redis_server'] ) && is_array( $GLOBALS['redis_server'] ) ) {
            $rs = $GLOBALS['redis_server'];
            if ( ! empty( $rs['host'] ) ) { $c['host'] = $rs['host']; }
            if ( ! empty( $rs['port'] ) ) { $c['port'] = $rs['port']; }
            if ( ! empty( $rs['auth'] ) ) { $c['password'] = $rs['auth']; }
        }
        if ( ! empty( $c['path'] ) ) {
            $c['scheme'] = 'unix';
        }
        $s = strtolower( (string) $c['scheme'] );
        if ( in_array( $s, array( 'tls', 'rediss', 'ssl' ), true ) ) {
            $c['scheme'] = 'tls';
        }
        return $c;
    }

    private function connect_redis() {
        if ( ! class_exists( 'Redis' ) ) {
            return false;
        }
        $c = $this->read_redis_config();
        try {
            $r = new \Redis();
            if ( $c['scheme'] === 'unix' && ! empty( $c['path'] ) ) {
                $ok = @$r->connect( (string) $c['path'] );
            } else {
                $host = (string) $c['host'];
                if ( $c['scheme'] === 'tls' && strpos( $host, 'tls://' ) !== 0 ) {
                    $host = 'tls://' . $host;
                }
                $ok = @$r->connect( $host, (int) $c['port'], min( 2.0, (float) $c['timeout'] ) );
            }
            if ( ! $ok ) {
                return false;
            }
            if ( $c['password'] !== null && $c['password'] !== '' ) {
                $auth = ( $c['username'] !== null && $c['username'] !== '' )
                    ? @$r->auth( array( (string) $c['username'], (string) $c['password'] ) )
                    : @$r->auth( (string) $c['password'] );
                if ( $auth !== true ) {
                    return false;
                }
            }
            if ( (int) $c['database'] > 0 && @$r->select( (int) $c['database'] ) !== true ) {
                return false;
            }
            $pong = @$r->ping();
            $ok   = ( $pong === true ) || ( is_string( $pong ) && strcasecmp( trim( $pong, " \t\n\r\0\x0B+" ), 'PONG' ) === 0 );
            if ( ! $ok ) {
                return false;
            }
            $this->redis = $r;
            return true;
        } catch ( \Throwable $e ) {
            return false;
        }
    }

    private function connect_memcached() {
        if ( ! class_exists( 'Memcached' ) ) {
            return false;
        }
        try {
            $servers = array();
            if ( isset( $GLOBALS['memcached_servers'] ) && is_array( $GLOBALS['memcached_servers'] ) ) {
                foreach ( $GLOBALS['memcached_servers'] as $s ) {
                    if ( is_array( $s ) && isset( $s[0] ) ) {
                        $servers[] = array( $s[0], isset( $s[1] ) ? (int) $s[1] : 11211 );
                    }
                }
            }
            if ( empty( $servers ) ) {
                $servers[] = array( '127.0.0.1', 11211 );
            }
            $m = new \Memcached();
            $m->addServers( $servers );
            // Cheap reachability probe.
            $m->set( $this->salt . ':__prorank_probe', 1, 5 );
            if ( $m->getResultCode() !== \Memcached::RES_SUCCESS ) {
                return false;
            }
            $this->mc = $m;
            return true;
        } catch ( \Throwable $e ) {
            return false;
        }
    }

    /* ------------------------------------------------------------------ *
     * Key building + versioned-namespace invalidation (no FLUSHDB)
     * ------------------------------------------------------------------ */

    private function group( $group ) {
        $group = (string) $group;
        return $group === '' ? 'default' : $group;
    }

    private function is_global( $group ) {
        return isset( $this->global_groups[ $group ] );
    }

    private function is_non_persistent( $group ) {
        return isset( $this->non_persistent_groups[ $group ] );
    }

    /**
     * Backend failed mid-request — degrade to the request-local array for the
     * rest of this request so a Redis/Memcached drop never makes WP cache
     * operations fail, and we don't keep hammering a dead connection.
     */
    private function degrade() {
        $this->backend = 'array';
    }

    /** Site/global scope segment for the key prefix. */
    private function scope( $group ) {
        if ( $this->is_global( $group ) || ! $this->multisite ) {
            return 'g';
        }
        return 'b' . $this->blog_id;
    }

    /**
     * Read a generation counter from the backend (memoised per request).
     * Generation keys are plain integers; bumping one orphans every key that
     * embedded the old value — invalidation without FLUSHDB/SCAN.
     */
    private function generation( $name ) {
        if ( isset( $this->gen_cache[ $name ] ) ) {
            return $this->gen_cache[ $name ];
        }
        $gkey = $this->salt . ':proc-oc:gen:' . $name;
        $val  = 1;
        try {
            if ( $this->backend === 'redis' && $this->redis ) {
                $cur = $this->redis->get( $gkey );
                if ( $cur === false ) {
                    $this->redis->set( $gkey, 1 );
                    $cur = 1;
                }
                $val = (int) $cur;
            } elseif ( $this->backend === 'memcached' && $this->mc ) {
                $cur = $this->mc->get( $gkey );
                if ( $this->mc->getResultCode() === \Memcached::RES_NOTFOUND ) {
                    $this->mc->set( $gkey, 1 );
                    $cur = 1;
                }
                $val = (int) $cur;
            }
        } catch ( \Throwable $e ) {
            $val = 1;
        }
        $this->gen_cache[ $name ] = $val;
        return $val;
    }

    private function bump_generation( $name ) {
        unset( $this->gen_cache[ $name ] );
        $gkey = $this->salt . ':proc-oc:gen:' . $name;
        try {
            if ( $this->backend === 'redis' && $this->redis ) {
                $this->redis->incr( $gkey );
                return true;
            }
            if ( $this->backend === 'memcached' && $this->mc ) {
                if ( $this->mc->increment( $gkey ) === false ) {
                    $this->mc->set( $gkey, 2 );
                }
                return true;
            }
        } catch ( \Throwable $e ) {
            return false;
        }
        return false;
    }

    private function build_key( $key, $group ) {
        $scope     = $this->scope( $group );
        // Universal generation: embedded in EVERY key regardless of scope or
        // blog. Bumping it on flush() invalidates global + all blogs + all
        // groups at once, without FLUSHDB or SCAN — required for correct
        // multisite wp_cache_flush() semantics.
        $all_gen   = $this->generation( 'all' );
        $site_gen  = $this->generation( 'site:' . $scope );
        $group_gen = $this->generation( 'grp:' . $scope . ':' . $group );
        $raw       = $this->salt . ':proc-oc:' . $all_gen . ':' . $scope . ':' . $site_gen . ':' . $group . ':' . $group_gen . ':' . $key;
        // Keep backend keys bounded.
        if ( strlen( $raw ) > 200 ) {
            $raw = $this->salt . ':proc-oc:' . $all_gen . ':' . $scope . ':' . $site_gen . ':' . $group . ':' . $group_gen . ':h:' . md5( $key );
        }
        return $raw;
    }

    /* ------------------------------------------------------------------ *
     * Backend get/set/delete (serialise so stored false != miss)
     * ------------------------------------------------------------------ */

    /**
     * Encode a value for the backend. Integers are stored UNSERIALIZED so
     * Redis INCRBY / Memcached increment work atomically; everything else is
     * serialized (so a stored `false` round-trips and is distinguishable from
     * a genuine miss). A bare-digit string set via set() is still serialized
     * (e.g. s:1:"5";), so it never collides with a real integer.
     */
    private function encode_payload( $value ) {
        return is_int( $value ) ? (string) $value : serialize( $value );
    }

    private function decode_payload( $raw ) {
        if ( $raw === false || $raw === null ) {
            return self::$MISS;
        }
        $val = @unserialize( $raw );
        if ( $val !== false || $raw === 'b:0;' ) {
            return $val;
        }
        // Not serialized data — a bare integer written by set(int)/incr/decr.
        if ( is_string( $raw ) && preg_match( '/^-?\d+$/', $raw ) === 1 ) {
            return (int) $raw;
        }
        return self::$MISS;
    }

    private function backend_get( $key, $group ) {
        if ( $this->backend === 'array' || $this->is_non_persistent( $group ) ) {
            return self::$MISS;
        }
        try {
            $bk = $this->build_key( $key, $group );
            if ( $this->backend === 'redis' && $this->redis ) {
                $raw = $this->redis->get( $bk );
                if ( $raw === false ) {
                    return self::$MISS; // genuine miss
                }
                return $this->decode_payload( $raw );
            }
            if ( $this->backend === 'memcached' && $this->mc ) {
                $val = $this->mc->get( $bk );
                if ( $this->mc->getResultCode() === \Memcached::RES_NOTFOUND ) {
                    return self::$MISS;
                }
                return $val;
            }
        } catch ( \Throwable $e ) {
            $this->log( 'backend_get: ' . $e->getMessage() );
            $this->degrade();
        }
        return self::$MISS;
    }

    private function backend_set( $key, $group, $value, $ttl ) {
        if ( $this->backend === 'array' || $this->is_non_persistent( $group ) ) {
            return true; // request-local only; handled by $this->cache
        }
        try {
            $bk  = $this->build_key( $key, $group );
            $ttl = max( 0, (int) $ttl );
            if ( $this->backend === 'redis' && $this->redis ) {
                $payload = $this->encode_payload( $value );
                return $ttl > 0
                    ? (bool) $this->redis->setex( $bk, $ttl, $payload )
                    : (bool) $this->redis->set( $bk, $payload );
            }
            if ( $this->backend === 'memcached' && $this->mc ) {
                return (bool) $this->mc->set( $bk, $value, $ttl );
            }
        } catch ( \Throwable $e ) {
            $this->log( 'backend_set: ' . $e->getMessage() );
            $this->degrade();
        }
        return false;
    }

    /**
     * Classify a phpredis `false` from a SET NX/XX call.
     * phpredis returns false for BOTH a clean NX/XX miss AND a backend error;
     * the two are told apart by getLastError(). A non-empty last error means a
     * real fault → degrade + BACKEND_ERR. A clean miss returns plain false.
     *
     * @return false|object false = legitimate NX/XX miss, BACKEND_ERR = fault.
     */
    private function redis_false_or_error( $context ) {
        $err = null;
        if ( $this->redis && method_exists( $this->redis, 'getLastError' ) ) {
            $err = $this->redis->getLastError();
        }
        if ( $err !== null && $err !== '' ) {
            $this->log( $context . ': redis error ' . $err );
            $this->degrade();
            return self::$BACKEND_ERR;
        }
        return false;
    }

    /**
     * Classify a Memcached `false`. Only the result codes that genuinely mean
     * "store condition not met" are a legitimate miss; connection/server
     * errors degrade + BACKEND_ERR.
     *
     * @param int[] $miss_codes Result codes that mean a clean (non-error) miss.
     * @return false|object
     */
    private function mc_false_or_error( array $miss_codes, $context ) {
        $rc = $this->mc ? $this->mc->getResultCode() : \Memcached::RES_FAILURE;
        if ( in_array( $rc, $miss_codes, true ) ) {
            return false;
        }
        $this->log( $context . ': memcached result code ' . $rc );
        $this->degrade();
        return self::$BACKEND_ERR;
    }

    /** Atomic add (SET NX / Memcached add). true|false; BACKEND_ERR on fault. */
    private function backend_add( $key, $group, $value, $ttl ) {
        try {
            $bk  = $this->build_key( $key, $group );
            $ttl = max( 0, (int) $ttl );
            if ( $this->backend === 'redis' && $this->redis ) {
                if ( method_exists( $this->redis, 'clearLastError' ) ) {
                    $this->redis->clearLastError();
                }
                $opts = $ttl > 0 ? array( 'nx', 'ex' => $ttl ) : array( 'nx' );
                $res  = $this->redis->set( $bk, $this->encode_payload( $value ), $opts );
                if ( $res ) {
                    return true;
                }
                // false = key already exists (clean NX miss) OR backend error.
                return $this->redis_false_or_error( 'backend_add' );
            }
            if ( $this->backend === 'memcached' && $this->mc ) {
                if ( $this->mc->add( $bk, $value, $ttl ) ) {
                    return true;
                }
                // RES_NOTSTORED = key already exists (legitimate add miss).
                return $this->mc_false_or_error( array( \Memcached::RES_NOTSTORED ), 'backend_add' );
            }
        } catch ( \Throwable $e ) {
            $this->log( 'backend_add: ' . $e->getMessage() );
            $this->degrade();
            return self::$BACKEND_ERR;
        }
        return false;
    }

    /** Atomic replace (SET XX / Memcached replace). true|false; BACKEND_ERR on fault. */
    private function backend_replace( $key, $group, $value, $ttl ) {
        try {
            $bk  = $this->build_key( $key, $group );
            $ttl = max( 0, (int) $ttl );
            if ( $this->backend === 'redis' && $this->redis ) {
                if ( method_exists( $this->redis, 'clearLastError' ) ) {
                    $this->redis->clearLastError();
                }
                $opts = $ttl > 0 ? array( 'xx', 'ex' => $ttl ) : array( 'xx' );
                $res  = $this->redis->set( $bk, $this->encode_payload( $value ), $opts );
                if ( $res ) {
                    return true;
                }
                // false = key absent (clean XX miss) OR backend error.
                return $this->redis_false_or_error( 'backend_replace' );
            }
            if ( $this->backend === 'memcached' && $this->mc ) {
                if ( $this->mc->replace( $bk, $value, $ttl ) ) {
                    return true;
                }
                // RES_NOTSTORED / RES_NOTFOUND = key absent (legit replace miss).
                return $this->mc_false_or_error(
                    array( \Memcached::RES_NOTSTORED, \Memcached::RES_NOTFOUND ),
                    'backend_replace'
                );
            }
        } catch ( \Throwable $e ) {
            $this->log( 'backend_replace: ' . $e->getMessage() );
            $this->degrade();
            return self::$BACKEND_ERR;
        }
        return false;
    }

    /** Atomic counter. int on success; BACKEND_ERR on fault. */
    private function backend_incr( $key, $group, $offset ) {
        try {
            $bk     = $this->build_key( $key, $group );
            $offset = (int) $offset;
            if ( $this->backend === 'redis' && $this->redis ) {
                $new = ( $offset >= 0 )
                    ? $this->redis->incrBy( $bk, $offset )
                    : $this->redis->decrBy( $bk, -$offset );
                if ( $new === false ) {
                    $this->degrade();
                    return self::$BACKEND_ERR;
                }
                if ( $new < 0 ) {
                    $this->redis->set( $bk, 0 );
                    $new = 0;
                }
                return (int) $new;
            }
            if ( $this->backend === 'memcached' && $this->mc ) {
                if ( $offset >= 0 ) {
                    $new = $this->mc->increment( $bk, $offset, $offset );
                } else {
                    $new = $this->mc->decrement( $bk, -$offset, 0 );
                }
                if ( $new === false ) {
                    $this->degrade();
                    return self::$BACKEND_ERR;
                }
                return (int) $new;
            }
        } catch ( \Throwable $e ) {
            $this->log( 'backend_incr: ' . $e->getMessage() );
            $this->degrade();
            return self::$BACKEND_ERR;
        }
        return self::$BACKEND_ERR;
    }

    private function backend_delete( $key, $group ) {
        if ( $this->backend === 'array' || $this->is_non_persistent( $group ) ) {
            return true;
        }
        try {
            $bk = $this->build_key( $key, $group );
            if ( $this->backend === 'redis' && $this->redis ) {
                return (bool) $this->redis->del( $bk );
            }
            if ( $this->backend === 'memcached' && $this->mc ) {
                return (bool) $this->mc->delete( $bk );
            }
        } catch ( \Throwable $e ) {
            $this->log( 'backend_delete: ' . $e->getMessage() );
            $this->degrade();
        }
        return false;
    }

    /* ------------------------------------------------------------------ *
     * Public WP_Object_Cache API
     * ------------------------------------------------------------------ */

    public function get( $key, $group = 'default', $force = false, &$found = null ) {
        $group = $this->group( $group );

        // When there is no persistent backend for this group (array mode, or a
        // non-persistent group) the request-local array IS the authoritative
        // store — $force must NOT bypass it, otherwise add()/replace()/incr()
        // (which read with $force=true) would never see existing values.
        if ( $this->backend === 'array' || $this->is_non_persistent( $group ) ) {
            $force = false;
        }

        $sc = $this->scope( $group );

        if ( ! $force && isset( $this->cache[ $sc ][ $group ] ) && array_key_exists( $key, $this->cache[ $sc ][ $group ] ) ) {
            $found = true;
            $this->cache_hits++;
            $v = $this->cache[ $sc ][ $group ][ $key ];
            return is_object( $v ) ? clone $v : $v;
        }

        $val = $this->backend_get( $key, $group );
        if ( $val === self::$MISS ) {
            $found = false;
            $this->cache_misses++;
            return false;
        }

        $found = true;
        $this->cache_hits++;
        $this->cache[ $sc ][ $group ][ $key ] = $val;
        return is_object( $val ) ? clone $val : $val;
    }

    public function get_multiple( $keys, $group = 'default', $force = false ) {
        $out = array();
        foreach ( (array) $keys as $k ) {
            $out[ $k ] = $this->get( $k, $group, $force );
        }
        return $out;
    }

    public function set( $key, $data, $group = 'default', $expire = 0 ) {
        $group = $this->group( $group );
        if ( is_object( $data ) ) {
            $data = clone $data;
        }
        $sc = $this->scope( $group );
        $this->cache[ $sc ][ $group ][ $key ] = $data;
        $this->backend_set( $key, $group, $data, $expire );
        return true;
    }

    public function set_multiple( $data, $group = 'default', $expire = 0 ) {
        $out = array();
        foreach ( (array) $data as $k => $v ) {
            $out[ $k ] = $this->set( $k, $v, $group, $expire );
        }
        return $out;
    }

    private function persistent_group( $group ) {
        return $this->backend !== 'array' && ! $this->is_non_persistent( $group );
    }

    public function add( $key, $data, $group = 'default', $expire = 0 ) {
        $group = $this->group( $group );

        if ( $this->persistent_group( $group ) ) {
            // Atomic SET NX — safe for lock-like keys under concurrency.
            $ok = $this->backend_add( $key, $group, $data, $expire );
            if ( $ok !== self::$BACKEND_ERR ) {
                if ( $ok === true ) {
                    $this->cache[ $this->scope( $group ) ][ $group ][ $key ] =
                        is_object( $data ) ? clone $data : $data;
                }
                return (bool) $ok;
            }
            // Backend faulted mid-request → fail open to request-local.
        }

        // Request-local store (array mode / non-persistent group / degraded).
        $found = null;
        $this->get( $key, $group, true, $found );
        if ( $found ) {
            return false;
        }
        return $this->set( $key, $data, $group, $expire );
    }

    public function add_multiple( $data, $group = 'default', $expire = 0 ) {
        $out = array();
        foreach ( (array) $data as $k => $v ) {
            $out[ $k ] = $this->add( $k, $v, $group, $expire );
        }
        return $out;
    }

    public function replace( $key, $data, $group = 'default', $expire = 0 ) {
        $group = $this->group( $group );

        if ( $this->persistent_group( $group ) ) {
            // Atomic SET XX.
            $ok = $this->backend_replace( $key, $group, $data, $expire );
            if ( $ok !== self::$BACKEND_ERR ) {
                if ( $ok === true ) {
                    $this->cache[ $this->scope( $group ) ][ $group ][ $key ] =
                        is_object( $data ) ? clone $data : $data;
                }
                return (bool) $ok;
            }
            // Backend faulted mid-request → fail open to request-local.
        }

        $found = null;
        $this->get( $key, $group, true, $found );
        if ( ! $found ) {
            return false;
        }
        return $this->set( $key, $data, $group, $expire );
    }

    public function delete( $key, $group = 'default' ) {
        $group = $this->group( $group );
        unset( $this->cache[ $this->scope( $group ) ][ $group ][ $key ] );
        return $this->backend_delete( $key, $group );
    }

    public function delete_multiple( $keys, $group = 'default' ) {
        $out = array();
        foreach ( (array) $keys as $k ) {
            $out[ $k ] = $this->delete( $k, $group );
        }
        return $out;
    }

    public function incr( $key, $offset = 1, $group = 'default' ) {
        $group  = $this->group( $group );
        $offset = (int) $offset;

        if ( $this->persistent_group( $group ) ) {
            // Atomic INCRBY/DECRBY (auto-creates the counter).
            $new = $this->backend_incr( $key, $group, $offset );
            if ( $new !== self::$BACKEND_ERR ) {
                $this->cache[ $this->scope( $group ) ][ $group ][ $key ] = (int) $new;
                return (int) $new;
            }
            // Backend faulted mid-request → fail open to request-local.
        }

        // Request-local counter (array mode / non-persistent group / degraded).
        $found = null;
        $cur   = $this->get( $key, $group, true, $found );
        $cur   = $found ? (int) $cur : 0;
        $new   = $cur + $offset;
        if ( $new < 0 ) {
            $new = 0;
        }
        $this->set( $key, $new, $group );
        return $new;
    }

    public function decr( $key, $offset = 1, $group = 'default' ) {
        return $this->incr( $key, -abs( (int) $offset ), $group );
    }

    /**
     * Versioned-namespace flush. Bumps the UNIVERSAL generation counter, which
     * is embedded in every key (global, every blog, every group) — so a single
     * bump fully invalidates the whole cache across all multisite scopes.
     * NEVER FLUSHDB / FLUSHALL.
     */
    public function flush() {
        $this->cache = array();
        $bumped = $this->bump_generation( 'all' );
        return ( $bumped || $this->backend === 'array' );
    }

    /** True per-group invalidation via the group generation counter. */
    public function flush_group( $group ) {
        $group = $this->group( $group );
        unset( $this->cache[ $this->scope( $group ) ][ $group ] );
        if ( $this->backend === 'array' || $this->is_non_persistent( $group ) ) {
            return true;
        }
        return (bool) $this->bump_generation( 'grp:' . $this->scope( $group ) . ':' . $group );
    }

    /** Clear only the request-local layer; persistent data untouched. */
    public function flush_runtime() {
        $this->cache     = array();
        $this->gen_cache = array();
        return true;
    }

    public function reset() {
        // Back-compat shim for very old core; behaves like flush_runtime().
        return $this->flush_runtime();
    }

    public function supports( $feature ) {
        switch ( $feature ) {
            case 'add_multiple':
            case 'set_multiple':
            case 'get_multiple':
            case 'delete_multiple':
            case 'flush_runtime':
            case 'flush_group':   // truly implemented via per-group generation
                return true;
            default:
                return false;
        }
    }

    /**
     * Public so ProRank health can report the REAL backend. The drop-in
     * class is always named WP_Object_Cache, so class-name sniffing cannot
     * tell Redis from a degraded array fallback — this can. Returns
     * 'redis' | 'memcached' | 'array'.
     */
    public function prorank_backend() {
        return $this->backend;
    }

    public function add_global_groups( $groups ) {
        foreach ( (array) $groups as $g ) {
            $this->global_groups[ $g ] = true;
        }
    }

    public function add_non_persistent_groups( $groups ) {
        foreach ( (array) $groups as $g ) {
            $this->non_persistent_groups[ $g ] = true;
        }
    }

    public function switch_to_blog( $blog_id ) {
        $this->blog_id   = (int) $blog_id;
        $this->gen_cache = array();
    }

    public function close() {
        return true; // real close on shutdown (registered below)
    }

    public function shutdown() {
        try {
            if ( $this->redis ) {
                @$this->redis->close();
            }
            if ( $this->mc ) {
                @$this->mc->quit();
            }
        } catch ( \Throwable $e ) {
            // best-effort
        }
    }

    private function log( $msg ) {
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'error_log' ) ) {
            error_log( '[ProRank Object Cache] ' . $msg );
        }
    }
}

/* ---------------------------------------------------------------------- *
 * Procedural wp_cache_* API
 * ---------------------------------------------------------------------- */

function wp_cache_init() {
    try {
        $GLOBALS['wp_object_cache'] = new WP_Object_Cache();
    } catch ( \Throwable $e ) {
        // Absolute last resort: never white-screen. A bare object satisfying
        // the API; everything is a request-local miss.
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'error_log' ) ) {
            error_log( '[ProRank Object Cache] wp_cache_init fatal, failing open: ' . $e->getMessage() );
        }
        $GLOBALS['wp_object_cache'] = new WP_Object_Cache();
    }
    if ( function_exists( 'register_shutdown_function' ) ) {
        register_shutdown_function( function () {
            if ( isset( $GLOBALS['wp_object_cache'] ) && method_exists( $GLOBALS['wp_object_cache'], 'shutdown' ) ) {
                $GLOBALS['wp_object_cache']->shutdown();
            }
        } );
    }
}

function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
    return $GLOBALS['wp_object_cache']->get( $key, $group, $force, $found );
}
function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
    return $GLOBALS['wp_object_cache']->get_multiple( $keys, $group, $force );
}
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
    return $GLOBALS['wp_object_cache']->set( $key, $data, $group, (int) $expire );
}
function wp_cache_set_multiple( $data, $group = '', $expire = 0 ) {
    return $GLOBALS['wp_object_cache']->set_multiple( $data, $group, (int) $expire );
}
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
    return $GLOBALS['wp_object_cache']->add( $key, $data, $group, (int) $expire );
}
function wp_cache_add_multiple( $data, $group = '', $expire = 0 ) {
    return $GLOBALS['wp_object_cache']->add_multiple( $data, $group, (int) $expire );
}
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
    return $GLOBALS['wp_object_cache']->replace( $key, $data, $group, (int) $expire );
}
function wp_cache_delete( $key, $group = '' ) {
    return $GLOBALS['wp_object_cache']->delete( $key, $group );
}
function wp_cache_delete_multiple( $keys, $group = '' ) {
    return $GLOBALS['wp_object_cache']->delete_multiple( $keys, $group );
}
function wp_cache_incr( $key, $offset = 1, $group = '' ) {
    return $GLOBALS['wp_object_cache']->incr( $key, $offset, $group );
}
function wp_cache_decr( $key, $offset = 1, $group = '' ) {
    return $GLOBALS['wp_object_cache']->decr( $key, $offset, $group );
}
function wp_cache_flush() {
    return $GLOBALS['wp_object_cache']->flush();
}
function wp_cache_flush_group( $group ) {
    return $GLOBALS['wp_object_cache']->flush_group( $group );
}
function wp_cache_flush_runtime() {
    return $GLOBALS['wp_object_cache']->flush_runtime();
}
function wp_cache_reset() {
    return $GLOBALS['wp_object_cache']->reset();
}
function wp_cache_supports( $feature ) {
    return $GLOBALS['wp_object_cache']->supports( $feature );
}
function wp_cache_prorank_backend() {
    if ( isset( $GLOBALS['wp_object_cache'] ) && method_exists( $GLOBALS['wp_object_cache'], 'prorank_backend' ) ) {
        return $GLOBALS['wp_object_cache']->prorank_backend();
    }
    return 'unknown';
}
function wp_cache_close() {
    return true;
}
function wp_cache_add_global_groups( $groups ) {
    $GLOBALS['wp_object_cache']->add_global_groups( $groups );
}
function wp_cache_add_non_persistent_groups( $groups ) {
    $GLOBALS['wp_object_cache']->add_non_persistent_groups( $groups );
}
function wp_cache_switch_to_blog( $blog_id ) {
    $GLOBALS['wp_object_cache']->switch_to_blog( $blog_id );
    return true;
}
