<?php
/**
 * Plugin Name: StaticQ Media
 * Plugin URI: https://staticq.io/
 * Description: Queue-based image processing and cloud offloading — resize, convert, and serve media from Cloudflare R2 with zero monthly fees.
 * Version: 3.3.4
 * Author: Benoit Aubert & Philippe Daix
 * License: GPL v2 or later
 * Text Domain: staticq-media
 * Domain Path: /languages
 * Requires at least: 6.0
 * Requires PHP: 7.4
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

// Plugin constants
define('SQMEDIA_VERSION', '3.3.4');
define('SQMEDIA_PLUGIN_FILE', __FILE__);
define('SQMEDIA_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('SQMEDIA_PLUGIN_URL', plugin_dir_url(__FILE__));
define('SQMEDIA_PLUGIN_BASENAME', plugin_basename(__FILE__));

/**
 * Main plugin class
 */
class SQMedia {
    
    /**
     * Single instance
     */
    private static $instance = null;
    
    /**
     * Whether plugin is initialized
     */
    private $initialized = false;
    
    /**
     * Component instances
     */
    private $database;
    private $settings;
    private $r2_handler;
    private $cloudflare_handler;
    private $url_rewriter;
    private $upload_handler;
    private $cron_handler;
    private $admin;
    
    /**
     * Get single instance
     */
    public static function get_instance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    /**
     * Constructor - only register hooks, don't load everything yet
     */
    private function __construct() {
        // Register activation/deactivation hooks BEFORE anything else
        register_activation_hook(__FILE__, [$this, 'activate']);
        register_deactivation_hook(__FILE__, [$this, 'deactivate']);
        
        // Register custom cron schedules EARLY (needed for wp-cron.php)
        add_filter('cron_schedules', [$this, 'add_cron_schedules']);
        
        // Defer full initialization to plugins_loaded
        add_action('plugins_loaded', [$this, 'init_plugin'], 10);
    }
    
    /**
     * Add custom cron schedules
     * Must be registered early so wp-cron.php can use them
     */
    public function add_cron_schedules(array $schedules): array {
        $schedules['sqmedia_one_minute'] = [
            'interval' => 60,
            'display' => __('Every Minute', 'staticq-media'),
        ];
        $schedules['sqmedia_five_minutes'] = [
            'interval' => 300,
            'display' => __('Every 5 Minutes', 'staticq-media'),
        ];
        return $schedules;
    }
    
    /**
     * Initialize plugin (called on plugins_loaded)
     */
    public function init_plugin(): void {
        if ($this->initialized) {
            return;
        }
        
        $this->initialized = true;
        
        // Load dependencies
        $this->load_dependencies();
        
        // Initialize core components only
        $this->database = new SQMedia_Database();
        $this->settings = SQMedia_Settings::get_instance();
        
        // Register cron hooks EARLY (before init) for wp-cron.php support
        // These must be available when WordPress processes scheduled events
        add_action('sqmedia_continue_registration', [$this, 'cron_continue_registration']);
        
        // File State Scanner cron hooks (new unified system)
        add_action('sqmedia_file_state_scan_continue', [$this, 'cron_file_state_scan']);
        add_action('sqmedia_build_file_index', [$this, 'cron_build_file_index']);
        add_action('sqmedia_file_state_fix_continue', [$this, 'cron_file_state_fix']);
        add_action('sqmedia_post_content_scan_continue', [$this, 'cron_post_content_scan']);
        add_action('sqmedia_post_content_fix_continue', [$this, 'cron_post_content_fix']);

        // Legacy hooks for backward compatibility
        add_action('sqmedia_continue_health_scan', [$this, 'cron_file_state_scan']);
        add_action('sqmedia_health_scan_continue', [$this, 'cron_file_state_scan']);
        add_action('sqmedia_continue_orphan_scan', [$this, 'cron_continue_orphan_scan']);
        add_action('sqmedia_continue_quarantine', [$this, 'cron_continue_quarantine']);
        add_action('sqmedia_continue_restore', [$this, 'cron_continue_restore']);
        
        // Load text domain
        $this->load_textdomain();
        
        // Check for upgrades
        $this->maybe_upgrade();
        
        // Initialize functional components on init hook (after WordPress is fully loaded)
        add_action('init', [$this, 'init_components'], 10);
    }
    
    /**
     * Initialize functional components (called on init)
     */
    public function init_components(): void {
        // URL rewriter - only if we have registered attachments
        if ($this->database->get_total_record_count() > 0) {
            $this->url_rewriter = new SQMedia_URL_Rewriter();
        }
        
        // Upload handler - only if configured
        if (SQMedia_Settings::is_fully_configured()) {
            $this->upload_handler = new SQMedia_Upload_Handler();
            $this->cron_handler = new SQMedia_Cron_Handler();

            // Self-heal: re-schedule cron if it went missing
            if (!wp_next_scheduled('sqmedia_process_queue')) {
                SQMedia_Cron_Handler::schedule_events();
            }
        }
        
        // Admin - if in admin context
        if (is_admin()) {
            $this->admin = new SQMedia_Admin();
            
            // Check for conflicting plugins
            add_action('admin_init', [$this, 'check_conflicting_plugins']);
        }
    }
    
    /**
     * List of known conflicting plugins
     * Format: 'plugin-folder/main-file.php' => 'Display Name'
     */
    private function get_conflicting_plugins(): array {
        return [
            // Image optimization plugins (resize/compress)
            'shortpixel-image-optimiser/wp-shortpixel.php' => 'ShortPixel Image Optimizer',
            'imagify/imagify.php' => 'Imagify',
            'ewww-image-optimizer/ewww-image-optimizer.php' => 'EWWW Image Optimizer',
            'wp-smushit/wp-smush.php' => 'Smush',
            'optimole-wp/optimole-wp.php' => 'Optimole',
            'tiny-compress-images/tiny-compress-images.php' => 'TinyPNG',
            'kraken-image-optimizer/kraken.php' => 'Kraken.io',
            'robin-image-optimizer/robin-image-optimizer.php' => 'Robin Image Optimizer',
            'resmushit-image-optimizer/resmushit.php' => 'reSmush.it',
            
            // Cloud offload plugins
            'amazon-s3-and-cloudfront/wordpress-s3.php' => 'WP Offload Media',
            'ilab-media-tools/ilab-media-tools.php' => 'Media Cloud',
        ];
    }
    
    /**
     * Check for conflicting plugins and show admin notice
     */
    public function check_conflicting_plugins(): void {
        // Handle dismissal first (before showing any notice)
        if (isset($_GET['sqmedia_dismiss_conflict']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'sqmedia_dismiss_conflict')) {
            $this->dismiss_conflict_notice();
            return;
        }
        
        // Only check once per session, and allow dismissal
        $dismissed = get_option('sqmedia_conflict_notice_dismissed', []);
        
        $conflicting = $this->get_conflicting_plugins();
        $active_conflicts = [];
        
        foreach ($conflicting as $plugin_path => $plugin_name) {
            // Skip if user dismissed this specific plugin
            if (in_array($plugin_path, $dismissed, true)) {
                continue;
            }
            
            if (is_plugin_active($plugin_path)) {
                $active_conflicts[$plugin_path] = $plugin_name;
            }
        }
        
        if (!empty($active_conflicts)) {
            add_action('admin_notices', function() use ($active_conflicts) {
                $this->render_conflict_notice($active_conflicts);
            });
        }
    }
    
    /**
     * Render the conflict warning notice
     */
    private function render_conflict_notice(array $conflicts): void {
        $plugin_names = implode(', ', $conflicts);
        $dismiss_url = wp_nonce_url(
            add_query_arg('sqmedia_dismiss_conflict', 'all'),
            'sqmedia_dismiss_conflict'
        );
        
        ?>
        <div class="notice notice-warning is-dismissible sqmedia-conflict-notice">
            <p>
                <strong><?php esc_html_e('StaticQ Compatibility Notice', 'staticq-media'); ?></strong>
            </p>
            <p>
                <?php
                echo esc_html(sprintf(
                    // translators: %s: conflicting plugin name
                    __('StaticQ detected %s is active. StaticQ handles image resizing, WebP generation, and cloud offloading — running both plugins may cause conflicts such as duplicate processing, metadata inconsistencies, or upload failures.', 'staticq-media'),
                    $plugin_names
                )); ?>
            </p>
            <p>
                <?php esc_html_e('We recommend using one solution or the other for best results.', 'staticq-media'); ?>
                <a href="<?php echo esc_url($dismiss_url); ?>" style="margin-left: 10px;">
                    <?php esc_html_e('Dismiss permanently', 'staticq-media'); ?>
                </a>
            </p>
        </div>
        <?php
    }
    
    /**
     * Dismiss conflict notice and redirect
     */
    private function dismiss_conflict_notice(): void {
        // Dismiss all current conflicts
        $dismissed = get_option('sqmedia_conflict_notice_dismissed', []);
        $conflicting = $this->get_conflicting_plugins();
        
        foreach ($conflicting as $plugin_path => $plugin_name) {
            if (is_plugin_active($plugin_path) && !in_array($plugin_path, $dismissed, true)) {
                $dismissed[] = $plugin_path;
            }
        }
        
        update_option('sqmedia_conflict_notice_dismissed', $dismissed);
        
        // Redirect to remove query args
        wp_safe_redirect(remove_query_arg(['sqmedia_dismiss_conflict', '_wpnonce']));
        exit;
    }
    
    /**
     * Cron: Continue registration in background
     * Delegates to base class cron_continue() which handles the multi-batch loop
     */
    public function cron_continue_registration(): void {
        $handler = new SQMedia_Registration_Handler();
        $handler->cron_continue();
    }

    /**
     * Cron: Continue post content scan in background
     */
    public function cron_post_content_scan(): void {
        $scanner = new SQMedia_Post_Content_Scanner();
        $scanner->cron_continue();
    }

    /**
     * Cron: Continue post content fix in background
     */
    public function cron_post_content_fix(): void {
        $scanner = new SQMedia_Post_Content_Scanner();
        $scan = $this->database->get_active_scan('post_content');
        if (!$scan || $scan->status !== 'fixing') {
            wp_clear_scheduled_hook('sqmedia_post_content_fix_continue');
            return;
        }
        $max_seconds = defined('SQMEDIA_CRON_MAX_SECONDS') ? (int) SQMEDIA_CRON_MAX_SECONDS : 30;
        $deadline = time() + $max_seconds;
        do {
            $result = $scanner->process_fix_batch($scan->scan_id);
            if (!empty($result['done']) || empty($result['success']) || !empty($result['locked'])) {
                break;
            }
        } while (time() < $deadline);
        if (!empty($result['done'])) {
            wp_clear_scheduled_hook('sqmedia_post_content_fix_continue');
        }
    }

    /**
     * Cron: Continue file state scan in background
     *
     * Loops through multiple batches per cron tick to fill the available
     * execution time, rather than processing a single batch and waiting
     * for the next cron trigger.
     */
    public function cron_file_state_scan(): void {

        $running_scan = $this->database->get_running_scan('file_state');

        if (!$running_scan || !in_array($running_scan->status, ['running', 'scanning'], true)) {
            wp_clear_scheduled_hook('sqmedia_file_state_scan_continue');
            wp_clear_scheduled_hook('sqmedia_health_scan_continue');
            wp_clear_scheduled_hook('sqmedia_continue_health_scan');
            return;
        }


        $max_seconds = defined('SQMEDIA_CRON_MAX_SECONDS') ? (int) SQMEDIA_CRON_MAX_SECONDS : 30;
        $deadline = time() + $max_seconds;
        $scanner = new SQMedia_File_State_Scanner();
        $batches = 0;
        $done = false;

        do {
            $result = $scanner->process_batch($running_scan->scan_id);
            $batches++;
            $done = !empty($result['done']);

            if ($done || !$result['success'] || !empty($result['locked'])) {
                break;
            }
        } while (time() < $deadline);

        $elapsed = time() + $max_seconds - $deadline; // seconds used so far

        if ($done || !$result['success'] || in_array($result['status'] ?? '', ['completed', 'failed'], true)) {
            wp_clear_scheduled_hook('sqmedia_file_state_scan_continue');
            wp_clear_scheduled_hook('sqmedia_health_scan_continue');
            wp_clear_scheduled_hook('sqmedia_continue_health_scan');
        }
    }
    
    /**
     * Cron: Build file index for Phase 2 scan
     * Called after local index is done, continues with bucket index
     */
    public function cron_build_file_index(string $scan_id = ''): void {
        
        // Try to get scan_id from the hook argument first, then from database
        if (empty($scan_id)) {
            $running_scan = $this->database->get_running_scan('file_state');
            if ($running_scan && $running_scan->status === 'building_index') {
                $scan_id = $running_scan->scan_id;
            }
        }
        
        if (empty($scan_id)) {
            wp_clear_scheduled_hook('sqmedia_build_file_index');
            return;
        }
        
        // Get scan info
        $scan = $this->database->get_scan($scan_id);
        
        if (!$scan) {
            wp_clear_scheduled_hook('sqmedia_build_file_index');
            return;
        }
        
        if ($scan->status !== 'building_index') {
            wp_clear_scheduled_hook('sqmedia_build_file_index');
            return;
        }
        
        
        // Get the scanner and build the index
        $scanner = new SQMedia_File_State_Scanner();
        $success = $scanner->build_file_indexes($scan_id);
        
        
        // Clear scheduled event - the method handles state transitions internally
        wp_clear_scheduled_hook('sqmedia_build_file_index');
    }
    
    /**
     * Cron: Continue file state fix in background
     *
     * Loops through multiple batches per cron tick to fill the available
     * execution time, rather than processing a single batch and waiting
     * for the next cron trigger.
     */
    public function cron_file_state_fix(): void {

        $running_scan = $this->database->get_running_scan('file_state');

        if (!$running_scan || $running_scan->status !== 'fixing') {
            wp_clear_scheduled_hook('sqmedia_file_state_fix_continue');
            return;
        }


        // Determine time budget: use PHP max_execution_time with a 10s safety margin
        $max_execution = (int) ini_get('max_execution_time');
        if ($max_execution <= 0) {
            // CLI or unlimited — cap at 55 seconds
            $time_limit = 55;
        } else {
            $time_limit = max(10, $max_execution - 10);
        }

        $start = time();
        $scanner = new SQMedia_File_State_Scanner();
        $batches = 0;
        $done = false;

        do {
            $result = $scanner->process_fix_batch($running_scan->scan_id);
            $batches++;
            $done = $result['done'] ?? false;

            if ($done) {
                break;
            }

            // Re-check scan status in case it was paused via AJAX
            $scan = $this->database->get_scan($running_scan->scan_id);
            if (!$scan || $scan->status !== 'fixing') {
                break;
            }
        } while ((time() - $start) < $time_limit);

        $elapsed = time() - $start;

        if ($done) {
            wp_clear_scheduled_hook('sqmedia_file_state_fix_continue');
        }
    }
    
    /**
     * Cron: Continue orphan scan in background
     */
    public function cron_continue_orphan_scan(): void {
        
        $running_scan = $this->database->get_running_scan('orphan');
        
        if (!$running_scan || $running_scan->status !== 'running') {
            wp_clear_scheduled_hook('sqmedia_continue_orphan_scan');
            return;
        }
        
        // Orphan scan processing would go here
    }

    /**
     * Cron: Continue quarantine-all in background
     *
     * Processes multiple batches per tick within the available time budget.
     */
    public function cron_continue_quarantine(): void {

        // Admin class has the quarantine methods — instantiate it
        if (!class_exists('SQMedia_Admin')) {
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-admin.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-settings-page.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-media-manager-page.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-file-state-renderer.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-file-state-ajax.php';
        }

        $admin = new SQMedia_Admin();
        $admin->cron_process_quarantine();
    }

    /**
     * Cron: Continue restore in background
     *
     * Processes multiple batches per tick within the available time budget.
     */
    public function cron_continue_restore(): void {

        if (!class_exists('SQMedia_Admin')) {
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-admin.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-settings-page.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-media-manager-page.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-file-state-renderer.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-file-state-ajax.php';
        }

        $admin = new SQMedia_Admin();
        $admin->cron_process_restore();
    }

    /**
     * Load required files
     */
    private function load_dependencies(): void {
        // Core classes
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-database.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-settings.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-attachment-record.php';
        
        // Service classes - Storage handlers
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-r2-handler.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-cloudflare-handler.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-image-processor.php';
        
        // Functional classes
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-url-rewriter.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-upload-handler.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-cron-handler.php';
        
        // Migration & Scan classes (batch processor must load before its subclasses)
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-batch-processor.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-registration-handler.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-file-state-scanner.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-post-content-analyzer.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-post-content-scanner.php';
        
        // Admin classes
        if (is_admin()) {
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-admin.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-settings-page.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-media-manager-page.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-file-state-renderer.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-file-state-ajax.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-registration-ajax.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-post-content-scanner-ajax.php';
            require_once SQMEDIA_PLUGIN_DIR . 'admin/class-sqmedia-post-content-scanner-renderer.php';

            // Cloudflare Worker deployment module
            require_once SQMEDIA_PLUGIN_DIR . 'includes/cloudflare/class-sqmedia-cloudflare-api.php';
            require_once SQMEDIA_PLUGIN_DIR . 'includes/cloudflare/class-sqmedia-cloudflare-worker.php';
            require_once SQMEDIA_PLUGIN_DIR . 'includes/cloudflare/class-sqmedia-cloudflare-deployer.php';
            require_once SQMEDIA_PLUGIN_DIR . 'includes/cloudflare/class-sqmedia-cloudflare-ajax.php';
            
            // Initialize AJAX handlers
            SQMedia_File_State_Ajax::init();
            SQMedia_Registration_Ajax::init();
            SQMedia_Post_Content_Scanner_Ajax::init();
            SQMedia_Cloudflare_Ajax::init();
        }
        
        // Load Composer autoloader for AWS SDK (R2)
        $autoloader = SQMEDIA_PLUGIN_DIR . 'vendor/autoload.php';
        if (file_exists($autoloader)) {
            require_once $autoloader;
        }
    }
    
    /**
     * Plugin activation
     */
    public function activate(): void {
        // Load dependencies for activation
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-database.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-settings.php';
        require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-cron-handler.php';
        
        // Create database tables
        SQMedia_Database::create_tables();
        
        // Set default options
        SQMedia_Settings::set_defaults();
        
        // Schedule cron events
        SQMedia_Cron_Handler::schedule_events();
        
        // Store version
        update_option('sqmedia_version', SQMEDIA_VERSION);
        
        // Flush rewrite rules
        flush_rewrite_rules();
    }
    
    /**
     * Plugin deactivation
     */
    public function deactivate(): void {
        // Load cron handler for deactivation
        if (!class_exists('SQMedia_Cron_Handler')) {
            require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-cron-handler.php';
        }
        
        // Unschedule cron events
        SQMedia_Cron_Handler::unschedule_events();
        wp_clear_scheduled_hook('sqmedia_continue_quarantine');
        wp_clear_scheduled_hook('sqmedia_continue_restore');
    }
    
    /**
     * Load text domain for translations
     */
    private function load_textdomain(): void {
        // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Custom translation paths
        load_plugin_textdomain(
            'staticq-media',
            false,
            dirname(SQMEDIA_PLUGIN_BASENAME) . '/languages'
        );
    }
    
    /**
     * Check and perform upgrades if needed
     */
    private function maybe_upgrade(): void {
        $current_version = get_option('sqmedia_version', '0');
        
        if (version_compare($current_version, SQMEDIA_VERSION, '<')) {
            // Run upgrade routines
            SQMedia_Database::create_tables(); // Ensures tables are up to date

            // Cancel any in-flight legacy registration batches
            // (registration now uses sq_scan_results via SQMedia_Batch_Processor)
            $active_legacy_batch = $this->database->get_active_batch();
            if ($active_legacy_batch && in_array($active_legacy_batch->status, ['running', 'paused'], true)) {
                $this->database->update_batch($active_legacy_batch->batch_id, [
                    'status' => 'cancelled',
                    'cancelled_at' => current_time('mysql'),
                ]);
                wp_clear_scheduled_hook('sqmedia_continue_registration');
            }

            // Update version
            update_option('sqmedia_version', SQMEDIA_VERSION);

            $this->log('Plugin upgraded', 'info', [
                'from' => $current_version,
                'to' => SQMEDIA_VERSION
            ]);
        }
    }
    
    /**
     * Get database instance
     */
    public function get_database(): SQMedia_Database {
        if ($this->database === null) {
            if (!class_exists('SQMedia_Database')) {
                require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-database.php';
            }
            $this->database = new SQMedia_Database();
        }
        return $this->database;
    }
    
    /**
     * Get settings instance
     */
    public function get_settings(): SQMedia_Settings {
        if ($this->settings === null) {
            if (!class_exists('SQMedia_Settings')) {
                require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-settings.php';
            }
            $this->settings = SQMedia_Settings::get_instance();
        }
        return $this->settings;
    }
    
    /**
     * Get R2 handler instance (lazy loading)
     */
    public function get_r2_handler(): SQMedia_R2_Handler {
        if ($this->r2_handler === null) {
            if (!class_exists('SQMedia_R2_Handler')) {
                require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-r2-handler.php';
            }
            $this->r2_handler = new SQMedia_R2_Handler();
        }
        return $this->r2_handler;
    }

    /**
     * Get the active storage handler.
     *
     * The free plugin only ships the R2 handler; kept as a separate method so
     * callers (and any future Pro plugin) can swap implementations.
     */
    public function get_storage_handler(): SQMedia_R2_Handler {
        return $this->get_r2_handler();
    }
    
    /**
     * Get Cloudflare handler instance (lazy loading)
     */
    public function get_cloudflare_handler(): SQMedia_Cloudflare_Handler {
        if ($this->cloudflare_handler === null) {
            if (!class_exists('SQMedia_Cloudflare_Handler')) {
                require_once SQMEDIA_PLUGIN_DIR . 'includes/class-sqmedia-cloudflare-handler.php';
            }
            $this->cloudflare_handler = new SQMedia_Cloudflare_Handler();
        }
        return $this->cloudflare_handler;
    }
    
    /**
     * Get URL rewriter instance
     */
    public function get_url_rewriter(): ?SQMedia_URL_Rewriter {
        return $this->url_rewriter;
    }
    
    /**
     * Get upload handler instance
     */
    public function get_upload_handler(): ?SQMedia_Upload_Handler {
        return $this->upload_handler;
    }
    
    /**
     * Get cron handler instance
     */
    public function get_cron_handler(): ?SQMedia_Cron_Handler {
        return $this->cron_handler;
    }
    
    /**
     * Log message
     */
    public function log(string $message, string $level = 'info', array $context = []): void {
        if (!defined('WP_DEBUG_LOG') || !WP_DEBUG_LOG) {
            return;
        }

        // Suppress per-image/per-size 'debug' chatter unless the developer
        // opts in via wp-config.php: define('SQMEDIA_DEBUG_VERBOSE', true);
        // Errors, warnings, and info-level events still log on standard
        // WP_DEBUG_LOG installs.
        if ($level === 'debug' && !(defined('SQMEDIA_DEBUG_VERBOSE') && SQMEDIA_DEBUG_VERBOSE)) {
            return;
        }

        $prefix = '[StaticQ] [' . strtoupper($level) . ']';
        $context_str = !empty($context) ? ' ' . json_encode($context) : '';
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
        error_log($prefix . ' ' . $message . $context_str);
    }
}

/**
 * Get plugin instance
 */
function sqmedia(): SQMedia {
    return SQMedia::get_instance();
}

// Initialize plugin (just registers hooks, defers actual initialization)
sqmedia();