<?php
/**
 * Post Content Scanner
 *
 * Scans wp_posts.post_content for image references and validates:
 * - Domain correctness (should be local WP URL, not CDN/foreign)
 * - Attachment existence (referenced attachment still in media library)
 * - File/size validity (referenced file still in attachment metadata)
 *
 * Extends SQMedia_Batch_Processor for scan lifecycle management.
 * Unit of work = posts (not attachments).
 *
 * @package StaticQ
 * @since 3.3.0
 */

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

class SQMedia_Post_Content_Scanner extends SQMedia_Batch_Processor {

    // Issue categories (prefixed to avoid collision with file state scanner)
    const CAT_WRONG_DOMAIN = 'pcs_wrong_domain';
    const CAT_DELETED_ATTACHMENT = 'pcs_deleted_attachment';
    const CAT_STALE_SIZE = 'pcs_stale_size';
    const CAT_BROKEN_URL = 'pcs_broken_url';

    protected $scan_type = 'post_content';
    protected $cron_hook = 'sqmedia_post_content_scan_continue';
    protected $batch_size = 10;

    /**
     * Analyzer instance
     */
    private ?SQMedia_Post_Content_Analyzer $analyzer = null;

    // =========================================================================
    // BATCH PROCESSOR ABSTRACT IMPLEMENTATIONS
    // =========================================================================

    /**
     * Initialize batch size from config
     */
    protected function init_batch_size(): void {
        if (defined('SQMEDIA_POST_CONTENT_SCAN_BATCH_SIZE')) {
            $this->batch_size = (int) SQMEDIA_POST_CONTENT_SCAN_BATCH_SIZE;
        }
        $this->batch_size = max(5, min(100, $this->batch_size));
    }

    /**
     * Get all published post IDs that contain images
     */
    protected function get_items_to_process(array $options = []): array {
        global $wpdb;

        $post_types = get_post_types(['public' => true]);
        $post_types = array_values($post_types);

        // Remove 'attachment' — we scan posts, not attachments
        $post_types = array_diff($post_types, ['attachment']);

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

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

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
        $ids = $wpdb->get_col($wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts}
             WHERE post_status IN ('publish', 'draft', 'pending', 'private', 'future')
             AND post_type IN ($placeholders)
             ORDER BY ID ASC",
            ...$post_types
        ));
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

        return array_map('intval', $ids);
    }

    /**
     * Process a single post: analyze its content for image issues
     */
    protected function process_single_item(int $post_id, array $context): array {
        $post = get_post($post_id);
        if (!$post || empty($post->post_content)) {
            return ['success' => true, 'issues' => []];
        }

        $analyzer = $this->get_analyzer();
        $issues = $analyzer->analyze($post_id, $post->post_content, $context);

        if (empty($issues)) {
            return ['success' => true, 'issues' => []];
        }

        // Store issues in sq_scan_items for the details panel and fix operations
        // Group by category to use the aggregation pattern
        $categories_seen = [];
        foreach ($issues as $issue) {
            $this->store_issue($context['scan_id'], $issue['category'], $post_id, $issue['details']);
            $categories_seen[$issue['category']] = true;
        }

        // Return in the format the base class expects
        $issue_list = [];
        foreach ($categories_seen as $cat => $_) {
            $issue_list[] = ['category' => $cat];
        }

        return [
            'success' => true,
            'issues' => $issue_list,
        ];
    }

    /**
     * Build processing context
     */
    protected function build_context(string $scan_id): array {
        $upload_dir = wp_upload_dir(null, false);
        $cdn_base = SQMedia_Settings::get_cdn_base_url();

        return [
            'scan_id' => $scan_id,
            'upload_baseurl' => $upload_dir['baseurl'],
            'site_host' => wp_parse_url(home_url(), PHP_URL_HOST),
            'cdn_host' => $cdn_base ? wp_parse_url($cdn_base, PHP_URL_HOST) : null,
            'cdn_base' => $cdn_base,
            'bucket_prefix' => SQMedia_Settings::get_r2_prefix(),
        ];
    }

    /**
     * Handle scan completion
     */
    protected function on_complete(string $scan_id, array $results): void {
        sqmedia()->log('Post Content scan completed', 'info', [
            'scan_id' => $scan_id,
            'total' => $results['summary']['processed'] ?? 0,
            'with_issues' => $results['summary']['with_issues'] ?? 0,
        ]);
    }

    /**
     * Get human-readable label
     */
    public function get_label(): string {
        return __('Post Content Scanner', 'staticq-media');
    }

    /**
     * Recover item IDs if transient was lost
     */
    protected function recover_item_ids(string $scan_id, object $scan): ?array {
        $ids = $this->get_items_to_process();
        if (empty($ids)) {
            return null;
        }
        $this->log('Recovered post IDs from database', ['count' => count($ids), 'scan_id' => $scan_id]);
        return $ids;
    }

    // =========================================================================
    // FIX OPERATIONS
    // =========================================================================

    /**
     * Start fixing all fixable issues
     */
    public function start_fix(string $scan_id): array {
        $scan = $this->db->get_scan($scan_id);
        if (!$scan) {
            return ['success' => false, 'error' => __('Scan not found.', 'staticq-media')];
        }

        if ($scan->status !== 'completed') {
            return ['success' => false, 'error' => __('Scan must be completed before fixing.', 'staticq-media')];
        }

        // Get distinct post IDs with fixable issues
        $fixable_categories = [self::CAT_WRONG_DOMAIN, self::CAT_STALE_SIZE];
        $post_ids = $this->db->get_scan_items_distinct_ids($scan_id, $fixable_categories);

        if (empty($post_ids)) {
            return ['success' => false, 'error' => __('No fixable issues found.', 'staticq-media')];
        }

        $total = count($post_ids);

        // Store in transient
        set_transient('sqmedia_fix_ids_' . $scan_id, $post_ids, HOUR_IN_SECONDS * 4);

        // Update scan status
        $results = $scan->results;
        $results['fix'] = [
            'total' => $total,
            'processed' => 0,
            'fixed' => 0,
            'failed' => 0,
        ];

        $this->db->update_scan($scan_id, [
            'status' => 'fixing',
            'results' => $results,
        ]);

        // Schedule cron for background fixing
        if (!wp_next_scheduled('sqmedia_post_content_fix_continue')) {
            wp_schedule_event(time() + 10, 'sqmedia_one_minute', 'sqmedia_post_content_fix_continue');
        }

        return [
            'success' => true,
            'scan_id' => $scan_id,
            'status' => 'fixing',
            'total' => $total,
            'processed' => 0,
        ];
    }

    /**
     * Process one fix batch (one post per batch for safety)
     */
    public function process_fix_batch(string $scan_id): array {
        $scan = $this->db->get_scan($scan_id);
        if (!$scan) {
            return ['success' => false, 'error' => __('Scan not found.', 'staticq-media')];
        }

        if ($scan->status !== 'fixing') {
            $is_done = in_array($scan->status, ['completed', 'cancelled'], true);
            return [
                'success' => true,
                'done' => $is_done,
                'status' => $scan->status,
                'results' => $scan->results,
            ];
        }

        // Lock
        $lock_key = 'sqmedia_fix_lock_' . $scan_id;
        if (get_transient($lock_key)) {
            return [
                'success' => true,
                'done' => false,
                'status' => 'fixing',
                'locked' => true,
                'results' => $scan->results,
            ];
        }
        set_transient($lock_key, time(), 30);

        $post_ids = get_transient('sqmedia_fix_ids_' . $scan_id);
        if (!$post_ids || !is_array($post_ids)) {
            delete_transient($lock_key);
            return ['success' => false, 'error' => __('Fix state was lost. Please start a new scan.', 'staticq-media')];
        }

        $results = $scan->results;
        $fix = $results['fix'] ?? ['total' => count($post_ids), 'processed' => 0, 'fixed' => 0, 'failed' => 0];
        $offset = $fix['processed'];

        if ($offset >= count($post_ids)) {
            // All done
            delete_transient($lock_key);
            delete_transient('sqmedia_fix_ids_' . $scan_id);
            $this->db->update_scan($scan_id, ['status' => 'completed', 'results' => $results]);
            wp_clear_scheduled_hook('sqmedia_post_content_fix_continue');
            return [
                'success' => true,
                'done' => true,
                'status' => 'completed',
                'total' => $fix['total'],
                'processed' => $fix['processed'],
                'progress' => 100,
                'results' => $results,
            ];
        }

        $post_id = $post_ids[$offset];
        $fix_result = $this->fix_post($post_id, $scan_id);

        $fix['processed']++;
        if ($fix_result['fixed'] ?? false) {
            $fix['fixed'] += $fix_result['fixed_count'] ?? 1;
        }
        if ($fix_result['failed'] ?? false) {
            $fix['failed']++;
        }

        $results['fix'] = $fix;
        $this->db->update_scan($scan_id, ['results' => $results]);
        delete_transient($lock_key);

        $done = ($fix['processed'] >= $fix['total']);
        if ($done) {
            delete_transient('sqmedia_fix_ids_' . $scan_id);
            $this->db->update_scan($scan_id, ['status' => 'completed', 'results' => $results]);
            wp_clear_scheduled_hook('sqmedia_post_content_fix_continue');
        }

        $progress = $fix['total'] > 0 ? round(($fix['processed'] / $fix['total']) * 100) : 100;

        return [
            'success' => true,
            'done' => $done,
            'status' => $done ? 'completed' : 'fixing',
            'total' => $fix['total'],
            'processed' => $fix['processed'],
            'fixed' => $fix['fixed'],
            'failed' => $fix['failed'],
            'progress' => $progress,
            'results' => $results,
        ];
    }

    /**
     * Fix all issues for a single post
     */
    public function fix_post(int $post_id, string $scan_id): array {
        global $wpdb;

        $post = get_post($post_id);
        if (!$post) {
            return ['fixed' => false, 'failed' => true, 'error' => 'Post not found'];
        }

        $content = $post->post_content;
        $original_content = $content;
        $fixed_count = 0;

        // Get all fixable issues for this post
        $items = $this->db->get_scan_items_for_id($scan_id, $post_id);

        foreach ($items as $item) {
            $category = $item->action_type;
            $details_arr = $item->details ? json_decode($item->details, true) : [];
            $item_list = $details_arr['items'] ?? [$details_arr];

            foreach ($item_list as $details) {
                switch ($category) {
                    case self::CAT_WRONG_DOMAIN:
                        $old_url = $details['url'] ?? '';
                        $new_url = $details['local_url'] ?? '';
                        if ($old_url && $new_url && $old_url !== $new_url) {
                            $content = str_replace($old_url, $new_url, $content);
                            $fixed_count++;
                        }
                        break;

                    case self::CAT_STALE_SIZE:
                        $fixed = $this->fix_stale_size($content, $details);
                        if ($fixed !== null) {
                            $content = $fixed;
                            $fixed_count++;
                        }
                        break;
                }
            }
        }

        if ($content === $original_content) {
            return ['fixed' => false, 'fixed_count' => 0];
        }

        // Save updated content
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update($wpdb->posts, ['post_content' => $content], ['ID' => $post_id]);
        clean_post_cache($post_id);

        // Remove fixed issues from scan_items
        $this->db->remove_scan_item($scan_id, self::CAT_WRONG_DOMAIN, $post_id);
        $this->db->remove_scan_item($scan_id, self::CAT_STALE_SIZE, $post_id);

        return ['fixed' => true, 'fixed_count' => $fixed_count];
    }

    /**
     * Fix a stale size reference in content
     *
     * @return string|null Updated content, or null if no change
     */
    private function fix_stale_size(string $content, array $details): ?string {
        $attachment_id = $details['attachment_id'] ?? 0;
        $old_file = $details['referenced_file'] ?? '';
        $old_size = $details['referenced_size'] ?? null;

        if (!$attachment_id || !$old_file) {
            return null;
        }

        $metadata = wp_get_attachment_metadata($attachment_id);
        if (!is_array($metadata) || empty($metadata['file'])) {
            return null;
        }

        $dir = dirname($metadata['file']);
        if ($dir === '.') {
            $dir = '';
        }

        $analyzer = $this->get_analyzer();
        $replacement = $analyzer->find_replacement(
            $old_size,
            (int) ($details['old_width'] ?? 0),
            (int) ($details['old_height'] ?? 0),
            $metadata
        );

        $new_file = $replacement['file'];
        $new_slug = $replacement['size_label'];

        if (empty($new_file)) {
            return null;
        }

        $old_path = ($dir !== '' ? $dir . '/' : '') . $old_file;
        $new_path = ($dir !== '' ? $dir . '/' : '') . $new_file;

        if ($old_path === $new_path) {
            return null;
        }

        $content = str_replace($old_path, $new_path, $content);

        // Update sizeSlug in Gutenberg block comments if slug changed
        if ($old_size && $new_slug !== $old_size) {
            $content = preg_replace(
                '/("id":' . $attachment_id . '[^}]*"sizeSlug":")' . preg_quote($old_size, '/') . '(")/U',
                '${1}' . $new_slug . '${2}',
                $content
            );
        }

        return $content;
    }

    // =========================================================================
    // FIX LIFECYCLE (pause/resume/stop)
    // =========================================================================

    /**
     * Stop (cancel) an ongoing fix
     */
    public function stop_fix(string $scan_id): array {
        $scan = $this->db->get_scan($scan_id);
        if (!$scan || $scan->status !== 'fixing') {
            return ['success' => false, 'error' => __('No fix in progress.', 'staticq-media')];
        }

        delete_transient('sqmedia_fix_ids_' . $scan_id);
        wp_clear_scheduled_hook('sqmedia_post_content_fix_continue');

        $this->db->update_scan($scan_id, ['status' => 'completed']);

        return ['success' => true, 'message' => __('Fix stopped.', 'staticq-media')];
    }

    // =========================================================================
    // HELPERS
    // =========================================================================

    /**
     * Get or create analyzer instance
     */
    private function get_analyzer(): SQMedia_Post_Content_Analyzer {
        if (!$this->analyzer) {
            $this->analyzer = new SQMedia_Post_Content_Analyzer();
        }
        return $this->analyzer;
    }

    /**
     * Start a scan (override to clean up old scan items)
     */
    public function start(array $options = []): array {
        // Clean up previous scan items before starting a new one
        $last = $this->db->get_latest_scan($this->scan_type);
        if ($last) {
            $this->db->delete_scan_items($last->scan_id);
        }

        return parent::start($options);
    }
}
