<?php

/**
 * Browser Caching Module
 *
 * Manages browser caching through .htaccess rules and HTTP headers
 * Implements mod_expires, mod_headers, and Cache-Control directives
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Modules\BaseModule;

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


/**
 * BrowserCacheModule class
 */


class BrowserCacheModule extends BaseModule {

    /**
     * Required tier for this module
     */
    protected string $feature_tier = 'free';
    
    /**
     * .htaccess marker start
     */
    private const MARKER_START = '# BEGIN ProRank SEO Browser Cache';
    
    /**
     * .htaccess marker end
     */
    private const MARKER_END = '# END ProRank SEO Browser Cache';
    
    /**
     * Default cache lifetimes by file type (in seconds)
     */
    private const DEFAULT_LIFETIMES = [
        'images' => 31536000,      // 1 year
        'css' => 31536000,          // 1 year
        'js' => 31536000,           // 1 year
        'fonts' => 31536000,        // 1 year
        'html' => 3600,             // 1 hour
        'xml' => 3600,              // 1 hour
        'json' => 3600,             // 1 hour
        'pdf' => 2592000,           // 30 days
        'media' => 2592000,         // 30 days (video/audio)
    ];
    
    /**
     * File extensions by type
     */
    private const FILE_EXTENSIONS = [
        'images' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg', 'ico', 'bmp', 'tiff'],
        'css' => ['css'],
        'js' => ['js', 'mjs'],
        'fonts' => ['ttf', 'otf', 'woff', 'woff2', 'eot'],
        'html' => ['html', 'htm'],
        'xml' => ['xml'],
        'json' => ['json'],
        'pdf' => ['pdf'],
        'media' => ['mp4', 'mp3', 'ogg', 'webm', 'wav', 'flac', 'avi', 'mov'],
    ];
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'browser_cache';
        $this->name = 'Browser Caching';
        $this->description = 'Configure browser caching headers to store static resources locally';
$this->parent_slug = 'performance';
    }
    
    /**
     * Initialize module hooks
     */
    public function init_hooks(): void {
        if (!$this->is_enabled()) {
            return;
        }
        
        // Add headers for PHP-served files
        add_action('send_headers', [$this, 'send_cache_headers']);
        
        // Update .htaccess when settings change (unified cache settings)
        add_action('update_option_prorank_cache_settings', [$this, 'update_htaccess']);
        
        // Clean up on deactivation (only if constant defined)
        if (defined('PRORANK_PLUGIN_FILE')) {
            register_deactivation_hook(\PRORANK_PLUGIN_FILE, [$this, 'remove_htaccess_rules']);
        }
    }
    
    /**
     * Send cache headers for PHP-served content
     */
    public function send_cache_headers(): void {
        if (is_admin() || is_user_logged_in()) {
            return;
        }
        
        $settings = $this->get_settings();
        
        if (empty($settings['browser_cache_enabled'])) {
            return;
        }
        
        // Don't cache if doing ajax or is preview
        if (wp_doing_ajax() || is_preview()) {
            header('Cache-Control: no-cache, no-store, must-revalidate');
            header('Pragma: no-cache');
            header('Expires: 0');
            return;
        }
        
        // HTML pages: always revalidate to prevent stale content
        header('Cache-Control: no-cache, must-revalidate');
        header('Vary: Accept-Encoding');
    }
    
    /**
     * Get cache time for current request
     */
    private function get_cache_time_for_current_request(array $settings): int {
        // HTML pages must NEVER be browser-cached with max-age.
        // Stale HTML causes broken CSS/JS references when optimization
        // modules change asset URLs (UCSS, CSS combine, JS delay).
        // Server-side page cache (ModernCacheModule) handles caching.
        // Browser should always revalidate HTML with the server.
        return 0;
    }
    
    /**
     * Add ETag header
     */
    private function add_etag_header(): void {
        $uri = prorank_get_server_var('REQUEST_URI');
        $modified = get_the_modified_time('U');
        $etag = md5($uri . $modified);
        
        header('ETag: "' . $etag . '"');
        
        // Check If-None-Match header
        $client_etag = prorank_get_server_var('HTTP_IF_NONE_MATCH');
        if (trim($client_etag, '"') === $etag) {
            header('HTTP/1.1 304 Not Modified');
            exit;
        }
    }
    
    /**
     * Update .htaccess with browser caching rules
     */
    public function update_htaccess(): bool {
        $settings = $this->get_settings();

        $enabled = !empty($settings['browser_cache_enabled']) || !empty($settings['enabled']);
        if (!$enabled) {
            return $this->remove_htaccess_rules();
        }

        try {
            $htaccess_path = prorank_home_file_path('.htaccess');

            // Check if .htaccess is writable
            if (!wp_is_writable($htaccess_path) && file_exists($htaccess_path)) {
                return false;
            }

            // Generate rules
            $rules = $this->generate_htaccess_rules($settings);

            // Read existing .htaccess
            $htaccess_content = file_exists($htaccess_path) ? file_get_contents($htaccess_path) : '';

            // Remove old rules
            $htaccess_content = $this->remove_existing_rules($htaccess_content);

            // Add new rules at the beginning (after any existing WordPress rules)
            $wordpress_begin = '# BEGIN WordPress';
            $pos = strpos($htaccess_content, $wordpress_begin);

            if ($pos !== false) {
                // Insert before WordPress rules
                $htaccess_content = substr($htaccess_content, 0, $pos) .
                                   $rules . "\n\n" .
                                   substr($htaccess_content, $pos);
            } else {
                // Add at the beginning
                $htaccess_content = $rules . "\n\n" . $htaccess_content;
            }

            // Write back to .htaccess
            return file_put_contents($htaccess_path, $htaccess_content) !== false;
        } catch (\Throwable $e) {
            prorank_log('ProRank SEO: Failed to update .htaccess - ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Generate .htaccess rules
     */
    private function generate_htaccess_rules(array $settings): string {
        $rules = [];
        $rules[] = self::MARKER_START;
        $rules[] = '';
        
        // Check for required modules
        $rules[] = '<IfModule mod_expires.c>';
        $rules[] = '    ExpiresActive On';
        $rules[] = '    ExpiresDefault "access plus 1 week"';
        $rules[] = '';
        
        // Add expires rules for each file type
        foreach (self::FILE_EXTENSIONS as $type => $extensions) {
            $lifetime = $this->get_browser_cache_setting_value($settings, $type);
            
            if ($lifetime > 0) {
                $expires = $this->seconds_to_expires_string($lifetime);
                
                foreach ($extensions as $ext) {
                    $rules[] = '    ExpiresByType ' . $this->get_mime_type($ext) . ' "access plus ' . $expires . '"';
                }
            }
        }
        
        $rules[] = '</IfModule>';
        $rules[] = '';
        
        // Add mod_headers rules for more control
        $rules[] = '<IfModule mod_headers.c>';
        
        // Remove ETags if disabled (better for CDN). Default to enabled.
        if (!($settings['etag_enabled'] ?? true)) {
            $rules[] = '    Header unset ETag';
            $rules[] = '    FileETag None';
        }
        
        // Add Cache-Control headers
        foreach (self::FILE_EXTENSIONS as $type => $extensions) {
            $lifetime = $this->get_browser_cache_setting_value($settings, $type);
            
            if ($lifetime > 0) {
                $ext_pattern = implode('|', $extensions);
                $rules[] = '    <FilesMatch "\.(' . $ext_pattern . ')$">';
                
                if ($settings['immutable_' . $type] ?? false) {
                    $rules[] = '        Header set Cache-Control "public, max-age=' . $lifetime . ', immutable"';
                } else {
                    $rules[] = '        Header set Cache-Control "public, max-age=' . $lifetime . '"';
                }
                
                $rules[] = '    </FilesMatch>';
            }
        }
        
        // Add CORS headers for fonts
        if ($settings['cors_fonts'] ?? true) {
            $rules[] = '    <FilesMatch "\.(ttf|otf|woff|woff2|eot)$">';
            $rules[] = '        Header set Access-Control-Allow-Origin "*"';
            $rules[] = '    </FilesMatch>';
        }
        
        // Add Vary: Accept-Encoding
        $rules[] = '    Header append Vary "Accept-Encoding"';
        
        // Security headers
        if ($settings['security_headers'] ?? true) {
            $rules[] = '    Header set X-Content-Type-Options "nosniff"';
            $rules[] = '    Header set X-Frame-Options "SAMEORIGIN"';
            $rules[] = '    Header set X-XSS-Protection "1; mode=block"';
        }
        
        $rules[] = '</IfModule>';
        $rules[] = '';
        
        // Add compression rules if enabled
        if ($settings['enable_compression'] ?? true) {
            $rules[] = '<IfModule mod_deflate.c>';
            $rules[] = '    # Compress HTML, CSS, JavaScript, Text, XML and fonts';
            $rules[] = '    AddOutputFilterByType DEFLATE application/javascript';
            $rules[] = '    AddOutputFilterByType DEFLATE application/rss+xml';
            $rules[] = '    AddOutputFilterByType DEFLATE application/vnd.ms-fontobject';
            $rules[] = '    AddOutputFilterByType DEFLATE application/x-font';
            $rules[] = '    AddOutputFilterByType DEFLATE application/x-font-opentype';
            $rules[] = '    AddOutputFilterByType DEFLATE application/x-font-otf';
            $rules[] = '    AddOutputFilterByType DEFLATE application/x-font-truetype';
            $rules[] = '    AddOutputFilterByType DEFLATE application/x-font-ttf';
            $rules[] = '    AddOutputFilterByType DEFLATE application/x-javascript';
            $rules[] = '    AddOutputFilterByType DEFLATE application/xhtml+xml';
            $rules[] = '    AddOutputFilterByType DEFLATE application/xml';
            $rules[] = '    AddOutputFilterByType DEFLATE font/opentype';
            $rules[] = '    AddOutputFilterByType DEFLATE font/otf';
            $rules[] = '    AddOutputFilterByType DEFLATE font/ttf';
            $rules[] = '    AddOutputFilterByType DEFLATE image/svg+xml';
            $rules[] = '    AddOutputFilterByType DEFLATE image/x-icon';
            $rules[] = '    AddOutputFilterByType DEFLATE text/css';
            $rules[] = '    AddOutputFilterByType DEFLATE text/html';
            $rules[] = '    AddOutputFilterByType DEFLATE text/javascript';
            $rules[] = '    AddOutputFilterByType DEFLATE text/plain';
            $rules[] = '    AddOutputFilterByType DEFLATE text/xml';
            $rules[] = '</IfModule>';
            $rules[] = '';
            
            // Try Brotli if available
            $rules[] = '<IfModule mod_brotli.c>';
            $rules[] = '    AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css';
            $rules[] = '    AddOutputFilterByType BROTLI_COMPRESS application/javascript application/x-javascript text/javascript';
            $rules[] = '    AddOutputFilterByType BROTLI_COMPRESS application/json application/xml application/rss+xml';
            $rules[] = '    AddOutputFilterByType BROTLI_COMPRESS font/opentype font/otf font/ttf';
            $rules[] = '    AddOutputFilterByType BROTLI_COMPRESS image/svg+xml image/x-icon';
            $rules[] = '</IfModule>';
        }
        
        $rules[] = self::MARKER_END;
        
        return implode("\n", $rules);
    }
    
    /**
     * Convert seconds to expires string
     */
    private function seconds_to_expires_string(int $seconds): string {
        if ($seconds >= 31536000) {
            $years = floor($seconds / 31536000);
            return $years . ' year' . ($years > 1 ? 's' : '');
        } elseif ($seconds >= 2592000) {
            $months = floor($seconds / 2592000);
            return $months . ' month' . ($months > 1 ? 's' : '');
        } elseif ($seconds >= 604800) {
            $weeks = floor($seconds / 604800);
            return $weeks . ' week' . ($weeks > 1 ? 's' : '');
        } elseif ($seconds >= 86400) {
            $days = floor($seconds / 86400);
            return $days . ' day' . ($days > 1 ? 's' : '');
        } elseif ($seconds >= 3600) {
            $hours = floor($seconds / 3600);
            return $hours . ' hour' . ($hours > 1 ? 's' : '');
        } else {
            $minutes = floor($seconds / 60);
            return $minutes . ' minute' . ($minutes > 1 ? 's' : '');
        }
    }
    
    /**
     * Get MIME type for file extension
     */
    private function get_mime_type(string $extension): string {
        $mime_types = [
            // Images
            'jpg' => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            'avif' => 'image/avif',
            'svg' => 'image/svg+xml',
            'ico' => 'image/x-icon',
            'bmp' => 'image/bmp',
            'tiff' => 'image/tiff',
            
            // Scripts & Styles
            'css' => 'text/css',
            'js' => 'application/javascript',
            'mjs' => 'application/javascript',
            
            // Fonts
            'ttf' => 'font/ttf',
            'otf' => 'font/otf',
            'woff' => 'font/woff',
            'woff2' => 'font/woff2',
            'eot' => 'application/vnd.ms-fontobject',
            
            // Documents
            'html' => 'text/html',
            'htm' => 'text/html',
            'xml' => 'application/xml',
            'json' => 'application/json',
            'pdf' => 'application/pdf',
            
            // Media
            'mp4' => 'video/mp4',
            'mp3' => 'audio/mpeg',
            'ogg' => 'audio/ogg',
            'webm' => 'video/webm',
            'wav' => 'audio/wav',
            'flac' => 'audio/flac',
            'avi' => 'video/x-msvideo',
            'mov' => 'video/quicktime',
        ];
        
        return $mime_types[$extension] ?? 'application/octet-stream';
    }

    /**
     * Resolve the configured browser cache lifetime for a given asset type.
     *
     * Supports both the current browser_cache_* keys and older cache_* keys.
     *
     * @param array  $settings Settings payload.
     * @param string $type     Asset type key.
     * @return int
     */
    private function get_browser_cache_setting_value(array $settings, string $type): int {
        $value = $settings['browser_cache_' . $type] ?? $settings['cache_' . $type] ?? self::DEFAULT_LIFETIMES[$type] ?? 0;
        return max(0, (int) $value);
    }
    
    /**
     * Remove existing rules from .htaccess
     */
    private function remove_existing_rules(string $content): string {
        $pattern = '/' . preg_quote(self::MARKER_START, '/') . '.*?' . preg_quote(self::MARKER_END, '/') . '/s';
        return preg_replace($pattern, '', $content);
    }
    
    /**
     * Remove .htaccess rules
     */
    public function remove_htaccess_rules(): bool {
        try {
            $htaccess_path = prorank_home_file_path('.htaccess');

            if (!file_exists($htaccess_path)) {
                return true;
            }

            if (!wp_is_writable($htaccess_path)) {
                return false;
            }

            $content = file_get_contents($htaccess_path);
            if ($content === false) {
                return false;
            }
            $content = $this->remove_existing_rules($content);

            return file_put_contents($htaccess_path, $content) !== false;
        } catch (\Throwable $e) {
            prorank_log('ProRank SEO: Failed to remove .htaccess rules - ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Get module settings
     * Reads from prorank_cache_settings (shared with UI)
     */
    private function get_settings(): array {
        $defaults = [
            'browser_cache_enabled' => false,
            'browser_cache_images' => (string) self::DEFAULT_LIFETIMES['images'],
            'browser_cache_css' => (string) self::DEFAULT_LIFETIMES['css'],
            'browser_cache_js' => (string) self::DEFAULT_LIFETIMES['js'],
            'browser_cache_fonts' => (string) self::DEFAULT_LIFETIMES['fonts'],
            'browser_cache_html' => (string) self::DEFAULT_LIFETIMES['html'],
        ];

        // Read from unified cache settings (same option as UI)
        $settings = get_option('prorank_cache_settings', []);
        return wp_parse_args($settings, $defaults);
    }

    /**
     * Check if module is enabled
     */
    public function is_enabled(): bool {
        $settings = $this->get_settings();
        return !empty($settings['browser_cache_enabled']);
    }

    /**
     * Get cache lifetime for a file type
     */
    private function get_cache_lifetime(string $type): int {
        $settings = $this->get_settings();
        $key = 'browser_cache_' . $type;
        return (int) ($settings[$key] ?? self::DEFAULT_LIFETIMES[$type] ?? 3600);
    }
}
