<?php

/**
 * JavaScript Minification Module
 *
 * Provides JavaScript minification with intelligent caching, source map support,
 * and per-script exclusion patterns.
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   2.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Modules\BaseModule;

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


/**
 * JsMinifyModule class
 *
 * Handles JavaScript minification for improved page load performance.
 */


class JsMinifyModule extends BaseModule {

    /**
     * Cache directory for minified files
     *
     * @var string
     */
    private string $cache_dir = '';

    /**
     * Cache URL base
     *
     * @var string
     */
    private string $cache_url = '';

    /**
     * Excluded scripts cache
     *
     * @var array
     */
    private array $excluded_scripts = [];

    /**
     * Scripts that should never be minified
     */
    private const ALWAYS_EXCLUDE = [
        'jquery',
        'jquery-core',
        'jquery-migrate',
        'wp-polyfill',
        'regenerator-runtime',
        'wp-hooks',
        'wp-i18n',
        'react',
        'react-dom',
        'lodash',
        // Contact Form 7 runtime scripts (SWV/validation) are small and
        // historically sensitive to minification.
        'swv',
        'swv-js',
        'contact-form-7',
        'wpcf7',
        // Newfold login-password script relies on ASI/newlines; older ProRank
        // builds broke it, so keep it out of minify by default.
        'nfd-secure-passwords',
        'nfd-secure-passwords-js',
        // Goodshuffle's small bootstrap is commonly delayed on interaction.
        // Keep the original plugin URL so delayed execution never points at a
        // transient ProRank cache artifact after cache purges.
        'gspro-gallery-gal',
        'gspro-gallery-gal-js',
    ];

    /**
     * File extensions that indicate already minified files
     */
    private const MINIFIED_PATTERNS = [
        '.min.js',
        '-min.js',
        '.bundle.js',
        '.prod.js',
    ];

    /**
     * ProRank-owned frontend scripts that are safe to minify.
     *
     * Keep this empty unless a script has been verified against the current
     * minifier pipeline. A broken cached ProRank asset is worse than leaving
     * a small first-party file untouched.
     */
    private const SAFE_PRORANK_HANDLES = [];

    /**
     * Cache-key schema for generated JS minify artifacts.
     *
     * Bump whenever the minifier implementation changes. This keeps regenerated
     * assets on new URLs so CDN edges cannot serve stale bodies for a filename
     * whose source URL and source mtime did not change.
     */
    private const CACHE_KEY_SCHEMA = '20260505-matthias-js-v2';

    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'js_minify';
        $this->name = 'JavaScript Minification';
        $this->description = 'Minify JavaScript files to reduce file size and improve load times';
        $this->feature_tier = 'free';
        $this->parent_slug = 'performance';
    }

    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        if (!$this->should_run()) {
            return;
        }

        // Set up cache directories
        $this->setup_cache_directories();

        // Hook into script loading to minify scripts
        add_filter('script_loader_src', [$this, 'safe_maybe_minify_script'], 10, 2);

        // Add inline script minification
        if ($this->get_setting('js_minify_inline', false) || $this->get_setting('minify_inline', false)) {
            add_filter('script_loader_tag', [$this, 'safe_minify_inline_scripts'], 10, 3);
        }

        // Cache cleanup on update
        add_action('upgrader_process_complete', [$this, 'clear_cache']);
        add_action('switch_theme', [$this, 'clear_cache']);

        // Admin cleanup action
        add_action('prorank_clear_js_cache', [$this, 'clear_cache']);

        // REST API endpoint for cache stats
        add_action('rest_api_init', [$this, 'register_rest_routes']);
    }

    /**
     * Fail-open wrapper for external script minification.
     *
     * @param mixed $src
     * @param mixed $handle
     * @return string
     */
    public function safe_maybe_minify_script($src, $handle): string {
        $fallback = is_string($src) ? $src : '';

        try {
            return $this->maybe_minify_script($fallback, (string) $handle);
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('maybe_minify_script', $e);
            return $fallback;
        }
    }

    /**
     * Fail-open wrapper for inline script minification.
     *
     * @param mixed $tag
     * @param mixed $handle
     * @param mixed $src
     * @return string
     */
    public function safe_minify_inline_scripts($tag, $handle, $src): string {
        $fallback = is_string($tag) ? $tag : '';

        try {
            return $this->minify_inline_scripts($fallback, (string) $handle, (string) $src);
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('minify_inline_scripts', $e);
            return $fallback;
        }
    }

    /**
     * Log frontend optimization failures without taking the page down.
     *
     * @param string     $callback
     * @param \Throwable $e
     * @return void
     */
    private function log_frontend_callback_error(string $callback, \Throwable $e): void {
        if (function_exists('prorank_log')) {
            prorank_log('[ProRank JsMinifyModule] ' . $callback . ' failed: ' . $e->getMessage());
        }
    }

    /**
     * Check if module should run
     *
     * @return bool
     */
    private function should_run(): bool {
        // Check if module is enabled
        if (!$this->is_enabled()) {
            return false;
        }

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

        // Don't run in admin (except on AJAX requests for inline previews)
        if (is_admin() && !wp_doing_ajax()) {
            return false;
        }

        // Never rewrite scripts on wp-login.php. A broken login script blocks
        // password reset and admin access, while the performance upside is tiny.
        if ($this->is_login_request()) {
            return false;
        }

        // Don't run on preview
        if (is_preview() || is_customize_preview()) {
            return false;
        }

        if (\ProRank\SEO\Plugin::is_elementor_runtime_request()) {
            return false;
        }

        // Don't run if user is logged in and setting is disabled
        if (is_user_logged_in() && !$this->get_setting('minify_logged_in', false)) {
            return false;
        }

        return true;
    }

    /**
     * Check if JS minification feature is explicitly enabled.
     *
     * Avoid using generic "enabled" flags from other performance groups
     * (e.g. cache settings), which can unintentionally activate JS minify.
     */
    private function is_minification_enabled(): bool {
        $enabled = $this->get_setting('js_minify_enabled', null);
        if ($enabled !== null) {
            return (bool) $enabled;
        }

        // Legacy fallback key used by some older settings payloads.
        $legacy_enabled = $this->get_setting('js_minify', null);
        return $legacy_enabled !== null ? (bool) $legacy_enabled : false;
    }

    /**
     * Set up cache directories
     *
     * @return void
     */
    private function setup_cache_directories(): void {
        $upload_dir = wp_upload_dir();

        $this->cache_dir = trailingslashit($upload_dir['basedir']) . 'prorank-seo/js-cache/';
        $this->cache_url = trailingslashit($upload_dir['baseurl']) . 'prorank-seo/js-cache/';

        // Create cache directory if it doesn't exist
        if (!file_exists($this->cache_dir)) {
            wp_mkdir_p($this->cache_dir);

            // Add index.php for security
            file_put_contents($this->cache_dir . 'index.php', '<?php // Silence is golden');

            // Add .htaccess for caching headers
            $htaccess = "<IfModule mod_expires.c>\n";
            $htaccess .= "    ExpiresActive On\n";
            $htaccess .= "    ExpiresByType application/javascript \"access plus 1 year\"\n";
            $htaccess .= "    ExpiresByType text/javascript \"access plus 1 year\"\n";
            $htaccess .= "</IfModule>\n";
            $htaccess .= "<IfModule mod_headers.c>\n";
            $htaccess .= "    Header set Cache-Control \"public, max-age=31536000, immutable\"\n";
            $htaccess .= "</IfModule>\n";
            file_put_contents($this->cache_dir . '.htaccess', $htaccess);
        }

        $this->purge_invalid_cache_files();
    }

    /**
     * Maybe minify a script
     *
     * @param string $src    Script source URL
     * @param string $handle Script handle
     * @return string Modified source URL (or original if not minified)
     */
    public function maybe_minify_script(string $src, string $handle): string {
        // Skip if excluded
        if ($this->is_script_excluded($handle, $src)) {
            return $src;
        }

        // Skip if already minified
        if ($this->is_already_minified($src)) {
            return $src;
        }

        // Skip external scripts (unless enabled)
        if (!$this->is_local_script($src) && !$this->get_setting('minify_external', false)) {
            return $src;
        }

        // Try to get cached version
        $cached_url = $this->get_cached_minified_url($src, $handle);

        if ($cached_url !== null) {
            return $cached_url;
        }

        // Minify and cache
        $minified_url = $this->minify_and_cache($src, $handle);

        return $minified_url ?? $src;
    }

    /**
     * Minify inline scripts
     *
     * @param string $tag    Script tag HTML
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return string Modified script tag
     */
    public function minify_inline_scripts(string $tag, string $handle, string $src): string {
        // Only process inline scripts (no src)
        if (!empty($src)) {
            return $tag;
        }

        // Skip excluded handles
        if (in_array($handle, self::ALWAYS_EXCLUDE, true)) {
            return $tag;
        }

        // Extract inline script content using DOMDocument
        $dom = new \DOMDocument();

        // Suppress warnings for HTML5 elements
        libxml_use_internal_errors(true);
        $dom->loadHTML('<!DOCTYPE html><html><head>' . $tag . '</head></html>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        libxml_clear_errors();

        $scripts = $dom->getElementsByTagName('script');

        if ($scripts->length === 0) {
            return $tag;
        }

        $script = $scripts->item(0);
        $content = $script->textContent;

        // Skip empty scripts
        if (empty(trim($content))) {
            return $tag;
        }

        // Skip if already minified (heuristic: no newlines and very long lines)
        if (strpos($content, "\n") === false && strlen($content) > 500) {
            return $tag;
        }

        // Minify the content
        $minified = $this->minify_js_content($content);

        if ($minified === null || strlen($minified) >= strlen($content)) {
            return $tag;
        }

        // Replace content
        $script->textContent = '';
        $script->appendChild($dom->createTextNode($minified));

        // Extract just the script tag
        $html = $dom->saveHTML($script);

        return $html ?: $tag;
    }

    /**
     * Check if script is excluded from minification
     *
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return bool
     */
    private function is_script_excluded(string $handle, string $src): bool {
        // Never minify ProRank-owned scripts here.
        // Many are module-oriented and/or localized; JShrink can break them.
        if (strpos($handle, 'prorank-') === 0 || strpos($handle, 'prorank_') === 0) {
            if (!in_array($handle, self::SAFE_PRORANK_HANDLES, true)) {
                return true;
            }
        }

        // Bookly assets are known to break with regex minification.
        if (stripos($handle, 'bookly') !== false || ($src !== '' && stripos($src, '/bookly') !== false)) {
            return true;
        }

        // Newfold / Bluehost login and platform scripts are often small,
        // newline-sensitive, and not worth minifying with the fallback engine.
        if (stripos($handle, 'nfd-') === 0 || ($src !== '' && stripos($src, '/newfold-') !== false)) {
            return true;
        }

        // Goodshuffle prints web components and raw module loaders outside the
        // normal enqueue chain. Minifying only its bootstrap creates fragile
        // dependencies between delayed tags and ProRank's js-cache files.
        if (stripos($handle, 'gspro') !== false || ($src !== '' && stripos($src, '/wp-gspro-gallery/') !== false)) {
            return true;
        }

        // Contact Form 7 runtime and SWV scripts are syntax-sensitive.
        if ($src !== '' && strpos($src, '/contact-form-7/') !== false) {
            return true;
        }

        // PixelYourSite's tracking bundle is syntax-sensitive and not worth
        // risking a regex-based minification pass.
        if ($src !== '' && strpos($src, '/pixelyoursite/') !== false) {
            return true;
        }

        // Check always-excluded scripts
        if (in_array($handle, self::ALWAYS_EXCLUDE, true)) {
            return true;
        }

        // Get user-defined exclusions (cached)
        if (empty($this->excluded_scripts)) {
            $excluded = $this->get_setting('exclude_scripts', '');
            if (!empty($excluded)) {
                $this->excluded_scripts = array_filter(
                    array_map('trim', explode("\n", $excluded))
                );
            }
        }

        // Check handle matches
        foreach ($this->excluded_scripts as $pattern) {
            if ($handle === $pattern) {
                return true;
            }

            // Wildcard matching
            if (strpos($pattern, '*') !== false) {
                $regex = '/^' . str_replace('\*', '.*', preg_quote($pattern, '/')) . '$/';
                if (preg_match($regex, $handle)) {
                    return true;
                }
            }

            // URL contains pattern
            if (!empty($src) && strpos($src, $pattern) !== false) {
                return true;
            }
        }

        // Allow filtering exclusions
        return apply_filters('prorank_js_minify_exclude', false, $handle, $src);
    }

    /**
     * Check if script is already minified
     *
     * @param string $src Script source URL
     * @return bool
     */
    private function is_already_minified(string $src): bool {
        $parsed = wp_parse_url($src);
        $path = $parsed['path'] ?? '';

        foreach (self::MINIFIED_PATTERNS as $pattern) {
            if (stripos($path, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if script is local
     *
     * @param string $src Script source URL
     * @return bool
     */
    private function is_local_script(string $src): bool {
        $site_host = wp_parse_url(home_url(), PHP_URL_HOST);
        $script_host = wp_parse_url($src, PHP_URL_HOST);

        return $script_host === null || $script_host === $site_host;
    }

    /**
     * Get cached minified URL if available
     *
     * @param string $src    Original source URL
     * @param string $handle Script handle
     * @return string|null Cached URL or null
     */
    private function get_cached_minified_url(string $src, string $handle): ?string {
        $cache_key = $this->generate_cache_key($src);
        $cache_file = $this->cache_dir . $cache_key . '.min.js';

        if (!file_exists($cache_file)) {
            return null;
        }

        $cached_content = file_get_contents($cache_file);
        if (
            !is_string($cached_content)
            || strpos($cached_content, '___PRESERVED_') !== false
            || $this->has_minification_syntax_red_flags($cached_content)
        ) {
            \ProRank\SEO\Core\FilesystemHelper::delete_file($cache_file);
            return null;
        }

        // Check if original file is newer (for local files)
        if ($this->is_local_script($src)) {
            $original_file = $this->url_to_path($src);

            if ($original_file && file_exists($original_file)) {
                if (filemtime($original_file) > filemtime($cache_file)) {
                    // Original is newer, need to regenerate
                    return null;
                }
            }
        }

        // Check cache expiry
        $max_age = $this->get_setting('cache_max_age', 30) * DAY_IN_SECONDS;
        if (time() - filemtime($cache_file) > $max_age) {
            return null;
        }

        return $this->cache_url . $cache_key . '.min.js';
    }

    /**
     * Minify script and cache result
     *
     * @param string $src    Original source URL
     * @param string $handle Script handle
     * @return string|null Cached URL or null on failure
     */
    private function minify_and_cache(string $src, string $handle): ?string {
        // Get script content
        $content = $this->get_script_content($src);

        if ($content === null || empty(trim($content))) {
            return null;
        }

        // Minify
        $minified = $this->minify_js_content($content);

        if ($minified === null) {
            return null;
        }

        // Fail safe: unresolved placeholders indicate broken output.
        if (strpos($minified, '___PRESERVED_') !== false) {
            return null;
        }

        // Fail safe: never cache output with obvious syntax red flags.
        if ($this->has_minification_syntax_red_flags($minified)) {
            return null;
        }

        // Skip if minification didn't reduce size significantly
        $original_size = strlen($content);
        $minified_size = strlen($minified);
        $savings_percent = (($original_size - $minified_size) / $original_size) * 100;

        if ($savings_percent < 5) {
            // Less than 5% savings, not worth the overhead
            return null;
        }

        // Save to cache
        $cache_key = $this->generate_cache_key($src);
        $cache_file = $this->cache_dir . $cache_key . '.min.js';

        // Add source map comment if enabled
        if ($this->get_setting('generate_sourcemaps', false)) {
            $sourcemap = $this->generate_source_map($src, $content, $minified);
            if ($sourcemap) {
                $map_file = $this->cache_dir . $cache_key . '.min.js.map';
                file_put_contents($map_file, wp_json_encode($sourcemap));
                $minified .= "\n//# sourceMappingURL=" . $cache_key . '.min.js.map';
            }
        }

        // Add header comment
        $header = sprintf(
            "/* ProRank SEO - Minified: %s | Original: %s bytes | Minified: %s bytes | Savings: %.1f%% */\n",
            $handle,
            number_format($original_size),
            number_format($minified_size),
            $savings_percent
        );

        if (file_put_contents($cache_file, $header . $minified) === false) {
            return null;
        }

        // Store stats
        $this->update_stats($handle, $original_size, $minified_size);

        return $this->cache_url . $cache_key . '.min.js';
    }

    /**
     * Get script content from URL
     *
     * @param string $src Script URL
     * @return string|null Script content or null
     */
    private function get_script_content(string $src): ?string {
        // Try local file first
        if ($this->is_local_script($src)) {
            $path = $this->url_to_path($src);

            if ($path && file_exists($path)) {
                $content = file_get_contents($path);
                return $content !== false ? $content : null;
            }
        }

        // Fetch remote script
        $response = wp_remote_get($src, [
            'timeout' => 10,
            'sslverify' => false,
        ]);

        if (is_wp_error($response)) {
            return null;
        }

        $code = wp_remote_retrieve_response_code($response);
        if ($code !== 200) {
            return null;
        }

        return wp_remote_retrieve_body($response);
    }

    /**
     * Minify JavaScript content
     *
     * Uses the bundled Matthias Mullie JS minifier. The previous fallback
     * regex minifier collapsed ASI-sensitive newlines and could emit invalid
     * JavaScript, so failures now return null and keep the original asset.
     *
     * @param string $content JavaScript content
     * @return string|null Minified content or null on error
     */
    private function minify_js_content(string $content): ?string {
        try {
            if (!$this->ensure_js_minify_library()) {
                return null;
            }

            $minifier = new \MatthiasMullie\Minify\JS($content);
            $minified = $minifier->minify();

            if (!is_string($minified)) {
                return null;
            }

            $minified = trim($minified);

            if ($minified === '' && trim($content) !== '') {
                return null;
            }

            return $minified;
        } catch (\Throwable $e) {
            // Log error but don't break the page
            if (function_exists('prorank_log')) {
                prorank_log('ProRank JS Minify Error: ' . $e->getMessage());
            }
            return null;
        }
    }

    /**
     * Ensure the bundled JS minifier is loaded.
     *
     * @return bool
     */
    private function ensure_js_minify_library(): bool {
        if (class_exists(\MatthiasMullie\Minify\JS::class)) {
            return true;
        }

        $base = dirname(__DIR__, 2) . '/lib/MatthiasMullie/Minify/';
        $files = [
            'Exceptions/BasicException.php',
            'Exceptions/IOException.php',
            'Exceptions/PatternMatchException.php',
            'Exceptions/FileImportException.php',
            'Exception.php',
            'Minify.php',
            'JS.php',
        ];

        foreach ($files as $file) {
            $path = $base . $file;
            if (file_exists($path)) {
                require_once $path;
            }
        }

        return class_exists(\MatthiasMullie\Minify\JS::class);
    }

    /**
     * Detect common invalid output produced by whitespace-only minification.
     *
     * @param string $js Minified JavaScript.
     * @return bool
     */
    private function has_minification_syntax_red_flags(string $js): bool {
        $patterns = [
            '/\blet\s+[A-Za-z_$][\w$]*(?:\s*=\s*[^;{}]+)?\s+(?:return|const|let|var|function|class|if|for|while|switch|try|[A-Za-z_$][\w$]*\s*=)/',
            '/\bconst\s+[A-Za-z_$][\w$]*(?:\s*=\s*[^;{}]+)?\s+(?:return|const|let|var|function|class|if|for|while|switch|try|[A-Za-z_$][\w$]*\s*=)/',
            '/\bvar\s+[A-Za-z_$][\w$]*(?:\s*=\s*[^;{}]+)?\s+(?:return|const|let|var|function|class|if|for|while|switch|try|[A-Za-z_$][\w$]*\s*=)/',
            '/\*\s+[^*\/][^\/]*\*\//',
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $js)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Detect WordPress login/reset-password screens.
     *
     * @return bool
     */
    private function is_login_request(): bool {
        $script_name = isset($_SERVER['SCRIPT_NAME']) ? (string) wp_unslash($_SERVER['SCRIPT_NAME']) : '';
        $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';

        return stripos($script_name, 'wp-login.php') !== false
            || stripos($request_uri, 'wp-login.php') !== false;
    }

    /**
     * Generate source map for minified file
     *
     * @param string $src      Original source URL
     * @param string $original Original content
     * @param string $minified Minified content
     * @return array|null Source map data or null
     */
    private function generate_source_map(string $src, string $original, string $minified): ?array {
        // Basic source map structure
        return [
            'version' => 3,
            'file' => basename($src),
            'sources' => [$src],
            'sourcesContent' => [$original],
            'mappings' => '', // Would need proper mapping generation
        ];
    }

    /**
     * Generate cache key for script
     *
     * @param string $src Script source URL
     * @return string Cache key
     */
    private function generate_cache_key(string $src): string {
        // Remove query string version
        $src_clean = preg_replace('/\?.*$/', '', $src);

        // Include file modification time for local files
        $mtime = '';
        if ($this->is_local_script($src)) {
            $path = $this->url_to_path($src);
            if ($path && file_exists($path)) {
                $mtime = (string) filemtime($path);
            }
        }

        return md5(self::CACHE_KEY_SCHEMA . '|' . $src_clean . '|' . $mtime);
    }

    /**
     * Convert URL to filesystem path
     *
     * @param string $url URL to convert
     * @return string|null Filesystem path or null
     */
    private function url_to_path(string $url): ?string {
        // Remove query string
        $url = preg_replace('/\?.*$/', '', $url);

        // Get site paths
        $site_url = site_url('/');
        $abspath = ABSPATH;

        // Convert URL to path
        if (strpos($url, $site_url) === 0) {
            $relative = substr($url, strlen($site_url));
            return $abspath . $relative;
        }

        // Try content URL
        $content_url = content_url('/');
        if (strpos($url, $content_url) === 0) {
            $relative = substr($url, strlen($content_url));
            return WP_CONTENT_DIR . '/' . $relative;
        }

        return null;
    }

    /**
     * Update minification stats
     *
     * @param string $handle        Script handle
     * @param int    $original_size Original size in bytes
     * @param int    $minified_size Minified size in bytes
     * @return void
     */
    private function update_stats(string $handle, int $original_size, int $minified_size): void {
        $stats = get_option('prorank_js_minify_stats', [
            'total_original' => 0,
            'total_minified' => 0,
            'files_count' => 0,
            'last_updated' => 0,
        ]);

        $stats['total_original'] += $original_size;
        $stats['total_minified'] += $minified_size;
        $stats['files_count']++;
        $stats['last_updated'] = time();

        update_option('prorank_js_minify_stats', $stats, false);
    }

    /**
     * Clear the minification cache
     *
     * @return int Number of files deleted
     */
    public function clear_cache(): int {
        if (empty($this->cache_dir) || !is_dir($this->cache_dir)) {
            $this->setup_cache_directories();
        }

        $files = glob($this->cache_dir . '*.{js,map}', GLOB_BRACE);
        $count = 0;

        if ($files) {
            foreach ($files as $file) {
                if (is_file($file)) {
                    \ProRank\SEO\Core\FilesystemHelper::delete_file($file);
                    $count++;
                }
            }
        }

        // Reset stats
        delete_option('prorank_js_minify_stats');

        return $count;
    }

    /**
     * Remove broken cache artifacts left by older minifier builds.
     *
     * @return int Number of files deleted.
     */
    private function purge_invalid_cache_files(): int {
        if (empty($this->cache_dir) || !is_dir($this->cache_dir)) {
            return 0;
        }

        $files = glob($this->cache_dir . '*.js');
        if (!$files) {
            return 0;
        }

        $count = 0;
        foreach ($files as $file) {
            if (!is_file($file)) {
                continue;
            }

            $content = file_get_contents($file);
            if (
                !is_string($content)
                || strpos($content, '___PRESERVED_') !== false
                || $this->has_minification_syntax_red_flags($content)
            ) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($file);
                $count++;
            }
        }

        return $count;
    }

    /**
     * Get cache statistics
     *
     * @return array Cache statistics
     */
    public function get_cache_stats(): array {
        $stats = get_option('prorank_js_minify_stats', [
            'total_original' => 0,
            'total_minified' => 0,
            'files_count' => 0,
            'last_updated' => 0,
        ]);

        // Calculate savings
        $savings_bytes = $stats['total_original'] - $stats['total_minified'];
        $savings_percent = $stats['total_original'] > 0
            ? ($savings_bytes / $stats['total_original']) * 100
            : 0;

        // Get cache directory size
        $cache_size = 0;
        if (!empty($this->cache_dir) && is_dir($this->cache_dir)) {
            $files = glob($this->cache_dir . '*.js');
            if ($files) {
                foreach ($files as $file) {
                    $cache_size += filesize($file);
                }
            }
        }

        return [
            'total_original_bytes' => $stats['total_original'],
            'total_minified_bytes' => $stats['total_minified'],
            'savings_bytes' => $savings_bytes,
            'savings_percent' => round($savings_percent, 2),
            'files_count' => $stats['files_count'],
            'cache_size_bytes' => $cache_size,
            'last_updated' => $stats['last_updated'],
        ];
    }

    /**
     * Register REST API routes
     *
     * @return void
     */
    public function register_rest_routes(): void {
        register_rest_route('prorank-seo/v1', '/js-minify/stats', [
            'methods' => 'GET',
            'callback' => [$this, 'rest_get_stats'],
            'permission_callback' => function() {
                return current_user_can('manage_options');
            },
        ]);

        register_rest_route('prorank-seo/v1', '/js-minify/clear-cache', [
            'methods' => 'POST',
            'callback' => [$this, 'rest_clear_cache'],
            'permission_callback' => function() {
                return current_user_can('manage_options');
            },
        ]);
    }

    /**
     * REST endpoint: Get stats
     *
     * @return \WP_REST_Response
     */
    public function rest_get_stats(): \WP_REST_Response {
        return new \WP_REST_Response($this->get_cache_stats());
    }

    /**
     * REST endpoint: Clear cache
     *
     * @return \WP_REST_Response
     */
    public function rest_clear_cache(): \WP_REST_Response {
        $count = $this->clear_cache();

        return new \WP_REST_Response([
            'success' => true,
            'files_deleted' => $count,
        ]);
    }
}
