<?php

/**
 * Schema Map Cache
 *
 * Two-layer cache for site-wide schema aggregation responses.
 *
 *  1. wp_cache (in-memory, request-scoped or object-cache-backed) for sub-ms hits
 *  2. transient (persistent) fallback so repeat cold requests don't pay the
 *     full collector cost
 *
 * Cache keys are scoped by (blog_id, page, per_page) so a multisite install
 * and pagination permutations never collide.
 *
 * Invalidation bumps a single version counter, rendering all prior entries
 * stale without having to walk and delete every transient. This keeps the
 * write path on save_post / option updates cheap and predictable.
 *
 * @package ProRank\SEO\Core\Schema
 * @since   2.2.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core\Schema;

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


class SchemaMapCache {

    /**
     * Cache group for wp_cache_* calls.
     */
    public const GROUP = 'prorank_schema_map';

    /**
     * Option name storing the current cache version.
     */
    public const VERSION_OPTION = 'prorank_schema_map_version';

    /**
     * Transient TTL (seconds). Aggregation results are cheap to recompute
     * so we keep the TTL short enough that schema drift from third-party
     * plugins never lingers for too long.
     */
    public const TTL = 6 * HOUR_IN_SECONDS;

    /**
     * Fetch a cached payload.
     *
     * @param int $page     Page number (1-indexed).
     * @param int $per_page Items per page.
     * @return array|null   Cached payload, or null on miss.
     */
    public function get(int $page, int $per_page): ?array {
        $key = $this->build_key($page, $per_page);

        $found = false;
        $memory = wp_cache_get($key, self::GROUP, false, $found);
        if ($found && is_array($memory)) {
            return $memory;
        }

        $transient = get_transient($key);
        if (is_array($transient)) {
            // Promote back into memory so repeat hits in the same request don't pay again.
            wp_cache_set($key, $transient, self::GROUP, self::TTL);
            return $transient;
        }

        return null;
    }

    /**
     * Store a payload in both layers.
     *
     * @param int   $page     Page number.
     * @param int   $per_page Items per page.
     * @param array $payload  Serialisable payload (flat, no objects).
     * @return void
     */
    public function set(int $page, int $per_page, array $payload): void {
        $key = $this->build_key($page, $per_page);
        wp_cache_set($key, $payload, self::GROUP, self::TTL);
        set_transient($key, $payload, self::TTL);
    }

    /**
     * Flush every cached payload by bumping the version counter.
     *
     * Faster than scanning and deleting every transient and works uniformly
     * whether an object cache is present or not. Stale transients still on
     * disk will be garbage-collected by WP on their next expiry check, or
     * the next $wpdb clean cycle.
     *
     * @return void
     */
    public function flush(): void {
        $current = (int) get_option(self::VERSION_OPTION, 1);
        $next = $current + 1;
        update_option(self::VERSION_OPTION, $next, false);
        // Also drop the in-memory group so the same request sees the flush.
        // wp_cache_flush_group is only guaranteed when the drop-in supports it,
        // so guard it — the version bump above makes the old keys unreachable anyway.
        if (function_exists('wp_cache_supports') && wp_cache_supports('flush_group')) {
            wp_cache_flush_group(self::GROUP);
        }
    }

    /**
     * Return the current cache version.
     *
     * @return int
     */
    public function version(): int {
        $version = (int) get_option(self::VERSION_OPTION, 1);
        return max(1, $version);
    }

    /**
     * Build the composite cache key.
     *
     * @param int $page     Page number.
     * @param int $per_page Items per page.
     * @return string
     */
    private function build_key(int $page, int $per_page): string {
        $blog_id = function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0;
        $version = $this->version();
        return sprintf(
            'schema_map_v%d_b%d_p%d_pp%d',
            $version,
            $blog_id,
            max(1, $page),
            max(1, $per_page)
        );
    }
}
