<?php
/**
 * Settings Class for StaticQ
 * 
 * Manages all plugin settings with static helper methods.
 * 
 * @package suspended-cloud/staticq
 * @since 2.0.0
 */

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

class SQMedia_Settings {

    /**
     * Single instance
     */
    private static $instance = null;

    // =========================================================================
    // FEATURE AVAILABILITY
    // =========================================================================

    /**
     * Check if all features are available
     *
     * Always returns true for WP.org distribution where all features are unlocked.
     */
    public static function is_pro_active(): bool {
        return true;
    }
    
    /**
     * Settings cache
     */
    private $cache = [];
    
    /**
     * Option name prefix
     */
    const PREFIX = 'sqmedia_';
    
    /**
     * Main settings option (stores array of settings)
     */
    const OPTION_NAME = 'sqmedia_settings';
    
    /**
     * Default settings
     */
    private static $defaults = [
        // General
        'enabled' => true,
        'manage_all_media' => false, // false = images only, true = all attachment types
        'storage_mode' => 'cloud_only', // local_only, cloud_and_local, cloud_only
        'storage_provider' => 'r2', // always 'r2' in the free plugin

        // R2
        'r2_account_id' => '',
        'r2_access_key_id' => '',
        'r2_secret_access_key' => '',
        'r2_bucket' => '',
        'r2_prefix' => 'wp-content/uploads',
        'r2_public_access' => 'custom_domain',
        'r2_public_url' => '',
        'force_prefix_in_url' => false,
        
        // Cloudflare
        'cloudflare_domain' => '',
        'cloudflare_enabled' => true,
        'enable_webp' => true,
        // Image Optimization
        'cloudflare_quality' => 'balanced',
        'strip_metadata' => true,
        
        // Front-end Delivery
        'enable_picture_delivery' => false,
        'delivery_ignored_classes' => '',
        'delivery_ignored_sizes' => '',
        'enable_lazyload' => false,
        'eager_first_image' => true,
        
        // File Dimensions
        'enable_big_image_scaling' => true,
        'big_image_size_threshold_width' => 2560,
        'big_image_size_threshold_height' => 2560,
        'original_file_handling' => 'preserve', // preserve, move_to_originals
        
        // Disabled sizes (array of size names)
        'disabled_sizes' => [],
        
        // Processing
        'cron_interval' => 60,
        'max_retries' => 3,
    ];
    
    /**
     * Get single instance
     */
    public static function get_instance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    /**
     * Constructor
     */
    private function __construct() {
        // Load settings into cache
        $this->cache = get_option(self::OPTION_NAME, []);
    }
    
    /**
     * Get a setting value
     */
    public function get(string $key, $default = null) {
        if (isset($this->cache[$key])) {
            return $this->cache[$key];
        }
        
        if ($default !== null) {
            return $default;
        }
        
        return self::$defaults[$key] ?? null;
    }
    
    /**
     * Set a setting value
     */
    public function set(string $key, $value): bool {
        $this->cache[$key] = $value;
        return update_option(self::OPTION_NAME, $this->cache);
    }
    
    /**
     * Save all settings at once
     */
    public function save_all(array $settings): bool {
        $this->cache = array_merge($this->cache, $settings);
        return update_option(self::OPTION_NAME, $this->cache);
    }
    
    /**
     * Delete a setting
     */
    public function delete(string $key): bool {
        unset($this->cache[$key]);
        return update_option(self::OPTION_NAME, $this->cache);
    }
    
    /**
     * Get all settings
     */
    public function get_all(): array {
        return array_merge(self::$defaults, $this->cache);
    }
    
    /**
     * Clear settings cache
     */
    public function clear_cache(): void {
        $this->cache = get_option(self::OPTION_NAME, []);
    }
    
    /**
     * Set default settings on activation
     */
    public static function set_defaults(): void {
        $existing = get_option(self::OPTION_NAME, []);
        
        // Only set defaults for missing keys
        $merged = array_merge(self::$defaults, $existing);
        update_option(self::OPTION_NAME, $merged);
    }
    
    // =========================================================================
    // STORAGE PROVIDER METHODS
    // =========================================================================
    
    /**
     * Get current storage provider
     * @return string always 'r2' in the free plugin
     */
    public static function get_storage_provider(): string {
        return 'r2';
    }

    /**
     * Check if using R2 storage
     */
    public static function is_r2_enabled(): bool {
        return true;
    }

    /**
     * Check if cloud storage is enabled based on storage mode
     */
    public static function is_cloud_enabled(): bool {
        $mode = self::get_storage_mode();
        return $mode === 'cloud_and_local' || $mode === 'cloud_only';
    }

    /**
     * Check if cloud storage is properly configured
     */
    public static function is_cloud_storage_configured(): bool {
        return self::is_r2_configured();
    }
    
    // =========================================================================
    // STATIC CONVENIENCE GETTERS - GENERAL
    // =========================================================================
    
    /**
     * Check if upload interception is enabled
     */
    public static function is_upload_interception_enabled(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['enabled']);
    }
    
    /**
     * Get storage mode
     * @return string 'local_only', 'cloud_and_local', or 'cloud_only'
     */
    public static function get_storage_mode(): string {
        $settings = get_option(self::OPTION_NAME, []);
        $mode = $settings['storage_mode'] ?? self::$defaults['storage_mode'];
        
        // Handle legacy settings migration
        if (!isset($settings['storage_mode'])) {
            // Check for old delete_local_files setting
            if (!empty($settings['delete_local_files'])) {
                return 'cloud_only';
            }
            return 'cloud_and_local';
        }

        return $mode;
    }

    /**
     * Check if local files should be deleted after upload
     */
    public static function should_delete_local_files(): bool {
        return self::get_storage_mode() === 'cloud_only';
    }
    
    /**
     * Get storage destination based on storage mode
     * Returns 'bucket' if cloud enabled, 'local' if local only, 'both' if cloud+local
     */
    public static function get_storage_destination(): string {
        $mode = self::get_storage_mode();
        switch ($mode) {
            case 'local_only':
                return 'local';
            case 'cloud_and_local':
                return 'both';
            case 'cloud_only':
            default:
                return 'bucket';
        }
    }
    
    /**
     * Get resize engine (cloudflare or wordpress)
     */
    public static function get_resize_engine(): string {
        return self::is_cloudflare_enabled() ? 'cloudflare' : 'wordpress';
    }
    
    // =========================================================================
    // R2 SETTINGS METHODS
    // =========================================================================
    
    /**
     * Get R2 account ID
     */
    public static function get_r2_account_id(): string {
        // Check wp-config.php constant first
        if (defined('SQMEDIA_R2_ACCOUNT_ID') && !empty(SQMEDIA_R2_ACCOUNT_ID)) {
            return SQMEDIA_R2_ACCOUNT_ID;
        }
        $settings = get_option(self::OPTION_NAME, []);
        return $settings['r2_account_id'] ?? '';
    }
    
    /**
     * Get R2 access key ID
     */
    public static function get_r2_access_key_id(): string {
        // Check wp-config.php constant first (recommended for security)
        if (defined('SQMEDIA_R2_ACCESS_KEY_ID') && !empty(SQMEDIA_R2_ACCESS_KEY_ID)) {
            return SQMEDIA_R2_ACCESS_KEY_ID;
        }
        $settings = get_option(self::OPTION_NAME, []);
        return $settings['r2_access_key_id'] ?? '';
    }
    
    /**
     * Get R2 secret access key
     */
    public static function get_r2_secret_access_key(): string {
        // Check wp-config.php constant first (recommended for security)
        if (defined('SQMEDIA_R2_SECRET_ACCESS_KEY') && !empty(SQMEDIA_R2_SECRET_ACCESS_KEY)) {
            return SQMEDIA_R2_SECRET_ACCESS_KEY;
        }
        $settings = get_option(self::OPTION_NAME, []);
        return $settings['r2_secret_access_key'] ?? '';
    }
    
    /**
     * Check if R2 credentials are stored in wp-config.php
     */
    public static function are_r2_credentials_in_config(): bool {
        return (defined('SQMEDIA_R2_ACCESS_KEY_ID') && !empty(SQMEDIA_R2_ACCESS_KEY_ID))
            || (defined('SQMEDIA_R2_SECRET_ACCESS_KEY') && !empty(SQMEDIA_R2_SECRET_ACCESS_KEY));
    }
    
    /**
     * Get R2 bucket name
     */
    public static function get_r2_bucket(): string {
        // Check wp-config.php constant first
        if (defined('SQMEDIA_R2_BUCKET') && !empty(SQMEDIA_R2_BUCKET)) {
            return SQMEDIA_R2_BUCKET;
        }
        $settings = get_option(self::OPTION_NAME, []);
        return $settings['r2_bucket'] ?? '';
    }

    /**
     * Get R2 prefix (path within bucket)
     */
    /**
     * Get the bucket prefix only when it should appear in public URLs.
     *
     * Single source of truth for URL-building code. Returns r2_prefix when
     * force_prefix_in_url is checked (Direct mode); empty otherwise (Worker
     * mode). Storage I/O code keeps using get_r2_prefix() directly — the
     * prefix is always present in bucket paths.
     */
    public static function get_url_prefix(): string {
        return self::force_prefix_in_url() ? self::get_r2_prefix() : '';
    }

    /**
     * Whether the R2 prefix should appear in public URLs.
     *
     * Off (default): Worker mode — prefix is internal-only routing, public
     * URLs match WordPress's standard /wp-content/uploads/... shape and the
     * Worker (or equivalent translator) maps requests to the bucket.
     *
     * On: Direct mode — no translator. The prefix is forced into the URL,
     * e.g. https://cdn.example.com/<prefix>/<path>. Use when files are
     * served directly from R2 (r2.dev URL or plain CNAME-to-bucket).
     */
    public static function force_prefix_in_url(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['force_prefix_in_url']);
    }

    public static function get_r2_prefix(): string {
        // Check wp-config.php constant first
        if (defined('SQMEDIA_R2_PREFIX') && !empty(SQMEDIA_R2_PREFIX)) {
            return SQMEDIA_R2_PREFIX;
        }
        $settings = get_option(self::OPTION_NAME, []);
        return $settings['r2_prefix'] ?? 'wp-content/uploads';
    }
    
    /**
     * Get R2 public access method
     * @return string 'public_bucket' or 'custom_domain'
     */
    public static function get_r2_public_access(): string {
        $settings = get_option(self::OPTION_NAME, []);
        return $settings['r2_public_access'] ?? 'custom_domain';
    }
    
    /**
     * Get R2 public URL (custom domain)
     */
    public static function get_r2_public_url(): string {
        $settings = get_option(self::OPTION_NAME, []);
        return rtrim($settings['r2_public_url'] ?? '', '/');
    }
    
    /**
     * Get R2 endpoint URL
     */
    public static function get_r2_endpoint(): string {
        $account_id = self::get_r2_account_id();
        if (empty($account_id)) {
            return '';
        }
        return 'https://' . $account_id . '.r2.cloudflarestorage.com';
    }
    
    /**
     * Check if R2 is configured
     */
    public static function is_r2_configured(): bool {
        return !empty(self::get_r2_bucket()) 
            && !empty(self::get_r2_account_id())
            && !empty(self::get_r2_access_key_id())
            && !empty(self::get_r2_secret_access_key());
    }
    
    /**
     * Build R2 URL for a relative path
     */
    public static function build_r2_url(string $relative_path): string {
        $public_access = self::get_r2_public_access();
        $prefix = self::get_r2_prefix();

        if ($public_access === 'custom_domain') {
            $base = self::get_r2_public_url();
            if (empty($base)) {
                return '';
            }
            $encoded_path = SQMedia_URL_Rewriter::encode_cdn_path(ltrim($relative_path, '/'));
            return $base . '/' . $prefix . '/' . $encoded_path;
        }

        // Public bucket access not fully supported yet
        // User should configure custom domain or Cloudflare Worker
        return '';
    }
    
    // =========================================================================
    // PROVIDER-AGNOSTIC METHODS
    // =========================================================================
    
    /**
     * Get current bucket name
     */
    public static function get_current_bucket(): string {
        return self::get_r2_bucket();
    }

    /**
     * Get current prefix
     */
    public static function get_current_prefix(): string {
        return self::get_r2_prefix();
    }

    /**
     * Build URL for current storage provider
     */
    public static function build_storage_url(string $relative_path): string {
        return self::build_r2_url($relative_path);
    }
    
    // =========================================================================
    // CLOUDFLARE SETTINGS METHODS
    // =========================================================================
    
    /**
     * Get Cloudflare domain
     *
     * Falls back to the WordPress site host when the dedicated setting is empty.
     * This is correct for the common case where the WP site itself is proxied
     * through Cloudflare. If CF Image Resizing is not actually enabled on that
     * zone, runtime calls will fail and missing variants will surface in the
     * Media Library Scanner — we deliberately do not silently fall back to a
     * different encoder.
     */
    public static function get_cloudflare_domain(): string {
        $settings = get_option(self::OPTION_NAME, []);
        $domain = $settings['cloudflare_domain'] ?? '';

        if (empty($domain)) {
            $domain = wp_parse_url(home_url(), PHP_URL_HOST) ?: '';
        }

        // Remove protocol if present
        $domain = preg_replace('#^https?://#', '', $domain);
        return rtrim($domain, '/');
    }
    
    /**
     * Check if Cloudflare Image Resizing is enabled
     */
    public static function is_cloudflare_enabled(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['cloudflare_enabled']);
    }
    
    /**
     * Check if WebP is enabled
     */
    public static function is_webp_enabled(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['enable_webp']);
    }
    
    /**
     * Get Cloudflare quality setting
     */
    public static function get_cloudflare_quality(): string {
        $settings = get_option(self::OPTION_NAME, []);
        return $settings['cloudflare_quality'] ?? 'balanced';
    }
    
    /**
     * Get quality value for Cloudflare
     */
    public static function get_quality_value(): int {
        $quality = self::get_cloudflare_quality();
        $map = [
            'maximum' => 100,
            'high' => 90,
            'balanced' => 80,
            'low' => 70,
            'minimum' => 60,
        ];
        return $map[$quality] ?? 80;
    }
    
    /**
     * Check if metadata stripping is enabled
     */
    public static function should_strip_metadata(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['strip_metadata']);
    }
    
    /**
     * Check if Cloudflare is configured
     */
    public static function is_cloudflare_configured(): bool {
        return !empty(self::get_cloudflare_domain()) && self::is_cloudflare_enabled();
    }
    
    // =========================================================================
    // IMAGE SETTINGS METHODS
    // =========================================================================
    
    /**
     * Check if picture element delivery is enabled
     */
    public static function is_picture_delivery_enabled(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['enable_picture_delivery']);
    }
    
    /**
     * Get CSS classes that should be ignored by image delivery.
     * Returns array of class names (e.g., ['custom-logo', 'no-staticq']).
     */
    public static function get_delivery_ignored_classes(): array {
        $settings = get_option(self::OPTION_NAME, []);
        $raw = $settings['delivery_ignored_classes'] ?? '';

        if (empty($raw)) {
            return [];
        }

        $classes = array_map('trim', explode(',', $raw));
        return array_filter($classes);
    }

    /**
     * Get WordPress image size names that should be excluded from delivered srcset.
     * Returns array of size names (e.g., ['thumbnail', 'medium']).
     */
    public static function get_delivery_ignored_sizes(): array {
        $settings = get_option(self::OPTION_NAME, []);
        $raw = $settings['delivery_ignored_sizes'] ?? '';

        if (empty($raw)) {
            return [];
        }

        $sizes = array_map('trim', explode(',', $raw));
        return array_filter($sizes);
    }

    /**
     * Check if lazy loading is enabled
     */
    public static function is_lazyload_enabled(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['enable_lazyload']);
    }

    /**
     * Check if the first image should use eager loading instead of lazy
     */
    public static function is_eager_first_image(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['eager_first_image']);
    }

    /**
     * Check if big image scaling is enabled
     */
    public static function is_big_image_scaling_enabled(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['enable_big_image_scaling']);
    }
    
    /**
     * Get big image threshold width
     */
    public static function get_big_image_threshold_width(): int {
        $settings = get_option(self::OPTION_NAME, []);
        return (int) ($settings['big_image_size_threshold_width'] ?? 2560);
    }
    
    /**
     * Alias for get_big_image_threshold_width
     */
    public static function get_big_image_width(): int {
        return self::get_big_image_threshold_width();
    }
    
    /**
     * Get big image threshold height
     */
    public static function get_big_image_threshold_height(): int {
        $settings = get_option(self::OPTION_NAME, []);
        return (int) ($settings['big_image_size_threshold_height'] ?? 2560);
    }
    
    /**
     * Alias for get_big_image_threshold_height
     */
    public static function get_big_image_height(): int {
        return self::get_big_image_threshold_height();
    }
    
    /**
     * Get original file handling mode
     */
    public static function get_original_file_handling(): string {
        $settings = get_option(self::OPTION_NAME, []);
        return $settings['original_file_handling'] ?? 'preserve';
    }
    
    /**
     * Get disabled sizes
     */
    public static function get_disabled_sizes(): array {
        $settings = get_option(self::OPTION_NAME, []);
        $disabled = $settings['disabled_sizes'] ?? [];
        return is_array($disabled) ? $disabled : [];
    }
    
    /**
     * Get enabled image sizes (excludes disabled ones)
     */
    public static function get_enabled_sizes(): array {
        $disabled_sizes = self::get_disabled_sizes();
        $all_sizes = wp_get_registered_image_subsizes();
        
        foreach ($disabled_sizes as $disabled_size) {
            unset($all_sizes[$disabled_size]);
        }
        
        return $all_sizes;
    }
    
    /**
     * Check if a specific size is enabled
     */
    public static function is_size_enabled(string $size_name): bool {
        $disabled_sizes = self::get_disabled_sizes();
        return !in_array($size_name, $disabled_sizes, true);
    }
    
    /**
     * Get custom sizes to ignore in scanner
     * These sizes won't be flagged as "deprecated" even if not in registered sizes
     */
    public static function get_ignored_sizes(): array {
        $settings = get_option(self::OPTION_NAME, []);
        $ignored = $settings['ignored_sizes'] ?? '';

        if (empty($ignored)) {
            return [];
        }

        // Parse comma-separated list, trim whitespace
        $sizes = array_map('trim', explode(',', $ignored));
        return array_filter($sizes); // Remove empty strings
    }
    
    /**
     * Check if a size should be ignored by the scanner
     */
    public static function is_size_ignored(string $size_name): bool {
        $ignored = self::get_ignored_sizes();
        return in_array($size_name, $ignored, true);
    }
    
    // =========================================================================
    // PROCESSING SETTINGS METHODS
    // =========================================================================
    
    /**
     * Get batch size for cron upload processing.
     * Override with SQMEDIA_CRON_BATCH_SIZE constant in wp-config.php.
     */
    public static function get_batch_size(): int {
        if (defined('SQMEDIA_CRON_BATCH_SIZE')) {
            return max(1, min(100, (int) SQMEDIA_CRON_BATCH_SIZE));
        }
        return 10;
    }
    
    /**
     * Get cron interval
     */
    public static function get_cron_interval(): int {
        $settings = get_option(self::OPTION_NAME, []);
        return (int) ($settings['cron_interval'] ?? 60);
    }
    
    /**
     * Get max retries
     */
    public static function get_max_retries(): int {
        $settings = get_option(self::OPTION_NAME, []);
        return (int) ($settings['max_retries'] ?? 3);
    }
    
    // =========================================================================
    // MEDIA TYPE SETTINGS
    // =========================================================================
    
    /**
     * Check if all media types are managed (not just images)
     */
    public static function is_all_media_enabled(): bool {
        $settings = get_option(self::OPTION_NAME, []);
        return !empty($settings['manage_all_media']);
    }
    
    /**
     * Check if an attachment should be managed based on current settings
     * 
     * @param int $attachment_id Attachment ID
     * @return bool True if this attachment should be managed
     */
    public static function should_manage_attachment(int $attachment_id): bool {
        $mime_type = get_post_mime_type($attachment_id);
        
        if (!$mime_type) {
            return false;
        }
        
        // Always manage images
        if (strpos($mime_type, 'image/') === 0) {
            return true;
        }
        
        // Manage non-images only if setting is enabled
        return self::is_all_media_enabled();
    }
    
    /**
     * Check if a MIME type is an image
     * 
     * @param string $mime_type MIME type to check
     * @return bool True if image
     */
    public static function is_image_mime_type(string $mime_type): bool {
        return strpos($mime_type, 'image/') === 0;
    }
    
    // =========================================================================
    // CHUNKED UPLOAD SETTINGS
    // =========================================================================
    
    /**
     * Get chunk upload threshold in bytes
     * Files larger than this will use chunked/multipart uploads
     * 
     * Default: 50MB, overridable via SQMEDIA_CHUNK_UPLOAD_THRESHOLD constant
     */
    public static function get_chunk_upload_threshold(): int {
        if (defined('SQMEDIA_CHUNK_UPLOAD_THRESHOLD')) {
            return (int) SQMEDIA_CHUNK_UPLOAD_THRESHOLD;
        }
        return 50 * 1024 * 1024; // 50MB
    }
    
    /**
     * Get chunk size for multipart uploads in bytes
     * 
     * Default: 10MB, overridable via SQMEDIA_CHUNK_SIZE constant
     */
    public static function get_chunk_size(): int {
        if (defined('SQMEDIA_CHUNK_SIZE')) {
            return max(5 * 1024 * 1024, (int) SQMEDIA_CHUNK_SIZE); // Minimum 5MB (S3/R2 requirement)
        }
        return 10 * 1024 * 1024; // 10MB
    }
    
    /**
     * Check if a file should use chunked upload based on size
     * 
     * @param int $file_size File size in bytes
     * @return bool True if chunked upload should be used
     */
    public static function should_use_chunked_upload(int $file_size): bool {
        return $file_size >= self::get_chunk_upload_threshold();
    }
    
    // =========================================================================
    // VALIDATION METHODS
    // =========================================================================
    
    /**
     * Check if the plugin is configured to operate.
     *
     * The contract is the user-facing "Intercept WordPress uploads" checkbox:
     * if it is on, the plugin runs (queues uploads, runs cron). Cloud-storage
     * readiness is a downstream choice in the pipeline, not a precondition for
     * the pipeline to exist — see is_cloud_storage_configured() for that.
     */
    public static function is_fully_configured(): bool {
        return self::is_upload_interception_enabled();
    }
    
    /**
     * Get configuration status
     */
    public static function get_config_status(): array {
        $provider = self::get_storage_provider();
        
        return [
            'storage_provider' => $provider,
            'r2_configured' => self::is_r2_configured(),
            'cloud_storage_configured' => self::is_cloud_storage_configured(),
            'storage_mode' => self::get_storage_mode(),
            'cloudflare_configured' => self::is_cloudflare_configured(),
            'fully_configured' => self::is_fully_configured(),
            'upload_interception' => self::is_upload_interception_enabled(),
            'cloudflare_enabled' => self::is_cloudflare_enabled(),
            'webp_enabled' => self::is_webp_enabled(),
            'picture_delivery' => self::is_picture_delivery_enabled(),
            'storage_destination' => self::get_storage_destination(),
            'delete_local' => self::should_delete_local_files(),
            'resize_engine' => self::is_cloudflare_enabled() ? 'cloudflare' : 'wordpress',
        ];
    }
    
    // =========================================================================
    // URL BUILDING METHODS
    // =========================================================================
    
    /**
     * Get the base URL for serving files from a separate CDN.
     *
     * Returns r2_public_url when set, otherwise empty. The CF domain is
     * intentionally NOT a fallback here — that field is used by CF Image
     * Resizing (a transform proxy concern) and conflating it with "where
     * files are publicly served from" caused URL rewriting to activate
     * unintentionally and leak the bucket prefix into Media Library URLs.
     *
     * Empty return means "let WordPress emit its own URLs"; the Worker
     * (or equivalent) is responsible for serving the bytes from the bucket.
     */
    public static function get_cdn_base_url(): string {
        return self::get_r2_public_url();
    }
    
    /**
     * Build the public URL for a file at $relative_path (relative to wp-uploads).
     *
     * Two independent dimensions:
     * - Host: r2_public_url if set, otherwise the WordPress site's host.
     * - Path: includes the bucket prefix when force_prefix_in_url is on
     *   (Direct mode), uses wp_upload_dir()['baseurl'] shape when off
     *   (Worker mode, the default — Worker translates URL to bucket).
     */
    public static function build_bucket_url(string $relative_path): string {
        $encoded_path = SQMedia_URL_Rewriter::encode_cdn_path(ltrim($relative_path, '/'));
        $cdn = self::get_r2_public_url();

        // Direct mode: prefix is forced into URL.
        if (self::force_prefix_in_url()) {
            // Host: custom domain if set, else the WP site host.
            $host = !empty($cdn) ? $cdn : home_url();
            $url = rtrim($host, '/');
            $prefix = trim(self::get_r2_prefix(), '/');
            if (!empty($prefix)) {
                $url .= '/' . $prefix;
            }
            return $url . '/' . $encoded_path;
        }

        // Worker mode (default): URL matches wp_upload_dir()['baseurl'] shape.
        $upload = wp_upload_dir();
        $baseurl = $upload['baseurl'] ?? '';
        if (empty($baseurl)) {
            return '';
        }

        // If a CDN host is configured, swap the WP host for it, keep the path.
        if (!empty($cdn)) {
            $wp_host    = wp_parse_url(home_url(), PHP_URL_HOST);
            $cdn_p      = wp_parse_url($cdn);
            $cdn_host   = $cdn_p['host']   ?? null;
            $cdn_scheme = $cdn_p['scheme'] ?? 'https';
            if ($wp_host && $cdn_host) {
                $baseurl = preg_replace(
                    '#^https?://' . preg_quote($wp_host, '#') . '#',
                    $cdn_scheme . '://' . $cdn_host,
                    $baseurl
                );
            }
        }

        return rtrim($baseurl, '/') . '/' . $encoded_path;
    }

    /**
     * Build Cloudflare Image Resizing URL
     */
    public static function build_resize_url(string $source_url, array $params = []): string {
        $domain = self::get_cloudflare_domain();
        
        if (empty($domain)) {
            return $source_url;
        }
        
        // Default parameters
        $defaults = [
            'format' => 'auto',
            'fit' => 'scale-down',
            'quality' => self::get_quality_value(),
        ];
        
        // Add metadata stripping if enabled
        if (self::should_strip_metadata()) {
            $defaults['metadata'] = 'none';
        }
        
        $params = array_merge($defaults, $params);
        
        // Build params string
        $param_parts = [];
        foreach ($params as $key => $value) {
            if ($value !== null && $value !== '') {
                $param_parts[] = $key . '=' . $value;
            }
        }
        $param_string = implode(',', $param_parts);
        
        // Get path from source URL
        $parsed = wp_parse_url($source_url);
        $path = $parsed['path'] ?? '';
        
        return 'https://' . $domain . '/cdn-cgi/image/' . $param_string . $path;
    }
    
}