<?php
/**
 * Upload Handler for StaticQ
 * 
 * Handles new image uploads - intercepts WordPress upload process
 * and inserts into main table with pending status for cron processing.
 * 
 * CRITICAL: This handler PREVENTS WordPress from generating sizes at upload time.
 * Sizes are generated later by the cron handler using Cloudflare.
 * 
 * @package suspended-cloud/cloudflare-media-offload
 * @since 2.0.0
 */

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

class SQMedia_Upload_Handler {
    
    /**
     * Database instance
     */
    private $db;
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->db = sqmedia()->get_database();
        $this->init_hooks();
    }
    
    /**
     * Initialize WordPress hooks
     * 
     * CRITICAL: Some hooks ALWAYS run (filename uniqueness, deletion)
     * while others only run when upload interception is enabled.
     */
    private function init_hooks(): void {
        // ALWAYS run: Handle attachment deletion to clean up bucket files
        add_action('delete_attachment', [$this, 'handle_delete'], 10, 2);
        
        // ALWAYS run: Ensure unique filenames against database (prevents R2 overwrites)
        add_filter('wp_unique_filename', [$this, 'ensure_unique_filename'], 10, 4);
        
        // Check if upload interception is enabled before registering upload hooks
        if (!SQMedia_Settings::is_upload_interception_enabled()) {
            return;
        }
        
        // =========================================================================
        // CRITICAL: DISABLE WORDPRESS SIZE GENERATION FOR IMAGES
        // These hooks PREVENT WordPress from generating sizes at upload time.
        // Sizes will be generated by cron handler using Cloudflare.
        // Note: Non-images (PDFs) are allowed to generate preview thumbnails.
        // =========================================================================
        
        // Disable WordPress native big-image scaling - we handle this via Cloudflare
        add_filter('big_image_size_threshold', [$this, 'disable_big_image_threshold'], 10, 1);
        
        // Prevent WordPress from generating intermediate sizes for IMAGES only
        // PDFs and other non-images can still generate preview thumbnails
        add_filter('intermediate_image_sizes_advanced', [$this, 'prevent_native_resizing'], 10, 3);
        
        // Intercept after upload to register in database
        add_filter('wp_generate_attachment_metadata', [$this, 'handle_upload'], 10, 3);
    }
    
    /**
     * Disable WordPress native big-image scaling
     * 
     * We handle big image scaling via Cloudflare in the cron handler.
     * 
     * @param int|false $threshold The threshold value in pixels. Return false to disable.
     * @return false Always returns false to disable WordPress scaling.
     */
    public function disable_big_image_threshold($threshold) {
        return false;
    }
    
    /**
     * Prevent native intermediate size generation for IMAGES only
     * 
     * WordPress normally generates all registered sizes (thumbnail, medium, large, etc.)
     * during upload. We return an empty array to prevent this FOR IMAGES.
     * 
     * For images: Sizes will be generated by the cron handler using Cloudflare Image Resizing.
     * For non-images (PDFs, etc.): WordPress can generate preview thumbnails normally.
     * 
     * @param array $sizes Array of size names and their data.
     * @param array $image_meta Image metadata.
     * @param int $attachment_id Attachment ID.
     * @return array Empty array for images, original sizes for non-images.
     */
    public function prevent_native_resizing($sizes, $image_meta, $attachment_id) {
        $mime_type = get_post_mime_type($attachment_id);
        $is_image = $mime_type && strpos($mime_type, 'image/') === 0;
        
        // For images: always prevent (we generate via Cloudflare)
        if ($is_image) {
            return [];
        }
        
        // For non-images (PDFs, etc.): let WordPress generate preview thumbnails
        // We'll upload these preview files along with the master in the cron handler
        return $sizes;
    }
    
    /**
     * Ensure unique filename against database records.
     *
     * WordPress's native file_exists() check fails when local files have been
     * deleted (bucket-only mode). This filter checks multiple sources to detect
     * filename collisions that would overwrite files in the bucket:
     *
     * 1. _wp_attached_file postmeta — master paths of all WP attachments
     * 2. Cross-type image conflicts — jpg/png/gif sharing the same WebP base
     * 3. Sub-sizes and original_image in same-directory attachments
     * 4. Inferred WebP variants (not stored in DB, derived at runtime)
     * 5. StaticQ table master_path (backup for edge cases)
     */
    public function ensure_unique_filename($filename, $ext, $dir, $unique_filename_callback) {
        $upload_dir = wp_upload_dir();
        $relative_dir = $this->get_relative_upload_dir($dir, $upload_dir['basedir']);

        if ($relative_dir === false) {
            return $filename;
        }

        $base = pathinfo($filename, PATHINFO_FILENAME);

        if (!$this->filename_has_conflict($base, $ext, $relative_dir)) {
            return $filename;
        }

        for ($i = 1; $i <= 100; $i++) {
            if (!$this->filename_has_conflict($base . '-' . $i, $ext, $relative_dir)) {
                return $base . '-' . $i . $ext;
            }
        }

        return $base . '-' . time() . $ext;
    }

    /**
     * Normalize an absolute upload directory to a path relative to the uploads basedir.
     *
     * @return string|false Relative directory (e.g. '2026/03') or false if outside basedir.
     */
    private function get_relative_upload_dir(string $dir, string $basedir) {
        // Normalize separators — critical on Windows where basedir uses backslashes
        $dir     = wp_normalize_path(rtrim($dir, '/\\'));
        $basedir = wp_normalize_path(rtrim($basedir, '/\\'));

        if (strpos($dir, $basedir) !== 0) {
            return false;
        }

        $relative = substr($dir, strlen($basedir));
        return ltrim($relative, '/');
    }

    /**
     * Check if a candidate filename would conflict with any existing file
     * in the upload directory, including sub-sizes, scaled variants,
     * original images, and inferred WebP derivatives.
     */
    private function filename_has_conflict(string $base, string $ext, string $relative_dir): bool {
        $lower_ext = strtolower($ext);
        $is_image  = in_array($lower_ext, ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff'], true);

        // Check 1: Master file paths in _wp_attached_file postmeta
        if ($this->conflicts_with_postmeta_masters($base, $ext, $relative_dir, $is_image, $lower_ext)) {
            return true;
        }

        // Check 2: Sub-sizes, original_image, and WebP variants of same-directory attachments
        if ($this->conflicts_with_existing_derivatives($base, $ext, $relative_dir, $is_image)) {
            return true;
        }

        // Check 3: StaticQ table (backup for orphaned bucket files)
        $prefix = $relative_dir !== '' ? $relative_dir . '/' : '';
        if ($this->conflicts_with_sq_table($prefix . $base . $ext, $prefix . $base . '-scaled' . $ext)) {
            return true;
        }

        return false;
    }

    /**
     * Check _wp_attached_file postmeta for master file collisions.
     *
     * Checks the exact path, scaled variant, WebP derivatives, and
     * cross-type image conflicts (e.g. jpg vs png sharing a WebP name).
     */
    private function conflicts_with_postmeta_masters(string $base, string $ext, string $relative_dir, bool $is_image, string $lower_ext): bool {
        global $wpdb;

        $prefix = $relative_dir !== '' ? $relative_dir . '/' : '';
        $paths  = [
            $prefix . $base . $ext,
            $prefix . $base . '-scaled' . $ext,
        ];

        // Images: check WebP master paths and cross-type conflicts
        if ($is_image) {
            $paths[] = $prefix . $base . '.webp';
            $paths[] = $prefix . $base . '-scaled.webp';

            // Cross-type: uploading photo.jpg when photo.png exists (both → photo.webp)
            foreach (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff'] as $img_ext) {
                if ($img_ext === $lower_ext) {
                    continue;
                }
                $paths[] = $prefix . $base . $img_ext;
                $paths[] = $prefix . $base . '-scaled' . $img_ext;
            }
        }

        // .webp uploads: check if any source image exists
        if ($lower_ext === '.webp') {
            foreach (['.jpg', '.jpeg', '.png', '.gif'] as $src_ext) {
                $paths[] = $prefix . $base . $src_ext;
                $paths[] = $prefix . $base . '-scaled' . $src_ext;
            }
        }

        $paths        = array_unique($paths);
        $placeholders = implode(',', array_fill(0, count($paths), '%s'));

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
        $count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_wp_attached_file' AND meta_value IN ($placeholders)",
                ...$paths
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

        return $count > 0;
    }

    /**
     * Check sub-sizes, original_image, and inferred WebP variants of
     * attachments in the same upload directory.
     */
    private function conflicts_with_existing_derivatives(string $base, string $ext, string $relative_dir, bool $is_image): bool {
        global $wpdb;

        // Find attachment IDs in the same directory (not subdirectories)
        if ($relative_dir !== '') {
            $dir_prefix = $wpdb->esc_like($relative_dir . '/');

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $same_dir_ids = $wpdb->get_col(
                $wpdb->prepare(
                    "SELECT post_id FROM {$wpdb->postmeta}
                     WHERE meta_key = '_wp_attached_file'
                     AND meta_value LIKE %s
                     AND meta_value NOT LIKE %s",
                    $dir_prefix . '%',
                    $dir_prefix . '%/%'
                )
            );
        } else {
            // Root-level uploads: files without a directory separator
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $same_dir_ids = $wpdb->get_col(
                "SELECT post_id FROM {$wpdb->postmeta}
                 WHERE meta_key = '_wp_attached_file'
                 AND meta_value NOT LIKE '%/%'"
            );
        }

        if (empty($same_dir_ids)) {
            return false;
        }

        // Build the set of filenames this upload would create
        $our_filenames = [
            $base . $ext,
            $base . '-scaled' . $ext,
        ];
        if ($is_image) {
            $our_filenames[] = $base . '.webp';
            $our_filenames[] = $base . '-scaled.webp';
        }
        $our_set = array_flip($our_filenames);

        foreach ($same_dir_ids as $att_id) {
            $meta = wp_get_attachment_metadata((int) $att_id);
            if (!is_array($meta)) {
                continue;
            }

            // Collect all filenames this existing attachment occupies on disk
            $existing = [];

            if (!empty($meta['sizes'])) {
                foreach ($meta['sizes'] as $size_data) {
                    if (!empty($size_data['file'])) {
                        $existing[] = $size_data['file'];
                    }
                }
            }

            if (!empty($meta['original_image'])) {
                $existing[] = $meta['original_image'];
            }

            // Direct collision: does any existing filename match ours?
            foreach ($existing as $fname) {
                if (isset($our_set[$fname])) {
                    return true;
                }
            }

            // WebP collision: would the inferred WebP variant of an existing
            // file clash with one of our filenames?
            if ($is_image) {
                foreach ($existing as $fname) {
                    $webp_variant = preg_replace('/\.(jpe?g|png|gif)$/i', '.webp', $fname);
                    if ($webp_variant !== $fname && isset($our_set[$webp_variant])) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * Check StaticQ's own table for master_path conflicts.
     * Backup for edge cases like orphaned bucket files whose WP attachment was deleted.
     */
    private function conflicts_with_sq_table(string $master_path, string $scaled_path): bool {
        global $wpdb;

        static $table_exists = null;
        $table = $wpdb->prefix . 'sqmedia_offloaded_files';

        if ($table_exists === null) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $table_exists = (bool) $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table));
        }

        if (!$table_exists) {
            return false;
        }

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE master_path IN (%s, %s)",
                $master_path,
                $scaled_path
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        return $count > 0;
    }
    
    /**
     * Handle new upload
     * 
     * This runs AFTER WordPress metadata is generated, but BEFORE sizes would be created.
     * We insert into our database with pending status for cron processing.
     */
    public function handle_upload(array $metadata, int $attachment_id, string $context): array {
        // Only process new uploads
        if ($context !== 'create') {
            return $metadata;
        }
        
        // Check if this attachment should be managed based on MIME type and settings
        $mime_type = get_post_mime_type($attachment_id);
        $is_image = strpos($mime_type, 'image/') === 0;
        
        if (!$is_image && !SQMedia_Settings::is_all_media_enabled()) {
            // Non-image and all-media is disabled - skip
            return $metadata;
        }
        
        // Check if plugin is configured
        if (!SQMedia_Settings::is_fully_configured()) {
            return $metadata;
        }
        
        // Get file path directly from postmeta to avoid WP Offload Media hijacking get_attached_file()
        $relative_path = get_post_meta($attachment_id, '_wp_attached_file', true);
        
        // If the file path looks like a cloud URL (from WP Offload Media), skip
        if (preg_match('#^[a-z0-9]+://#i', $relative_path)) {
            return $metadata;
        }
        
        $upload_dir = wp_upload_dir();
        $file_path = $upload_dir['basedir'] . '/' . $relative_path;
        
        if (!$file_path || !file_exists($file_path)) {
            return $metadata;
        }
        
        $media_type = $is_image ? 'image' : 'media';
        
        // Insert into main table with pending status
        $record_id = $this->db->insert_pending($attachment_id, $relative_path);
        
        if ($record_id) {
            // Trigger cron to process soon
            if (!wp_next_scheduled('sqmedia_process_queue')) {
                wp_schedule_single_event(time() + 10, 'sqmedia_process_queue');
            }
            
        } else {
        }
        
        return $metadata;
    }
    
    /**
     * Handle attachment deletion
     * 
     * Always attempts deletion from BOTH bucket and local storage
     * to ensure no orphaned files regardless of current settings.
     */
    public function handle_delete(int $attachment_id, $post): void {
        
        // Get record from database
        $record = $this->db->get_record($attachment_id);
        
        if (!$record) {
            return;
        }
        
        $s3 = sqmedia()->get_storage_handler();
        $bucket_prefix = SQMedia_Settings::get_current_prefix();
        $upload_dir = wp_upload_dir();
        
        // Collect relative paths (without bucket prefix)
        $relative_paths = [];
        
        // Master file
        if (!empty($record->master_path)) {
            $relative_paths[] = $record->master_path;
            
            // WebP version of master
            $webp_path = preg_replace('/\.(jpg|jpeg|png|gif)$/i', '.webp', $record->master_path);
            if ($webp_path !== $record->master_path) {
                $relative_paths[] = $webp_path;
            }
        }
        
        // Get sizes from WordPress metadata (source of truth)
        $metadata = wp_get_attachment_metadata($attachment_id);
        $sizes = $metadata['sizes'] ?? [];
        
        // Size files
        if (is_array($sizes)) {
            $dir = $record->master_dir ? $record->master_dir . '/' : '';
            foreach ($sizes as $size_name => $size_data) {
                if (!empty($size_data['file'])) {
                    $rel_path = $dir . $size_data['file'];
                    $relative_paths[] = $rel_path;
                    
                    // WebP version (if not already a WebP)
                    if (!str_ends_with($size_data['file'], '.webp')) {
                        $webp_path = preg_replace('/\.(jpg|jpeg|png|gif)$/i', '.webp', $rel_path);
                        if ($webp_path !== $rel_path) {
                            $relative_paths[] = $webp_path;
                        }
                    }
                }
            }
        }
        
        // Original image (if scaled)
        $original_image = $record->original_image ?? $metadata['original_image'] ?? null;
        if ($original_image) {
            $dir = $record->master_dir ? $record->master_dir . '/' : '';
            $rel_path = $dir . $original_image;
            $relative_paths[] = $rel_path;
            
            // WebP version of original
            $webp_path = preg_replace('/\.(jpg|jpeg|png|gif)$/i', '.webp', $rel_path);
            if ($webp_path !== $rel_path) {
                $relative_paths[] = $webp_path;
            }
        }
        
        // Remove duplicates
        $relative_paths = array_unique($relative_paths);
        
        // Delete from BOTH locations
        $deleted_bucket = 0;
        $deleted_local = 0;
        
        foreach ($relative_paths as $rel_path) {
            // Try bucket deletion
            $bucket_path = $bucket_prefix ? $bucket_prefix . '/' . $rel_path : $rel_path;
            if ($s3->delete_file($bucket_path)) {
                $deleted_bucket++;
            }
            
            // Try local deletion
            $local_path = $upload_dir['basedir'] . '/' . $rel_path;
            if (file_exists($local_path)) {
                wp_delete_file($local_path);
                if (!file_exists($local_path)) {
                    $deleted_local++;
                }
            }
        }
        
        
        // Delete record from database
        $this->db->delete_record($attachment_id);
        
    }
}