<?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) can break with regex minifiers.
        'swv',
        'swv-js',
        'contact-form-7',
        'wpcf7',
    ];

    /**
     * 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 = [];

    /**
     * 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, '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, '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']);
    }

    /**
     * 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;
        }

        // Don't run on preview
        if (is_preview() || is_customize_preview()) {
            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);
        }
    }

    /**
     * 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;
        }

        // 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;
        }

        // 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;
        }

        // 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 a Terser-like approach for reliable minification.
     *
     * @param string $content JavaScript content
     * @return string|null Minified content or null on error
     */
    private function minify_js_content(string $content): ?string {
        try {
            // Use the JShrink-like minification
            $minified = $this->jshrink_minify($content);

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

    /**
     * JShrink-like JavaScript minifier
     *
     * A pure PHP implementation that safely minifies JavaScript.
     *
     * @param string $js JavaScript content
     * @return string Minified JavaScript
     */
    private function jshrink_minify(string $js): string {
        // Preserve strings and regex literals
        $preserved = [];
        $index = 0;

        // Preserve template literals
        $js = $this->safe_preg_replace_callback('/`(?:[^`\\\\]|\\\\.)*`/s', function($match) use (&$preserved, &$index) {
            $key = "___PRESERVED_{$index}___";
            $preserved[$key] = $match[0];
            $index++;
            return $key;
        }, $js, 'template literals');

        // Preserve double-quoted strings
        $js = $this->safe_preg_replace_callback('/"(?:[^"\\\\]|\\\\.)*"/s', function($match) use (&$preserved, &$index) {
            $key = "___PRESERVED_{$index}___";
            $preserved[$key] = $match[0];
            $index++;
            return $key;
        }, $js, 'double-quoted strings');

        // Preserve single-quoted strings
        $js = $this->safe_preg_replace_callback("/\'(?:[^\'\\\\]|\\\\.)*\'/s", function($match) use (&$preserved, &$index) {
            $key = "___PRESERVED_{$index}___";
            $preserved[$key] = $match[0];
            $index++;
            return $key;
        }, $js, 'single-quoted strings');

        // Preserve regex literals (simplified - after operators)
        $js = $this->safe_preg_replace_callback('/([=(:,;\[!&|?])\s*\/(?!\/)(?:[^\/\\\\]|\\\\.)+\/[gimsuvy]*/', function($match) use (&$preserved, &$index) {
            $key = "___PRESERVED_{$index}___";
            $preserved[$key] = $match[0];
            $index++;
            return $key;
        }, $js, 'regex literals');

        // Remove single-line comments (but not in URLs like http://)
        $js = $this->safe_preg_replace('/(?<!:)\/\/[^\n]*/', '', $js, 'single-line comments');

        // Remove multi-line comments
        $js = $this->safe_preg_replace('/\/\*[\s\S]*?\*\//', '', $js, 'multi-line comments');

        // Remove unnecessary whitespace (collapse multiple spaces/newlines to single space)
        $js = $this->safe_preg_replace('/\s+/', ' ', $js, 'whitespace collapse');

        // Carefully remove spaces around punctuation (but NOT operators that need context)
        // Remove space BEFORE: } ] ) ; ,
        $js = $this->safe_preg_replace('/\s+([}\]);,])/', '$1', $js, 'space before punctuation');
        // Remove space AFTER: { [ ( ; ,
        $js = $this->safe_preg_replace('/([{\[(;,])\s+/', '$1', $js, 'space after punctuation');

        // Remove spaces around colons (but carefully for ternary)
        $js = $this->safe_preg_replace('/\s*:\s*/', ':', $js, 'colon spacing');

        // Fix keywords that need trailing space
        $keywords = ['return', 'throw', 'new', 'delete', 'typeof', 'void', 'yield', 'await', 'case', 'in', 'of', 'instanceof', 'var', 'let', 'const', 'function', 'class', 'extends', 'import', 'export', 'from', 'if', 'else', 'for', 'while', 'do', 'switch', 'try', 'catch', 'finally'];
        foreach ($keywords as $keyword) {
            // Add space after keyword if followed by non-punctuation
            $js = $this->safe_preg_replace('/\b' . $keyword . '\b(?=[^\s;{}()\[\],])/', $keyword . ' ', $js, 'keyword spacing: ' . $keyword);
        }

        // Fix common issues
        $js = str_replace('}else', '} else', $js);
        $js = str_replace('}catch', '} catch', $js);
        $js = str_replace('}finally', '} finally', $js);
        $js = str_replace('}while', '} while', $js);
        $js = $this->safe_preg_replace('/else\s*if/', 'else if', $js, 'else-if spacing');

        // Restore preserved content
        foreach ($preserved as $key => $value) {
            $js = str_replace($key, $value, $js);
        }

        return trim($js);
    }

    /**
     * Run preg_replace_callback safely.
     *
     * @param string   $pattern
     * @param callable $callback
     * @param string   $subject
     * @param string   $context
     * @return string
     */
    private function safe_preg_replace_callback(string $pattern, callable $callback, string $subject, string $context): string {
        $result = preg_replace_callback($pattern, $callback, $subject);

        if ($result === null) {
            throw new \RuntimeException(
                sprintf(
                    'PCRE failure while minifying %s: %s',
                    $context,
                    $this->get_pcre_error_message()
                )
            );
        }

        return $result;
    }

    /**
     * Run preg_replace safely.
     *
     * @param string       $pattern
     * @param string|array $replacement
     * @param string       $subject
     * @param string       $context
     * @return string
     */
    private function safe_preg_replace(string $pattern, $replacement, string $subject, string $context): string {
        $result = preg_replace($pattern, $replacement, $subject);

        if ($result === null) {
            throw new \RuntimeException(
                sprintf(
                    'PCRE failure while minifying %s: %s',
                    $context,
                    $this->get_pcre_error_message()
                )
            );
        }

        return $result;
    }

    /**
     * Return the last PCRE error as a human-readable string.
     *
     * @return string
     */
    private function get_pcre_error_message(): string {
        if (function_exists('preg_last_error_msg')) {
            return preg_last_error_msg();
        }

        return 'PCRE error code ' . preg_last_error();
    }

    /**
     * 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($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;
    }

    /**
     * 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,
        ]);
    }
}
