<?php

/**
 * Static Page Caching Module
 *
 * Implements file-based page caching for improved performance
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Modules\BaseModule;
use WP_Filesystem_Base;

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


/**
 * Cache module class
 */


class CacheModule extends BaseModule {

    /**
     * Required tier for this module
     */
    protected string $feature_tier = 'free';
    
    /**
     * Cache directory path
     *
     * @var string
     */
    private const CACHE_DIR = WP_CONTENT_DIR . '/cache/prorank-seo';
    
    /**
     * Buffer started flag
     *
     * @var bool
     */
    private bool $buffer_started = false;

    /**
     * Whether this request cache payload has already been written.
     *
     * @var bool
     */
    private bool $cache_saved = false;

    /**
     * Accumulates chunked output until a full HTML document is available.
     *
     * @var string
     */
    private string $buffer_capture_accumulator = '';
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'cache';
        $this->name = 'Static Page Cache';
        $this->description = 'File-based page caching for improved performance';
$this->parent_slug = 'performance';
    }
    
    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        // Check if module should run
        if (!$this->should_run()) {
            return;
        }
        
        // Hook for serving cached pages (high priority)
        add_action('template_redirect', [$this, 'serve_cached_page'], 1);
        
        // Hook for starting output buffer
        add_action('template_redirect', [$this, 'start_buffer'], 2);
        
        // Hook for saving cache on shutdown
        add_action('shutdown', [$this, 'save_cache'], 0);
        
        // Cache invalidation hooks
        add_action('save_post', [$this, 'clear_post_cache']);
        add_action('edit_post', [$this, 'clear_post_cache']);
        add_action('delete_post', [$this, 'clear_post_cache']);
        add_action('wp_trash_post', [$this, 'clear_post_cache']);
        add_action('switch_theme', [$this, 'clear_all_cache']);
        add_action('wp_update_nav_menu', [$this, 'clear_all_cache']);
        add_action('update_option_' . 'prorank_settings', [$this, 'clear_all_cache']);
        add_action('comment_post', [$this, 'clear_comment_cache'], 10, 2);
        add_action('edit_comment', [$this, 'clear_comment_cache']);
        add_action('delete_comment', [$this, 'clear_comment_cache']);
        add_action('wp_set_comment_status', [$this, 'clear_comment_cache']);
    }
    
    /**
     * Check if module should run
     *
     * @return bool
     */
    private function should_run(): bool {
        // Check if ModernCacheModule is enabled - avoid conflict
        // ModernCacheModule is the preferred 2025 version
        $plugin = $this->plugin();
        if ($plugin->is_initialized()) {
            try {
                $modules = $plugin->modules();
                $modern_cache = $modules->get_module('modern_cache');
                if ($modern_cache && $modern_cache->is_enabled()) {
                    // ModernCacheModule is active, skip CacheModule to avoid conflict
                    return false;
                }
            } catch (\RuntimeException $e) {
                // Module manager not available yet
            }
        }

        // Check if module is enabled
        if (!$this->is_enabled()) {
            return false;
        }

        // Check if caching is enabled in unified settings
        $enabled = $this->get_setting('cache_enabled', null);
        if ($enabled === null) {
            $enabled = $this->get_setting('enabled', false);
        }

        return (bool) $enabled;
    }
    
    /**
     * Check if current request should be cached
     *
     * @return bool
     */
    private function should_cache(): bool {
        // Don't cache for logged-in users
        if (is_user_logged_in()) {
            return false;
        }
        
        // Only cache GET requests
        if (prorank_get_request_method() !== 'GET') {
            return false;
        }
        
        // Don't cache if query string present (except allowed ones)
        if (prorank_has_query_params() && !$this->is_query_string_allowed()) {
            return false;
        }
        
        // Check excluded URLs
        if ($this->is_url_excluded()) {
            return false;
        }
        
        // Check excluded cookies
        if ($this->has_excluded_cookie()) {
            return false;
        }
        
        // Check excluded user agents
        if ($this->is_user_agent_excluded()) {
            return false;
        }
        
        // Don't cache 404 pages
        if (is_404()) {
            return false;
        }
        
        // Don't cache search pages
        if (is_search()) {
            return false;
        }
        
        return true;
    }
    
    /**
     * Serve cached page if available
     *
     * @return void
     */
    public function serve_cached_page(): void {
        if (!$this->should_cache()) {
            return;
        }
        
        $cache_file = $this->get_cache_file_path();
        
        if (!file_exists($cache_file)) {
            return;
        }
        
        // Check cache lifetime
        $lifetime = (int) $this->get_setting('cache_lifetime', null);
        if ($lifetime <= 0) {
            $lifetime = (int) $this->get_setting('lifetime', 3600);
        }
        if (time() - filemtime($cache_file) > $lifetime) {
            return;
        }
        
        // Check if gzip version exists and client supports it
        $gzip_file = $cache_file . '.gz';
        $use_gzip = file_exists($gzip_file) && $this->client_accepts_gzip();
        
        // Set headers
        header('X-ProRank-Cache: HIT');
        $shared_ttl = max(60, $lifetime);
        header('Cache-Control: public, max-age=0, must-revalidate, s-maxage=' . $shared_ttl . ', stale-while-revalidate=60, stale-if-error=86400');
        $served_file = $use_gzip ? $gzip_file : $cache_file;
        $this->track_cache_hit(is_file($served_file) ? (int) @filesize($served_file) : 0);
        
        if ($use_gzip) {
            header('Content-Encoding: gzip');
            \ProRank\SEO\Core\FilesystemHelper::output_file($gzip_file);
        } else {
            \ProRank\SEO\Core\FilesystemHelper::output_file($cache_file);
        }
        
        exit;
    }
    
    /**
     * Start output buffering
     *
     * @return void
     */
    public function start_buffer(): void {
        if (!$this->should_cache()) {
            return;
        }

        if ($this->buffer_started) {
            return;
        }

        header('X-ProRank-Cache: MISS');
        $this->track_cache_miss();
        ob_start([$this, 'capture_buffer'], 0);
        $this->buffer_started = true;
    }

    /**
     * Output buffer handler that persists the final HTML while returning the
     * original output unchanged to the browser.
     *
     * @param string $content Buffered output
     * @param int    $phase   Output buffer phase
     * @return string
     */
    public function capture_buffer(string $content, int $phase = 0): string {
        if (!$this->buffer_started || $this->cache_saved) {
            return $content;
        }

        if ($content === '' || !$this->should_cache()) {
            return $content;
        }

        $status_code = function_exists('http_response_code') ? (int) http_response_code() : 200;
        if ($status_code !== 200) {
            return $content;
        }

        $candidate_payload = $content;

        if ($phase !== 0 && defined('PHP_OUTPUT_HANDLER_END') && defined('PHP_OUTPUT_HANDLER_FINAL')) {
            $is_final = (bool) ($phase & (PHP_OUTPUT_HANDLER_END | PHP_OUTPUT_HANDLER_FINAL));
            $this->buffer_capture_accumulator .= $content;
            if (!$is_final) {
                return $content;
            }
            $candidate_payload = $this->buffer_capture_accumulator;
            $this->buffer_capture_accumulator = '';
        } elseif (stripos($content, '<html') === false || stripos($content, '</html>') === false) {
            $this->buffer_capture_accumulator .= $content;
            if (
                stripos($this->buffer_capture_accumulator, '<html') === false
                || stripos($this->buffer_capture_accumulator, '</html>') === false
            ) {
                return $content;
            }
            $candidate_payload = $this->buffer_capture_accumulator;
            $this->buffer_capture_accumulator = '';
        }

        if (
            stripos($candidate_payload, '<html') === false
            || stripos($candidate_payload, '</html>') === false
        ) {
            return $content;
        }

        $this->cache_saved = true;
        $this->save_cache_content($candidate_payload);

        return $content;
    }
    
    /**
     * Save cached content
     *
     * @return void
     */
    public function save_cache(): void {
        if (!$this->buffer_started || $this->cache_saved) {
            return;
        }

        $content = ob_get_contents();

        if (empty($content)) {
            return;
        }

        $this->cache_saved = true;
        $this->save_cache_content($content);
    }

    /**
     * Persist cache payload to disk.
     *
     * @param string $content HTML output
     * @return void
     */
    private function save_cache_content(string $content): void {
        try {
            // Add cache signature
            $signature = "\n<!-- Cached by ProRank SEO on " . wp_date('Y-m-d H:i:s') . " -->";
            $content .= $signature;

            // Get cache file path
            $cache_file = $this->get_cache_file_path();

            // Ensure cache directory exists, including the nested host path.
            if (!$this->ensure_cache_file_directory($cache_file)) {
                return;
            }

            // Initialize filesystem best-effort so WP_Filesystem is available
            // when possible, but always fall back to the shared helper for writes.
            $this->init_filesystem();

            // Save HTML file
            if (!\ProRank\SEO\Core\FilesystemHelper::put_contents($cache_file, $content, FS_CHMOD_FILE)) {
                prorank_log('ProRank SEO: Cache save_cache failed to write HTML file: ' . $cache_file);
                return;
            }

            // Save gzipped version if enabled
            if ($this->get_setting('gzip', true)) {
                $gzip_content = gzencode($content, 9);
                if ($gzip_content !== false) {
                    if (!\ProRank\SEO\Core\FilesystemHelper::put_contents($cache_file . '.gz', $gzip_content, FS_CHMOD_FILE)) {
                        prorank_log('ProRank SEO: Cache save_cache failed to write gzip file: ' . $cache_file . '.gz');
                    }
                }
            }
        } catch (\Throwable $e) {
            prorank_log('ProRank SEO: Cache save_cache failed - ' . $e->getMessage());
        }
    }
    
    /**
     * Clear cache for a specific post
     *
     * @param int $post_id Post ID
     * @return void
     */
    public function clear_post_cache(int $post_id): void {
        if ( \ProRank\SEO\Plugin::is_elementor_builder_save_request() ) {
            return;
        }

        // Clear the specific post cache
        $post = get_post($post_id);
        if (!$post) {
            return;
        }
        
        $permalink = get_permalink($post);
        if ($permalink) {
            $this->clear_url_cache($permalink);
        }
        
        // Also clear home page and archives
        $this->clear_url_cache(home_url('/'));
        
        // Clear category/tag archives if post has them
        $taxonomies = get_object_taxonomies($post->post_type);
        foreach ($taxonomies as $taxonomy) {
            $terms = wp_get_post_terms($post_id, $taxonomy);
            foreach ($terms as $term) {
                $term_link = get_term_link($term);
                if (!is_wp_error($term_link)) {
                    $this->clear_url_cache($term_link);
                }
            }
        }
    }
    
    /**
     * Clear cache when comment is posted/edited
     *
     * @param int $comment_id Comment ID
     * @param int $comment_approved Comment approval status
     * @return void
     */
    public function clear_comment_cache($comment_id, $comment_approved = null): void {
        $comment = get_comment($comment_id);
        if ($comment && $comment->comment_post_ID) {
            $this->clear_post_cache((int) $comment->comment_post_ID);
        }
    }
    
    /**
     * Clear all cache
     *
     * @return void
     */
    public function clear_all_cache(): void {
        if (!$this->init_filesystem()) {
            return;
        }
        
        global $wp_filesystem;
        
        $cache_dir = self::CACHE_DIR;
        if ($wp_filesystem->exists($cache_dir)) {
            $this->delete_directory($cache_dir);
            // Recreate empty cache directory
            $this->ensure_cache_directory();
        }
    }
    
    /**
     * Clear cache for specific URL
     *
     * @param string $url URL to clear
     * @return void
     */
    private function clear_url_cache(string $url): void {
        $cache_file = $this->get_cache_file_path($url);
        
        if (!$this->init_filesystem()) {
            return;
        }
        
        global $wp_filesystem;
        
        if ($wp_filesystem->exists($cache_file)) {
            $wp_filesystem->delete($cache_file);
        }
        
        $gzip_file = $cache_file . '.gz';
        if ($wp_filesystem->exists($gzip_file)) {
            $wp_filesystem->delete($gzip_file);
        }
    }
    
    /**
     * Get cache file path for current or specific URL
     *
     * @param string|null $url Optional URL
     * @return string
     */
    private function get_cache_file_path(?string $url = null): string {
        if ($url === null) {
            $url = $this->get_current_url();
        }
        
        $parsed = wp_parse_url($url);
        $path = isset($parsed['path']) ? $parsed['path'] : '/';
        $host = isset($parsed['host']) ? $parsed['host'] : '';
        
        // Clean up path
        $path = trim($path, '/');
        if (empty($path)) {
            $path = 'index';
        }
        
        // Replace special characters
        $path = str_replace(['/', '\\', '?', '&', '='], '-', $path);
        
        // Add query string hash if present
        if (!empty($parsed['query'])) {
            $path .= '-' . md5($parsed['query']);
        }
        
        // Add device type suffix if mobile detection is enabled
        if ($this->get_setting('mobile_separate', false)) {
            if ($this->is_mobile_device()) {
                $path .= '-mobile';
            }
        }
        
        return self::CACHE_DIR . '/' . $host . '/' . $path . '.html';
    }
    
    /**
     * Get current URL
     *
     * @return string
     */
    private function get_current_url(): string {
        $protocol = is_ssl() ? 'https' : 'http';
        $host = prorank_get_server_var('HTTP_HOST');
        $uri = prorank_get_server_var('REQUEST_URI');
        
        return $protocol . '://' . $host . $uri;
    }
    
    /**
     * Check if query string is allowed
     *
     * @return bool
     */
    private function is_query_string_allowed(): bool {
        $allowed = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
        
        foreach (array_keys(prorank_get_query_params()) as $key) {
            if (!in_array($key, $allowed, true)) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * Check if current URL is excluded
     *
     * @return bool
     */
    private function is_url_excluded(): bool {
        $excluded = $this->get_setting('cache_exclude_urls', '');
        if ('' === trim((string) $excluded)) {
            $excluded = $this->get_setting('exclude_urls', '');
        }
        if (empty($excluded)) {
            return false;
        }
        
        $current_url = $this->get_current_url();
        $patterns = array_filter(array_map('trim', explode("\n", $excluded)));
        
        foreach ($patterns as $pattern) {
            if (strpos($current_url, $pattern) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if request has excluded cookie
     *
     * @return bool
     */
    private function has_excluded_cookie(): bool {
        $excluded = $this->get_setting('exclude_cookies', '');
        if (empty($excluded)) {
            return false;
        }
        
        $patterns = array_filter(array_map('trim', explode("\n", $excluded)));
        
        foreach ($patterns as $pattern) {
            foreach ($_COOKIE as $name => $value) {
                if (strpos($name, $pattern) !== false) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    /**
     * Check if user agent is excluded
     *
     * @return bool
     */
    private function is_user_agent_excluded(): bool {
        $excluded = $this->get_setting('exclude_agents', '');
        if (empty($excluded)) {
            return false;
        }
        
        $user_agent = prorank_get_server_var('HTTP_USER_AGENT');
        $patterns = array_filter(array_map('trim', explode("\n", $excluded)));
        
        foreach ($patterns as $pattern) {
            if (stripos($user_agent, $pattern) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if client accepts gzip
     *
     * @return bool
     */
    private function client_accepts_gzip(): bool {
        $accept_encoding = prorank_get_server_var('HTTP_ACCEPT_ENCODING');
        return strpos($accept_encoding, 'gzip') !== false;
    }
    
    /**
     * Check if device is mobile
     *
     * @return bool
     */
    private function is_mobile_device(): bool {
        $user_agent = prorank_get_server_var('HTTP_USER_AGENT');
        $mobile_agents = ['Mobile', 'Android', 'iPhone', 'iPad', 'Windows Phone'];
        
        foreach ($mobile_agents as $agent) {
            if (stripos($user_agent, $agent) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Ensure cache directory exists
     *
     * @return bool
     */
    private function ensure_cache_directory(): bool {
        $cache_dir = self::CACHE_DIR;

        if (is_dir($cache_dir)) {
            return true;
        }

        return wp_mkdir_p($cache_dir);
    }

    /**
     * Ensure the cache directory for a specific file path exists.
     *
     * @param string $cache_file Cache file path.
     * @return bool
     */
    private function ensure_cache_file_directory(string $cache_file): bool {
        if (!$this->ensure_cache_directory()) {
            return false;
        }

        $dir = dirname($cache_file);
        if ($dir === '' || $dir === '.') {
            return true;
        }

        if (is_dir($dir)) {
            return true;
        }

        return wp_mkdir_p($dir);
    }

    /**
     * Track a cache hit for dashboard/cache-insights metrics.
     *
     * @param int $bytes_saved Bytes served from cache.
     * @return void
     */
    private function track_cache_hit(int $bytes_saved = 0): void {
        $stats = get_transient('prorank_cache_stats');
        if (!is_array($stats)) {
            $stats = [];
        }

        $stats = wp_parse_args($stats, [
            'hits' => 0,
            'misses' => 0,
            'bytes_served' => 0,
            'response_times' => [],
            'last_updated' => time(),
        ]);

        $stats['hits'] = (int) $stats['hits'] + 1;
        $stats['bytes_served'] = (int) $stats['bytes_served'] + max(0, $bytes_saved);
        $stats['last_updated'] = time();

        set_transient('prorank_cache_stats', $stats, HOUR_IN_SECONDS);
    }

    /**
     * Track a cache miss for dashboard/cache-insights metrics.
     *
     * @return void
     */
    private function track_cache_miss(): void {
        $stats = get_transient('prorank_cache_stats');
        if (!is_array($stats)) {
            $stats = [];
        }

        $stats = wp_parse_args($stats, [
            'hits' => 0,
            'misses' => 0,
            'bytes_served' => 0,
            'response_times' => [],
            'last_updated' => time(),
        ]);

        $stats['misses'] = (int) $stats['misses'] + 1;
        $stats['last_updated'] = time();

        set_transient('prorank_cache_stats', $stats, HOUR_IN_SECONDS);
    }
    
    /**
     * Initialize WordPress filesystem
     *
     * @return bool
     */
    private function init_filesystem(): bool {
        return \ProRank\SEO\Core\FilesystemHelper::get_filesystem() !== null;
    }
    
    /**
     * Delete directory recursively
     *
     * @param string $dir Directory path
     * @return bool
     */
    private function delete_directory(string $dir): bool {
        if (!is_dir($dir)) {
            return true;
        }

        $files = @scandir($dir);
        if (is_array($files)) {
            foreach (array_diff($files, ['.', '..']) as $file) {
                $path = trailingslashit($dir) . $file;
                if (is_dir($path) && !is_link($path)) {
                    $this->delete_directory($path);
                } else {
                    \ProRank\SEO\Core\FilesystemHelper::delete_file($path);
                }
            }
        }

        return \ProRank\SEO\Core\FilesystemHelper::delete_dir($dir);
    }
}
