<?php

namespace FleekDash\Core;

/**
 * Cache Invalidation Manager
 *
 * Centralized cache invalidation with debouncing and consolidation.
 * Prevents duplicate invalidations from multiple WordPress hooks
 * and handles deadlock-safe database operations.
 *
 * @package FleekDash\Core
 * @since 2.2.0
 */
class CacheInvalidationManager
{
    /**
     * Singleton instance
     */
    private static ?self $instance = null;

    /**
     * Track which invalidations have been scheduled this request
     *
     * @var array<string, bool>
     */
    private array $scheduledInvalidations = [];

    /**
     * Invalidation version key for cache staleness detection
     * Shared across all requests via transient
     */
    private const INVALIDATION_VERSION_KEY = 'fleekdash_cache_invalidation_version';

    /**
     * Posts list transients + REST ETag transients (RestApiCacheFilters).
     * Must be cleared together or browsers keep 304 + stale JSON for GET /posts.
     *
     * @var array<int, string>
     */
    private const POSTS_LIST_AND_ETAG_CACHE_PATTERNS = [
        '_transient_fleekdash_posts_list_%',
        '_transient_timeout_fleekdash_posts_list_%',
        '_transient_fleekdash_etag_%',
        '_transient_timeout_fleekdash_etag_%',
    ];

    /**
     * Track if shutdown handler is registered
     */
    private bool $shutdownRegistered = false;

    /**
     * Pending invalidation patterns to execute at shutdown
     *
     * @var array<string, array>
     */
    private array $pendingPatterns = [];

    /**
     * Get singleton instance
     */
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * Private constructor for singleton
     */
    private function __construct()
    {
        // Register shutdown handler on first use
        $this->registerShutdownHandler();
    }

    /**
     * Register shutdown handler to process pending invalidations
     */
    private function registerShutdownHandler(): void
    {
        if ($this->shutdownRegistered) {
            return;
        }

        // Use WordPress shutdown action for better compatibility
        \add_action('shutdown', [$this, 'processQueuedInvalidations'], 999);

        $this->shutdownRegistered = true;
    }

    /**
     * Schedule cache invalidation (debounced)
     *
     * Multiple calls with the same key during a single request
     * will only result in one actual invalidation.
     *
     * @param string $invalidation_key Unique key for this invalidation type
     * @param array $patterns Array of LIKE patterns to delete
     * @param bool $immediate If true, execute immediately instead of at shutdown
     * @return bool True if scheduled, false if already scheduled
     */
    public function scheduleInvalidation(string $invalidation_key, array $patterns, bool $immediate = false): bool
    {
        // Check if already scheduled this request
        if (isset($this->scheduledInvalidations[$invalidation_key])) {
            if (\function_exists('fleekdash_log_debug')) {
                \fleekdash_log_debug(
                    sprintf('[CacheInvalidationManager] Skipping duplicate invalidation: %s', $invalidation_key)
                );
            }
            return false;
        }

        // Mark as scheduled
        $this->scheduledInvalidations[$invalidation_key] = true;

        // Update invalidation version for cache staleness detection
        // This allows concurrent requests to detect stale cache even if it still exists
        // Uses shared transient so all requests see the same version
        // Use time() directly as version (always incrementing, no read-modify-write)
        // This eliminates race conditions where multiple concurrent requests read the same version
        // and write the same incremented value, causing version counter to skip numbers
        $current_time = time();

        // Store only one transient (remove redundancy)
        // Using time() as version ensures it's always incrementing and atomic (no race conditions)
        // Store timestamp directly in INVALIDATION_VERSION_KEY (no need for separate _timestamp key)
        \set_transient(self::INVALIDATION_VERSION_KEY, $current_time, DAY_IN_SECONDS);

        if ($immediate) {
            // Execute immediately with deadlock safety
            return $this->executeInvalidation($patterns, $invalidation_key);
        }

        // Queue for shutdown (batched execution)
        $this->pendingPatterns[$invalidation_key] = $patterns;

        if (\function_exists('fleekdash_log_debug')) {
            \fleekdash_log_debug(
                sprintf('[CacheInvalidationManager] Scheduled invalidation: %s (%d patterns)', $invalidation_key, count($patterns))
            );
        }

        return true;
    }

    /**
     * Execute cache invalidation with deadlock-safe queries
     *
     * @param array $patterns LIKE patterns to delete
     * @param string $description Description for logging
     * @return bool Success
     */
    private function executeInvalidation(array $patterns, string $description = 'Cache'): bool
    {
        global $wpdb;

        if (empty($patterns)) {
            return true;
        }

        $queries = [];

        foreach ($patterns as $pattern) {
            // Build the prepared DELETE query
            if (strpos($pattern, '_transient_') === 0) {
                // Transient pattern - also delete timeout
                $timeout_pattern = str_replace('_transient_', '_transient_timeout_', $pattern);

                $queries[] = $wpdb->prepare(
                    "DELETE FROM {$wpdb->options}
                     WHERE option_name LIKE %s
                     OR option_name LIKE %s",
                    $pattern,
                    $timeout_pattern
                );
            } else {
                // Regular option pattern
                $queries[] = $wpdb->prepare(
                    "DELETE FROM {$wpdb->options}
                     WHERE option_name LIKE %s",
                    $pattern
                );
            }
        }

        // Execute with deadlock safety
        $result = DeadlockSafeQuery::batchDelete($queries, $description);

        if (\function_exists('fleekdash_log_debug')) {
            \fleekdash_log_debug(
                sprintf(
                    '[CacheInvalidationManager] Executed %s: %d queries, %d rows deleted, success: %s',
                    $description,
                    $result['query_count'],
                    $result['total_deleted'],
                    $result['success'] ? 'yes' : 'no'
                )
            );
        }

        return $result['success'];
    }

    /**
     * Process all queued invalidations at shutdown
     *
     * @internal Called by WordPress shutdown action
     */
    public function processQueuedInvalidations(): void
    {
        if (empty($this->pendingPatterns)) {
            return;
        }

        if (\function_exists('fleekdash_log_debug')) {
            \fleekdash_log_debug(
                sprintf('[CacheInvalidationManager] Processing %d queued invalidations', count($this->pendingPatterns))
            );
        }

        // Consolidate all patterns to minimize queries
        $all_patterns = [];

        foreach ($this->pendingPatterns as $patterns) {
            foreach ($patterns as $pattern) {
                // Use pattern as key to dedupe
                $all_patterns[$pattern] = true;
            }
        }

        // Execute consolidated invalidation
        $this->executeInvalidation(array_keys($all_patterns), 'Consolidated cache invalidation');

        // Clear pending
        $this->pendingPatterns = [];
    }

    /**
     * Invalidate analytics cache (debounced)
     *
     * Called when content changes (posts, comments, users)
     */
    public function invalidateAnalyticsCache(): void
    {
        $patterns = [
            // Analytics transients
            '_transient_fleekdash_analytics_%',
            // Analytics timestamps
            'fleekdash_analytics_%_timestamp',
        ];

        $this->scheduleInvalidation('analytics_cache', $patterns);
    }

    /**
     * Invalidate content-specific analytics cache (debounced)
     *
     * More targeted invalidation for content changes
     */
    public function invalidateContentCache(): void
    {
        $patterns = [
            // Content stats
            '_transient_fleekdash_analytics__content%',
            '_transient_fleekdash_analytics__stats%',
            '_transient_fleekdash_analytics__popular_posts%',
            '_transient_fleekdash_analytics__kpi_summary%',
            // Timestamps
            'fleekdash_analytics__content%_timestamp',
            'fleekdash_analytics__stats%_timestamp',
            'fleekdash_analytics__kpi_summary%_timestamp',
        ];

        $this->scheduleInvalidation('content_cache', $patterns);
    }

    /**
     * Invalidate stats cache (debounced)
     *
     * Called when posts/comments/users change
     */
    public function invalidateStatsCache(): void
    {
        $patterns = [
            '_transient_fleekdash_stats_%',
            // Legacy stats transients (before StatsCache::TRANSIENT_KEY_PREFIX)
            '_transient_posts_by_date_%',
            '_transient_posts_by_author_%',
            '_transient_posts_by_taxonomy_%',
            '_transient_comments_by_date_%',
            '_transient_user_registrations_%',
            '_transient_dashboard_stats%',
            '_transient_large_dataset_%',
            '_transient_chart_data_%',
        ];

        $this->scheduleInvalidation('stats_cache', $patterns);
    }

    /**
     * Invalidate all FleekDash caches (debounced)
     *
     * Use sparingly - only for full cache clear operations
     */
    public function invalidateAllCaches(): void
    {
        $patterns = [
            // All FleekDash transients
            '_transient_fleekdash_%',
            '_transient_fleekdash_stats_%',
            '_transient_posts_by_date_%',
            '_transient_posts_by_author_%',
            '_transient_posts_by_taxonomy_%',
            '_transient_comments_by_date_%',
            '_transient_user_registrations_%',
            '_transient_dashboard_stats%',
            '_transient_large_dataset_%',
            '_transient_chart_data_%',
            '_transient_bridge_%',
            // All timestamps
            'fleekdash_%_timestamp',
        ];

        $this->scheduleInvalidation('all_caches', $patterns, true); // Execute immediately
    }

    /**
     * Check if an invalidation is already scheduled
     *
     * @param string $invalidation_key Key to check
     * @return bool True if already scheduled
     */
    public function isScheduled(string $invalidation_key): bool
    {
        return isset($this->scheduled_invalidations[$invalidation_key]);
    }

    /**
     * Check if a cache key matches invalidation patterns and should be considered stale
     *
     * This prevents race conditions where:
     * 1. Request A schedules invalidation (but executes at shutdown)
     * 2. Request B reads cache (still exists, returns stale data)
     * 3. Request C reads cache (still exists, returns stale data)
     * 4. Shutdown: Cache is deleted
     * 5. Request D: Cache missing, regenerates
     *
     * By checking invalidation version, we can detect if cache was created
     * before an invalidation was scheduled, even if the cache still exists.
     *
     * @param string $cache_key Cache key to check (without _transient_ prefix)
     * @param int $cache_timestamp Timestamp when cache was created
     * @return bool True if cache should be considered stale
     */
    public static function isCacheStale(string $cache_key, int $cache_timestamp): bool
    {
        // Get invalidation timestamp directly from INVALIDATION_VERSION_KEY
        // Removed redundant _timestamp key - INVALIDATION_VERSION_KEY already stores the timestamp
        $invalidation_timestamp = \get_transient(self::INVALIDATION_VERSION_KEY);
        if ($invalidation_timestamp === false) {
            return false; // No invalidations have occurred - cache is fresh
        }

        // Check if this cache key matches any invalidation patterns
        $invalidation_patterns = [
            'fleekdash_tags_',
            'fleekdash_labels_',
            'fleekdash_taxonomies_',
            'fleekdash_authors_',
            'fleekdash_posts_list_',
        ];

        foreach ($invalidation_patterns as $pattern) {
            // Cache key matches invalidation pattern and was created before or at same time as invalidation
            if (strpos($cache_key, $pattern) === 0 && $cache_timestamp <= $invalidation_timestamp) {
                return true; // Cache is stale - invalidation happened at or after cache creation
            }
        }

        return false; // Cache is fresh - created after last invalidation
    }

    /**
     * Reset scheduled invalidations (useful for testing)
     */
    public function reset(): void
    {
        $this->scheduledInvalidations = [];
        $this->pendingPatterns = [];
        \delete_transient(self::INVALIDATION_VERSION_KEY);
    }

    /**
     * Setup WordPress hooks for automatic cache invalidation
     *
     * This replaces the hooks in AnalyticsCacheService and StatsCache
     * to ensure only ONE invalidation happens per event.
     */
    public static function setupHooks(): void
    {
        $instance = self::getInstance();

        // Content hooks - use low priority to run after other plugins
        \add_action('save_post', [$instance, 'onContentChange'], 999);
        \add_action('deleted_post', [$instance, 'onContentChange'], 999);
        \add_action('trashed_post', [$instance, 'onContentChange'], 999);
        \add_action('wp_insert_post', [$instance, 'onContentChange'], 999);

        // Comment hooks
        \add_action('wp_insert_comment', [$instance, 'onContentChange'], 999);
        \add_action('edit_comment', [$instance, 'onContentChange'], 999);
        \add_action('deleted_comment', [$instance, 'onContentChange'], 999);
        \add_action('transition_comment_status', [$instance, 'onContentChange'], 999);

        // User hooks
        \add_action('user_register', [$instance, 'onUserChange'], 999);
        \add_action('profile_update', [$instance, 'onUserChange'], 999);
        \add_action('delete_user', [$instance, 'onUserChange'], 999);

        // Post status transition - only once
        \add_action('transition_post_status', [$instance, 'onContentChange'], 999);

        // Term hooks (tags, categories, custom taxonomies)
        \add_action('created_term', [$instance, 'onTermChange'], 999);
        \add_action('edited_term', [$instance, 'onTermChange'], 999);
        \add_action('delete_term', [$instance, 'onTermChange'], 999);
    }

    /**
     * Handler for content changes (posts, comments)
     */
    public function onContentChange(): void
    {
        $this->invalidateContentCache();
        $this->invalidateStatsCache();
        $this->invalidatePostsListCache();
    }

    /**
     * Handler for user changes
     */
    public function onUserChange(): void
    {
        $this->invalidateStatsCache();
        $this->invalidatePostsMetadataCache(); // Authors cache

        // Only invalidate user-specific analytics patterns
        $patterns = [
            '_transient_fleekdash_analytics__user_registrations%',
            '_transient_fleekdash_analytics__kpi_summary%',
            'fleekdash_analytics__user_registrations%_timestamp',
        ];

        $this->scheduleInvalidation('user_analytics_cache', $patterns);
    }

    /**
     * Handler for term changes (tags, categories, custom taxonomies)
     */
    public function onTermChange(): void
    {
        $this->invalidatePostsMetadataCache();
        $this->invalidatePostsListCache(); // Taxonomy filters affect posts list
    }

    /**
     * Invalidate posts list transient cache
     *
     * Called when posts/comments change (save, delete, status transition)
     * or when terms change (affects taxonomy filters)
     */
    public function invalidatePostsListCache(): void
    {
        $this->scheduleInvalidation(
            'posts_list_cache',
            self::POSTS_LIST_AND_ETAG_CACHE_PATTERNS,
        );
    }

    /**
     * Flush posts list + REST ETag caches immediately (no shutdown wait).
     * Use after REST mutations that bypass wp_insert_post (e.g. direct date DB update).
     */
    public function flushPostsListCachesNow(): void
    {
        \set_transient(self::INVALIDATION_VERSION_KEY, time(), DAY_IN_SECONDS);
        $this->executeInvalidation(
            self::POSTS_LIST_AND_ETAG_CACHE_PATTERNS,
            'posts_list_immediate_flush',
        );
    }

    /**
     * Invalidate posts metadata cache (tags, labels, taxonomies, authors)
     *
     * Called when terms or users change
     */
    public function invalidatePostsMetadataCache(): void
    {
        $patterns = [
            '_transient_fleekdash_tags_%',
            '_transient_fleekdash_labels_%',
            '_transient_fleekdash_taxonomies_%',
            '_transient_fleekdash_authors_%',
        ];

        $this->scheduleInvalidation('posts_metadata_cache', $patterns);
    }
}
