<?php

/**
 * Admin interface class with URL management
 *
 * @package ContentPerformanceAnalyzer
 * @since 1.0.0
 */
// Prevent direct access
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
class CPAP_Admin {
    /**
     * Admin page hook suffixes — populated in add_admin_menu()
     */
    private $page_hooks = array();

    /**
     * Constructor
     */
    public function __construct() {
    }

    /**
     * Initialize admin interface
     */
    public function init() {
        add_action( 'admin_menu', array($this, 'add_admin_menu') );
        add_action( 'admin_init', array($this, 'register_settings') );
        add_action( 'admin_init', array($this, 'handle_manual_trigger') );
        add_action( 'admin_init', array($this, 'handle_url_upload') );
        add_action( 'wp_ajax_cpanalyzer_retry_missing_ai', array($this, 'ajax_retry_missing_ai') );
        add_action( 'wp_ajax_cpanalyzer_rescan_all_urls', array($this, 'ajax_rescan_all_urls') );
        add_action( 'wp_ajax_cpanalyzer_sync_batch_data', array($this, 'ajax_sync_batch_data') );
        add_action( 'wp_ajax_cpanalyzer_force_restart_processing', array($this, 'ajax_force_restart_processing') );
        add_action( 'wp_ajax_cpanalyzer_reset_all_data', array($this, 'ajax_reset_all_data') );
        add_action( 'wp_ajax_cpanalyzer_test_google_connection', array($this, 'ajax_test_google_connection') );
        add_action( 'wp_ajax_cpanalyzer_google_oauth_disconnect', array($this, 'ajax_google_oauth_disconnect') );
        add_action( 'wp_ajax_cpanalyzer_generate_refresh_brief', array($this, 'ajax_generate_refresh_brief') );
        add_action( 'wp_ajax_cpanalyzer_get_refresh_brief', array($this, 'ajax_get_refresh_brief') );
        add_action( 'wp_ajax_cpanalyzer_trigger_data_collection', array($this, 'ajax_trigger_data_collection') );
        add_action( 'wp_ajax_cpanalyzer_get_site_pages', array($this, 'ajax_get_site_pages') );
        add_action( 'wp_ajax_cpanalyzer_import_site_pages', array($this, 'ajax_import_site_pages') );
        add_action( 'wp_ajax_cpanalyzer_fix_image_alt', array($this, 'ajax_fix_image_alt') );
        add_action( 'wp_ajax_cpanalyzer_list_google_properties', array($this, 'ajax_list_google_properties') );
        add_action( 'wp_ajax_cpanalyzer_generate_answer_block', array($this, 'ajax_generate_answer_block') );
        add_action( 'wp_ajax_cpanalyzer_mark_page_fixed', array($this, 'ajax_mark_page_fixed') );
        add_action( 'wp_ajax_cpanalyzer_create_refresh_draft', array($this, 'ajax_create_refresh_draft') );
        // At-a-glance priority pages on the WordPress dashboard.
        add_action( 'wp_dashboard_setup', array($this, 'register_dashboard_widget') );
        // Handle the Google OAuth callback before any output.
        add_action( 'admin_init', array($this, 'handle_oauth_callback'), 1 );
        // Hosted one-click Google flow (start + return legs via the proxy).
        add_action( 'admin_init', array($this, 'handle_hosted_oauth'), 1 );
        // Surface a fatal Google connection failure on plugin pages.
        add_action( 'admin_notices', array($this, 'render_google_connection_notice') );
        add_action( 'admin_notices', array($this, 'render_new_flags_notice') );
        add_action( 'admin_notices', array($this, 'render_recovery_notice') );
        add_action( 'admin_notices', array($this, 'render_first_value_review_ask') );
        add_action( 'admin_init', array($this, 'handle_review_ask_action') );
        // Also on the daily cron: a site nobody logs into for three weeks
        // would otherwise never reach a verdict, so the notice and the
        // review ask would never fire either.
        add_action( 'cpanalyzer_daily_refresh', array($this, 'maybe_check_recovery_verdicts') );
        add_action( 'cpanalyzer_ai_drain', array($this, 'run_ai_drain') );
        add_action( 'wp_ajax_cpanalyzer_validate_psi_key', array($this, 'ajax_validate_psi_key') );
        add_action( 'wp_ajax_cpanalyzer_test_psi_api', array($this, 'ajax_test_psi_api') );
        add_action( 'wp_ajax_cpanalyzer_check_status', array($this, 'ajax_check_status') );
        add_action( 'wp_ajax_cpanalyzer_reprocess_one_url', array($this, 'ajax_reprocess_one_url') );
        add_action( 'admin_enqueue_scripts', array($this, 'enqueue_admin_scripts') );
        // Ensure batch processor AJAX handlers are registered
        $this->ensure_batch_processor_ajax();
        // Handle CSV export before any output is sent
        add_action( 'admin_init', array($this, 'handle_csv_export'), 5 );
        // Sync any existing batch data on admin init
        add_action( 'admin_init', array($this, 'sync_batch_data_to_insights'), 20 );
        // Clear any existing non-uploaded data on plugin activation
        add_action( 'admin_init', array($this, 'ensure_uploaded_urls_only'), 25 );
    }

    /**
     * Ensure batch processor AJAX handlers are registered
     */
    private function ensure_batch_processor_ajax() {
        if ( !class_exists( 'CPAP_Batch_Processor' ) ) {
            require_once CPANALYZER_PLUGIN_DIR . 'includes/class-cpa-batch-processor.php';
        }
        // Register AJAX handlers directly
        add_action( 'wp_ajax_cpanalyzer_start_batch_process', array($this, 'proxy_batch_ajax_start') );
        add_action( 'wp_ajax_cpanalyzer_get_batch_status', array($this, 'proxy_batch_ajax_status') );
        add_action( 'wp_ajax_cpanalyzer_retry_failed_url', array($this, 'proxy_batch_ajax_retry') );
        add_action( 'wp_ajax_cpanalyzer_cancel_batch', array($this, 'proxy_batch_ajax_cancel') );
    }

    /**
     * Handle CSV export before any HTML output
     */
    public function handle_csv_export() {
        // Only handle on insights page
        if ( !isset( $_GET['page'] ) || $_GET['page'] !== 'content-insights' ) {
            return;
        }
        // Check if export was requested
        if ( isset( $_POST['cpanalyzer_export_csv'] ) ) {
            if ( !current_user_can( 'manage_options' ) ) {
                wp_die( esc_html__( 'You do not have permission to export this data.', 'content-performance-analyzer' ) );
            }
            if ( isset( $_POST['cpanalyzer_export_csv_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['cpanalyzer_export_csv_nonce'] ) ), 'cpanalyzer_export_csv' ) ) {
                $this->export_insights_to_csv();
            } else {
                wp_die( esc_html__( 'Security check failed', 'content-performance-analyzer' ) );
            }
        }
    }

    /**
     * Proxy AJAX methods to batch processor
     */
    public function proxy_batch_ajax_start() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $batch_processor = new CPAP_Batch_Processor();
        $batch_processor->ajax_start_batch_process();
    }

    public function proxy_batch_ajax_status() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $batch_processor = new CPAP_Batch_Processor();
        $batch_processor->ajax_get_batch_status();
    }

    public function proxy_batch_ajax_retry() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $batch_processor = new CPAP_Batch_Processor();
        $batch_processor->ajax_retry_failed_url();
    }

    public function proxy_batch_ajax_cancel() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $batch_processor = new CPAP_Batch_Processor();
        $batch_processor->ajax_cancel_batch();
    }

    /**
     * Add admin menu pages
     */
    public function add_admin_menu() {
        $insights_hook = add_menu_page(
            __( 'Content Insights', 'content-performance-analyzer' ),
            __( 'Content Insights', 'content-performance-analyzer' ),
            'manage_options',
            'content-insights',
            array($this, 'insights_page'),
            'dashicons-chart-line',
            30
        );
        $this->page_hooks[] = $insights_hook;
        $this->page_hooks['insights'] = $insights_hook;
        $url_management_hook = add_submenu_page(
            'content-insights',
            __( 'URL Management', 'content-performance-analyzer' ),
            __( 'URL Management', 'content-performance-analyzer' ),
            'manage_options',
            'cpanalyzer-url-management',
            array($this, 'url_management_page')
        );
        $this->page_hooks[] = $url_management_hook;
        $this->page_hooks['url_management_page'] = $url_management_hook;
        $this->page_hooks['internal_links'] = add_submenu_page(
            'content-insights',
            __( 'Internal Links', 'content-performance-analyzer' ),
            __( 'Internal Links', 'content-performance-analyzer' ),
            'manage_options',
            'cpanalyzer-internal-links',
            array($this, 'internal_links_page')
        );
        $this->page_hooks['interlinking'] = add_submenu_page(
            'content-insights',
            __( 'Smart Interlinking', 'content-performance-analyzer' ),
            __( 'Smart Interlinking', 'content-performance-analyzer' ),
            'manage_options',
            'cpanalyzer-interlinking',
            array($this, 'interlinking_page')
        );
        if ( !apply_filters( 'cpanalyzer_show_smart_interlinking_menu', false ) ) {
            remove_submenu_page( 'content-insights', 'cpanalyzer-interlinking' );
        }
        $this->page_hooks['url_management'] = add_submenu_page(
            'content-insights',
            __( 'CPA Settings', 'content-performance-analyzer' ),
            __( 'Settings', 'content-performance-analyzer' ),
            'manage_options',
            'cpanalyzer-settings',
            array($this, 'settings_page')
        );
    }

    /**
     * Show an error banner on plugin pages when the Google connection has
     * failed fatally (e.g. invalid_grant) and GA4/GSC data can no longer
     * be refreshed. Without this, a dead token fails silently and the
     * dashboard just shows zeros.
     */
    public function render_google_connection_notice() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        // Only on this plugin's admin pages (toplevel_page_content-insights,
        // content-insights_page_*).
        $screen = ( function_exists( 'get_current_screen' ) ? get_current_screen() : null );
        if ( !$screen || false === strpos( (string) $screen->id, 'content-insights' ) ) {
            return;
        }
        if ( !class_exists( 'CPAP_Google_OAuth' ) ) {
            return;
        }
        $oauth = new CPAP_Google_OAuth();
        $error = $oauth->get_connection_error();
        if ( empty( $error ) ) {
            return;
        }
        $error_message = ( !empty( $error['message'] ) ? $error['message'] : $error['code'] );
        $settings_url = admin_url( 'admin.php?page=cpanalyzer-settings' );
        ?>
        <div class="notice notice-error">
            <p>
                <strong><?php 
        esc_html_e( 'Google connection lost — GA4 and Search Console data can\'t be refreshed.', 'content-performance-analyzer' );
        ?></strong>
            </p>
            <p>
                <?php 
        printf( 
            /* translators: %s: OAuth error returned by Google, e.g. "invalid_grant — Bad Request" */
            esc_html__( 'Google rejected the stored token: %s', 'content-performance-analyzer' ),
            '<code>' . esc_html( $error_message ) . '</code>'
         );
        ?>
            </p>
            <?php 
        if ( 'invalid_grant' === $error['code'] ) {
            ?>
                <p>
                    <?php 
            esc_html_e( 'This usually means the refresh token expired or was revoked. If your OAuth consent screen is in Testing mode, Google expires tokens every 7 days — set the user type to Internal or publish the app to Production to stop this from recurring.', 'content-performance-analyzer' );
            ?>
                </p>
            <?php 
        }
        ?>
            <p>
                <a href="<?php 
        echo esc_url( $settings_url );
        ?>" class="button button-primary">
                    <?php 
        esc_html_e( 'Reconnect Google Account', 'content-performance-analyzer' );
        ?>
                </a>
            </p>
        </div>
        <?php 
    }

    /**
     * Register plugin settings
     */
    public function register_settings() {
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_openai_key', array(
            'sanitize_callback' => array($this, 'sanitize_ai_api_key'),
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_serper_api_key', array(
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_ai_provider', array(
            'sanitize_callback' => array($this, 'sanitize_ai_provider'),
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_ai_model', array(
            'sanitize_callback' => array($this, 'sanitize_ai_model'),
            'default'           => '',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_property_id', array(
            'sanitize_callback' => 'sanitize_text_field',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_site_url', array(
            'sanitize_callback' => array($this, 'sanitize_site_url'),
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_conversion_events', array(
            'sanitize_callback' => array($this, 'sanitize_conversion_events'),
            'default'           => '',
        ) );
        // OAuth 2.0 credentials (replaced Service Account JSON).
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_oauth_client_id', array(
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_oauth_client_secret', array(
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_batch_size', array(
            'sanitize_callback' => 'intval',
            'default'           => 10,
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_off_peak_hour', array(
            'sanitize_callback' => 'intval',
            'default'           => 2,
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_enable_auto_scan', array(
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => 'no',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_verify_ssl', array(
            'sanitize_callback' => function ( $v ) {
                return ( $v === 'no' ? 'no' : 'yes' );
            },
            'default'           => 'yes',
        ) );
        // PageSpeed Insights settings
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_pagespeed_enable', array(
            'sanitize_callback' => function ( $v ) {
                return ( $v === 'yes' ? 'yes' : 'no' );
            },
            'default'           => 'no',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_pagespeed_api_key', array(
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_llms_txt_enable', array(
            'sanitize_callback' => array($this, 'sanitize_llms_txt_enable'),
            'default'           => 'no',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_psi_pages', array(
            'sanitize_callback' => array($this, 'sanitize_psi_pages'),
            'default'           => array(),
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_pagespeed_strategy', array(
            'sanitize_callback' => array($this, 'sanitize_psi_strategy'),
            'default'           => 'mobile',
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_pagespeed_rate_limit', array(
            'sanitize_callback' => function ( $v ) {
                return max( 1, min( 60, intval( $v ) ) );
            },
            'default'           => 30,
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_pagespeed_categories', array(
            'sanitize_callback' => array($this, 'sanitize_psi_categories'),
            'default'           => array('PERFORMANCE'),
        ) );
        register_setting( 'cpanalyzer_settings', 'cpanalyzer_pagespeed_cache_hours', array(
            'sanitize_callback' => function ( $v ) {
                return max( 1, min( 168, intval( $v ) ) );
            },
            'default'           => 24,
        ) );
    }

    /**
     * Handle URL file upload
     */
    public function handle_url_upload() {
        if ( !isset( $_POST['cpanalyzer_upload_urls_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['cpanalyzer_upload_urls_nonce'] ) ), 'cpanalyzer_upload_urls' ) ) {
            return;
        }
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        $url_input = ( isset( $_POST['cpanalyzer_url_list'] ) ? trim( (string) wp_unslash( $_POST['cpanalyzer_url_list'] ) ) : '' );
        if ( $url_input === '' ) {
            add_settings_error(
                'cpanalyzer_messages',
                'cpanalyzer_upload_error',
                __( 'Please paste at least one URL, one per line.', 'content-performance-analyzer' ),
                'error'
            );
            return;
        }
        $urls = preg_split( '/\\r\\n|\\r|\\n/', $url_input );
        $upload_mode = ( isset( $_POST['cpanalyzer_upload_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['cpanalyzer_upload_mode'] ) ) : 'replace' );
        $processed = $this->process_uploaded_urls( $urls, $upload_mode );
        if ( $processed['success'] ) {
            add_settings_error(
                'cpanalyzer_messages',
                'cpanalyzer_upload_success',
                sprintf(
                    /* translators: 1: total URLs processed; 2: URLs added; 3: URLs skipped */
                    __( 'Successfully processed %1$d URLs. %2$d added, %3$d skipped.', 'content-performance-analyzer' ),
                    $processed['total'],
                    $processed['added'],
                    $processed['skipped']
                ),
                'success'
            );
        } else {
            add_settings_error(
                'cpanalyzer_messages',
                'cpanalyzer_upload_error',
                $processed['message'],
                'error'
            );
        }
    }

    /**
     * Process uploaded URLs
     */
    private function process_uploaded_urls( $urls, $mode = 'replace' ) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // Clean and validate URLs
        $valid_urls = array();
        foreach ( $urls as $url ) {
            $url = trim( $url );
            // Skip empty lines
            if ( empty( $url ) ) {
                continue;
            }
            // Ensure URL starts with /
            if ( strpos( $url, '/' ) !== 0 && strpos( $url, 'http' ) !== 0 ) {
                $url = '/' . $url;
            }
            // Normalize URL
            $url = cpanalyzer_normalize_path( $url );
            // Validate URL format
            if ( !empty( $url ) ) {
                $valid_urls[] = $url;
            }
        }
        if ( empty( $valid_urls ) ) {
            return array(
                'success' => false,
                'message' => __( 'No valid URLs found in the file.', 'content-performance-analyzer' ),
            );
        }
        // Handle replace mode
        if ( $mode === 'replace' ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->query( "TRUNCATE TABLE " . esc_sql( $table_name ) );
        }
        // Insert URLs with order preservation
        $added = 0;
        $skipped = 0;
        $order_index = 1;
        foreach ( $valid_urls as $url ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM " . esc_sql( $table_name ) . " WHERE url = %s", $url ) );
            if ( $exists && $mode === 'merge' ) {
                $skipped++;
                $order_index++;
                continue;
            }
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $result = $wpdb->insert( $table_name, array(
                'url'        => $url,
                'status'     => 'pending',
                'priority'   => $order_index,
                'created_at' => current_time( 'mysql' ),
            ), array(
                '%s',
                '%s',
                '%d',
                '%s'
            ) );
            if ( $result ) {
                $added++;
            } else {
                $skipped++;
            }
            $order_index++;
        }
        return array(
            'success' => true,
            'total'   => count( $valid_urls ),
            'added'   => $added,
            'skipped' => $skipped,
        );
    }

    /**
     * AJAX: list every published, publicly viewable page and post on this
     * site — the same set of URLs the XML sitemap covers — so users can
     * import them without pasting URLs by hand.
     */
    public function ajax_get_site_pages() {
        check_ajax_referer( 'cpanalyzer_site_pages', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'You do not have permission to do this.', 'content-performance-analyzer' ),
            ), 403 );
        }
        // The analyzer matches pages by URL path (GA4 pagePath, GSC page),
        // which plain permalinks (?p=123) can't provide — every permalink
        // would collapse to '/'. Surface that instead of importing garbage.
        if ( '' === (string) get_option( 'permalink_structure' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Your site uses plain permalinks, so pages can\'t be identified by URL path. Go to Settings → Permalinks and choose any pretty structure (e.g. "Post name"), then try again.', 'content-performance-analyzer' ),
            ) );
        }
        $post_types = get_post_types( array(
            'public' => true,
        ), 'names' );
        unset($post_types['attachment']);
        $query = new WP_Query(array(
            'post_type'              => array_values( $post_types ),
            'post_status'            => 'publish',
            'posts_per_page'         => 2000,
            'orderby'                => array(
                'type'  => 'ASC',
                'title' => 'ASC',
            ),
            'fields'                 => 'ids',
            'no_found_rows'          => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        ));
        $pages = array();
        $seen = array();
        // The homepage is not a post when the site shows latest posts on
        // front, so add it explicitly. Dedupe below covers static front pages.
        $pages['/'] = array(
            'path'  => '/',
            'title' => __( 'Homepage', 'content-performance-analyzer' ),
            'type'  => 'homepage',
        );
        $seen['/'] = true;
        foreach ( $query->posts as $post_id ) {
            if ( !is_post_publicly_viewable( $post_id ) ) {
                continue;
            }
            $permalink = get_permalink( $post_id );
            if ( !$permalink ) {
                continue;
            }
            $path = cpanalyzer_normalize_path( (string) $permalink );
            if ( '' === $path || isset( $seen[$path] ) ) {
                continue;
            }
            $seen[$path] = true;
            $title = get_the_title( $post_id );
            $pages[$path] = array(
                'path'  => $path,
                'title' => ( '' !== $title ? $title : __( '(no title)', 'content-performance-analyzer' ) ),
                'type'  => get_post_type( $post_id ),
            );
        }
        wp_send_json_success( array(
            'pages' => array_values( $pages ),
            'total' => count( $pages ),
        ) );
    }

    /**
     * AJAX: import the pages selected in the site-page picker, reusing the
     * same validation and storage path as the manual URL upload form.
     */
    public function ajax_import_site_pages() {
        check_ajax_referer( 'cpanalyzer_site_pages', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'You do not have permission to do this.', 'content-performance-analyzer' ),
            ), 403 );
        }
        $raw_paths = ( isset( $_POST['paths'] ) ? (array) wp_unslash( $_POST['paths'] ) : array() );
        $paths = array();
        foreach ( $raw_paths as $raw_path ) {
            if ( !is_string( $raw_path ) ) {
                continue;
            }
            $path = sanitize_text_field( $raw_path );
            if ( '' === $path ) {
                continue;
            }
            // Normalize now so '/pricing' and '/pricing/' dedupe here instead
            // of tripping the table's unique key during insert.
            $path = cpanalyzer_normalize_path( $path );
            if ( '' !== $path && !isset( $paths[$path] ) ) {
                $paths[$path] = $path;
            }
            if ( count( $paths ) >= 5000 ) {
                break;
            }
        }
        $paths = array_values( $paths );
        if ( empty( $paths ) ) {
            wp_send_json_error( array(
                'message' => __( 'Select at least one page to import.', 'content-performance-analyzer' ),
            ) );
        }
        $mode_raw = ( isset( $_POST['mode'] ) ? sanitize_text_field( wp_unslash( $_POST['mode'] ) ) : 'replace' );
        $mode = ( 'merge' === $mode_raw ? 'merge' : 'replace' );
        $result = $this->process_uploaded_urls( $paths, $mode );
        if ( empty( $result['success'] ) ) {
            wp_send_json_error( array(
                'message' => $result['message'],
            ) );
        }
        wp_send_json_success( array(
            'message' => sprintf(
                /* translators: 1: total pages processed; 2: pages added; 3: pages skipped */
                __( 'Imported %1$d pages: %2$d added, %3$d skipped.', 'content-performance-analyzer' ),
                $result['total'],
                $result['added'],
                $result['skipped']
            ),
        ) );
    }

    /**
     * AJAX: generate alt text with AI for the images the scraper found
     * missing it, then apply the text to the Media Library and — when the
     * page is a local post the user can edit — to the page content itself.
     *
     * Only operates on the scraped list; never invents images.
     */
    public function ajax_fix_image_alt() {
        check_ajax_referer( 'cpanalyzer_fix_alt', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'You do not have permission to do this.', 'content-performance-analyzer' ),
            ), 403 );
        }
        $path = ( isset( $_POST['url'] ) ? cpanalyzer_normalize_path( sanitize_text_field( wp_unslash( $_POST['url'] ) ) ) : '' );
        if ( '' === $path ) {
            wp_send_json_error( array(
                'message' => __( 'Missing page URL.', 'content-performance-analyzer' ),
            ) );
        }
        $content_data = (array) get_option( 'cpanalyzer_content_data', array() );
        $images = array();
        if ( !empty( $content_data[$path]['images_missing_alt_list'] ) && is_array( $content_data[$path]['images_missing_alt_list'] ) ) {
            $images = array_values( array_filter( array_map( 'esc_url_raw', $content_data[$path]['images_missing_alt_list'] ) ) );
        }
        if ( empty( $images ) ) {
            wp_send_json_error( array(
                'message' => __( 'No image list stored for this page yet. Rescan the page first.', 'content-performance-analyzer' ),
            ) );
        }
        // Resolve the page to a local post so its content can be patched too.
        $post_id = 0;
        if ( '/' === $path ) {
            if ( 'page' === get_option( 'show_on_front' ) ) {
                $post_id = (int) get_option( 'page_on_front' );
            }
        } else {
            $post_id = (int) url_to_postid( home_url( $path ) );
        }
        $post = ( $post_id > 0 ? get_post( $post_id ) : null );
        $can_edit_post = $post && current_user_can( 'edit_post', $post_id );
        $post_content = ( $can_edit_post ? (string) $post->post_content : '' );
        $content_changed = false;
        $ai = new CPAP_AI_Processor();
        $page_title = ( $post ? get_the_title( $post ) : '' );
        $results = array();
        $remaining = array();
        $fixed = 0;
        // The page's primary keyword (from the stored AI action) gives the
        // generator topical context beyond the page title.
        $primary_keyword = '';
        foreach ( (array) get_option( 'cpanalyzer_page_actions', array() ) as $cpanalyzer_action_row ) {
            if ( !empty( $cpanalyzer_action_row['url'] ) && $cpanalyzer_action_row['url'] === $path ) {
                $primary_keyword = sanitize_text_field( (string) ($cpanalyzer_action_row['primary_keyword'] ?? '') );
                break;
            }
        }
        foreach ( array_slice( $images, 0, 20 ) as $src ) {
            $attachment_id = $this->cpanalyzer_find_attachment_id( $src );
            if ( 0 === $attachment_id ) {
                $results[] = array(
                    'image'  => $src,
                    'status' => 'skipped',
                    'note'   => __( 'Not in the Media Library (external or theme image) — add alt text where the image is defined.', 'content-performance-analyzer' ),
                );
                $remaining[] = $src;
                continue;
            }
            // Reuse existing library alt text before asking the AI.
            $alt = trim( (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) );
            if ( '' === $alt ) {
                $generated = $ai->generate_image_alt_text( $src, array(
                    'page_title' => $page_title,
                    'keyword'    => $primary_keyword,
                    'path'       => $path,
                ) );
                if ( is_wp_error( $generated ) ) {
                    $results[] = array(
                        'image'  => $src,
                        'status' => 'skipped',
                        'note'   => $generated->get_error_message(),
                    );
                    $remaining[] = $src;
                    continue;
                }
                $alt = $generated;
                update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt );
            }
            $note = __( 'Alt text saved to the Media Library.', 'content-performance-analyzer' );
            if ( '' !== $post_content ) {
                $patched = $this->cpanalyzer_apply_alt_to_content( $post_content, $src, $alt );
                if ( null !== $patched ) {
                    $post_content = $patched;
                    $content_changed = true;
                    $note = __( 'Alt text saved to the Media Library and applied in the page content.', 'content-performance-analyzer' );
                }
            }
            $fixed++;
            $results[] = array(
                'image'  => $src,
                'status' => 'fixed',
                'alt'    => $alt,
                'note'   => $note,
            );
        }
        if ( $content_changed ) {
            wp_update_post( array(
                'ID'           => $post_id,
                'post_content' => wp_slash( $post_content ),
            ) );
        }
        // Keep the stored scan state in sync so the modal reflects reality
        // without waiting for the next full rescan.
        $content_data[$path]['images_missing_alt_list'] = $remaining;
        $content_data[$path]['images_without_alt'] = count( $remaining );
        update_option( 'cpanalyzer_content_data', $content_data );
        wp_send_json_success( array(
            'results'   => $results,
            'remaining' => $remaining,
            'summary'   => sprintf( 
                /* translators: 1: images fixed; 2: images skipped */
                __( '%1$d fixed, %2$d skipped.', 'content-performance-analyzer' ),
                $fixed,
                count( $results ) - $fixed
             ),
        ) );
    }

    /**
     * Register the dashboard widget with the top priority pages.
     */
    public function register_dashboard_widget() {
        if ( !current_user_can( 'manage_options' ) || !function_exists( 'wp_add_dashboard_widget' ) ) {
            return;
        }
        wp_add_dashboard_widget( 'cpanalyzer_priority_pages', __( 'Content Performance — Priority Pages', 'content-performance-analyzer' ), array($this, 'render_dashboard_widget') );
    }

    /**
     * Render the dashboard widget: the pages most worth fixing this week,
     * ranked by impact, plus a one-line recovery summary. Render-only —
     * everything shown is read from the options the scans already write.
     */
    public function render_dashboard_widget() {
        $page_flags = (array) get_option( 'cpanalyzer_page_flags', array() );
        $gsc_rows = (array) get_option( 'cpanalyzer_gsc_rows', array() );
        $ga_rows = (array) get_option( 'cpanalyzer_ga_rows', array() );
        $insights_url = admin_url( 'admin.php?page=content-insights' );
        if ( empty( $page_flags ) ) {
            echo '<p>' . esc_html__( 'No pages analyzed yet. Import your pages and run the first scan to see which content needs attention.', 'content-performance-analyzer' ) . '</p>';
            echo '<p><a class="button button-primary" href="' . esc_url( admin_url( 'admin.php?page=cpanalyzer-url-management' ) ) . '">' . esc_html__( 'Run your first scan', 'content-performance-analyzer' ) . '</a></p>';
            return;
        }
        // Rank: decay first (by clicks lost), then SEO opportunities (by
        // impressions at stake), then conversion gaps (by traffic).
        $ranked = array();
        foreach ( $page_flags as $url => $flag ) {
            if ( !is_string( $flag ) || in_array( $flag, array('', 'none'), true ) ) {
                continue;
            }
            $gsc = ( isset( $gsc_rows[$url] ) && is_array( $gsc_rows[$url] ) ? $gsc_rows[$url] : array() );
            $ga = ( isset( $ga_rows[$url] ) && is_array( $ga_rows[$url] ) ? $ga_rows[$url] : array() );
            $clicks = (float) ($gsc['clicks'] ?? 0);
            $clicks_prev = (float) ($gsc['clicks_prev'] ?? 0);
            $impressions = (float) ($gsc['impressions'] ?? 0);
            $views = (float) ($ga['views'] ?? 0);
            if ( 'decay' === $flag ) {
                $group = 0;
                $score = max( 0, $clicks_prev - $clicks );
                $stat = sprintf( 
                    /* translators: 1: previous clicks, 2: current clicks */
                    __( 'clicks %1$s → %2$s', 'content-performance-analyzer' ),
                    number_format_i18n( $clicks_prev ),
                    number_format_i18n( $clicks )
                 );
            } elseif ( 'seo' === $flag ) {
                $group = 1;
                $score = $impressions;
                $stat = sprintf( 
                    /* translators: %s: impression count */
                    __( '%s impressions, weak clicks', 'content-performance-analyzer' ),
                    number_format_i18n( $impressions )
                 );
            } elseif ( 'conversion' === $flag ) {
                $group = 2;
                $score = $views;
                $stat = sprintf( 
                    /* translators: %s: view count */
                    __( '%s views, few conversions', 'content-performance-analyzer' ),
                    number_format_i18n( $views )
                 );
            } else {
                $group = 3;
                $score = $views;
                $stat = '';
            }
            $ranked[] = array(
                'url'   => (string) $url,
                'flag'  => $flag,
                'group' => $group,
                'score' => $score,
                'stat'  => $stat,
            );
        }
        if ( empty( $ranked ) ) {
            echo '<p>' . esc_html__( 'No priority issues right now — every analyzed page looks healthy.', 'content-performance-analyzer' ) . '</p>';
            echo '<p><a href="' . esc_url( $insights_url ) . '">' . esc_html__( 'Open Content Insights', 'content-performance-analyzer' ) . '</a></p>';
            return;
        }
        usort( $ranked, static function ( $a, $b ) {
            if ( $a['group'] !== $b['group'] ) {
                return $a['group'] <=> $b['group'];
            }
            return $b['score'] <=> $a['score'];
        } );
        $flag_labels = array(
            'decay'       => __( 'Decay', 'content-performance-analyzer' ),
            'seo'         => __( 'SEO opportunity', 'content-performance-analyzer' ),
            'conversion'  => __( 'Conversion gap', 'content-performance-analyzer' ),
            'performance' => __( 'Performance', 'content-performance-analyzer' ),
        );
        $flag_colors = array(
            'decay'       => '#d63638',
            'seo'         => '#dba617',
            'conversion'  => '#2271b1',
            'performance' => '#646970',
        );
        echo '<ul style="margin:0;">';
        foreach ( array_slice( $ranked, 0, 4 ) as $item ) {
            $color = $flag_colors[$item['flag']] ?? '#646970';
            $label = $flag_labels[$item['flag']] ?? $item['flag'];
            echo '<li style="margin-bottom:8px;">';
            echo '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' . esc_attr( $color ) . ';margin-right:6px;" aria-hidden="true"></span>';
            echo '<strong>' . esc_html( $label ) . '</strong> — ';
            echo '<a href="' . esc_url( $insights_url ) . '">' . esc_html( $item['url'] ) . '</a>';
            if ( '' !== $item['stat'] ) {
                echo '<br><small style="margin-left:14px;color:#646970;">' . esc_html( $item['stat'] ) . '</small>';
            }
            echo '</li>';
        }
        echo '</ul>';
        // One-line recovery summary for pages already marked fixed.
        $refreshed_map = (array) get_option( 'cpanalyzer_refreshed_map', array() );
        if ( !empty( $refreshed_map ) && function_exists( 'cpanalyzer_recovery_status' ) ) {
            $counts = array();
            foreach ( $refreshed_map as $url => $entry ) {
                if ( !is_array( $entry ) ) {
                    continue;
                }
                $status = ( function_exists( 'cpanalyzer_recovery_status_for' ) ? cpanalyzer_recovery_status_for(
                    (string) $url,
                    $entry,
                    $gsc_rows,
                    $ga_rows
                ) : cpanalyzer_recovery_status( $entry, null, null ) );
                if ( !empty( $status['state'] ) && 'none' !== $status['state'] ) {
                    $counts[$status['state']] = ($counts[$status['state']] ?? 0) + 1;
                }
            }
            if ( !empty( $counts ) ) {
                $parts = array();
                $state_labels = array(
                    'recovered'   => __( 'recovered', 'content-performance-analyzer' ),
                    'improving'   => __( 'improving', 'content-performance-analyzer' ),
                    'watching'    => __( 'watching', 'content-performance-analyzer' ),
                    'no_lift_yet' => __( 'no lift yet', 'content-performance-analyzer' ),
                    'no_lift'     => __( 'no lift', 'content-performance-analyzer' ),
                    'unmeasured'  => __( 'waiting on data', 'content-performance-analyzer' ),
                );
                foreach ( $state_labels as $state => $label ) {
                    if ( !empty( $counts[$state] ) ) {
                        $parts[] = number_format_i18n( $counts[$state] ) . ' ' . $label;
                    }
                }
                echo '<p style="border-top:1px solid #dcdcde;padding-top:8px;margin-bottom:0;">';
                echo '<strong>' . esc_html__( 'Fixes you applied:', 'content-performance-analyzer' ) . '</strong> ' . esc_html( implode( ' · ', $parts ) );
                echo '</p>';
            }
        }
        echo '<p style="margin-bottom:0;"><a href="' . esc_url( $insights_url ) . '">' . esc_html__( 'Open Content Insights →', 'content-performance-analyzer' ) . '</a></p>';
    }

    /**
     * Record that the user says a page has been fixed. Captures the
     * baseline (current clicks/views) and the pre-decay peak from the
     * 6-bucket click trend so the recovery verdict has something honest to
     * compare against later. Server-side values only — nothing numeric is
     * trusted from the request.
     *
     * @param string $url URL key as used in the options arrays.
     * @return array|WP_Error The stored entry, or an error for unknown pages.
     */
    public function record_page_fixed( $url ) {
        $url = (string) $url;
        $page_flags = (array) get_option( 'cpanalyzer_page_flags', array() );
        $gsc_rows = (array) get_option( 'cpanalyzer_gsc_rows', array() );
        $ga_rows = (array) get_option( 'cpanalyzer_ga_rows', array() );
        $content_data = (array) get_option( 'cpanalyzer_content_data', array() );
        if ( '' === $url || !array_key_exists( $url, $page_flags ) && !array_key_exists( $url, $gsc_rows ) && !array_key_exists( $url, $ga_rows ) && !array_key_exists( $url, $content_data ) ) {
            return new WP_Error('unknown_page', __( 'This page is not in the analyzed set.', 'content-performance-analyzer' ));
        }
        $gsc = ( isset( $gsc_rows[$url] ) && is_array( $gsc_rows[$url] ) ? $gsc_rows[$url] : array() );
        $ga = ( isset( $ga_rows[$url] ) && is_array( $ga_rows[$url] ) ? $ga_rows[$url] : array() );
        $peak = 0;
        if ( !empty( $gsc['trend'] ) && is_array( $gsc['trend'] ) ) {
            foreach ( $gsc['trend'] as $bucket_clicks ) {
                if ( is_numeric( $bucket_clicks ) ) {
                    $peak = max( $peak, (int) $bucket_clicks );
                }
            }
        }
        $entry = array(
            'marked_at'   => time(),
            'marked_week' => ( class_exists( 'CPAP_Snapshots' ) ? CPAP_Snapshots::current_week_start() : gmdate( 'Y-m-d' ) ),
            'baseline'    => array(
                'clicks' => (int) ($gsc['clicks'] ?? 0),
                'views'  => (int) ($ga['views'] ?? 0),
            ),
            'peak_clicks' => $peak,
        );
        $map = (array) get_option( 'cpanalyzer_refreshed_map', array() );
        $map[$url] = $entry;
        update_option( 'cpanalyzer_refreshed_map', $map, false );
        return $entry;
    }

    /**
     * Remove a page's fixed mark (undo).
     *
     * @param string $url URL key.
     * @return bool Whether an entry was removed.
     */
    public function remove_page_fixed( $url ) {
        $map = (array) get_option( 'cpanalyzer_refreshed_map', array() );
        if ( !array_key_exists( $url, $map ) ) {
            return false;
        }
        unset($map[$url]);
        update_option( 'cpanalyzer_refreshed_map', $map, false );
        return true;
    }

    /**
     * Once per UTC day, recompute the recovery verdict for every marked
     * page and record state transitions worth announcing. Recovered,
     * improving, and the final no-lift verdict become admin-notice news;
     * interim states change silently. The first recovered verdict ever
     * also arms the one-time review ask.
     */
    public function maybe_check_recovery_verdicts() {
        $map = get_option( 'cpanalyzer_refreshed_map', array() );
        if ( empty( $map ) || !is_array( $map ) ) {
            return;
        }
        if ( get_option( 'cpanalyzer_verdict_check_day' ) === gmdate( 'Y-m-d' ) ) {
            return;
        }
        update_option( 'cpanalyzer_verdict_check_day', gmdate( 'Y-m-d' ), false );
        if ( !function_exists( 'cpanalyzer_recovery_status_for' ) ) {
            return;
        }
        $gsc_rows = (array) get_option( 'cpanalyzer_gsc_rows', array() );
        $ga_rows = (array) get_option( 'cpanalyzer_ga_rows', array() );
        $news = (array) get_option( 'cpanalyzer_recovery_news', array() );
        $announce = array('recovered', 'improving', 'no_lift');
        $changed = false;
        foreach ( $map as $url => $entry ) {
            if ( !is_array( $entry ) ) {
                continue;
            }
            $status = cpanalyzer_recovery_status_for(
                (string) $url,
                $entry,
                $gsc_rows,
                $ga_rows
            );
            $state = (string) ($status['state'] ?? '');
            $last = (string) ($entry['last_state'] ?? 'watching');
            if ( '' === $state || 'none' === $state || $state === $last ) {
                continue;
            }
            $map[$url]['last_state'] = $state;
            $changed = true;
            if ( in_array( $state, $announce, true ) ) {
                $news[] = array(
                    'url'   => (string) $url,
                    'state' => $state,
                    'label' => (string) ($status['label'] ?? ''),
                    'time'  => time(),
                );
                if ( 'recovered' === $state && !get_option( 'cpanalyzer_review_ask_state' ) ) {
                    update_option( 'cpanalyzer_review_ask_state', array(
                        'state' => 'armed',
                        'time'  => time(),
                    ), false );
                }
            }
        }
        if ( $changed ) {
            update_option( 'cpanalyzer_refreshed_map', $map, false );
            update_option( 'cpanalyzer_recovery_news', array_slice( $news, -10 ), false );
        }
    }

    /**
     * Admin notice announcing verdict transitions — and, on the first
     * recovery ever, the one-time review ask. Mirrors the new-flags
     * notice: shown everywhere except the Insights page, which displays
     * the detail inline and clears the news.
     */
    public function render_recovery_notice() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        $this->maybe_check_recovery_verdicts();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page check for notice display.
        $current_page = ( isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '' );
        if ( 'content-insights' === $current_page ) {
            return;
        }
        $news = get_option( 'cpanalyzer_recovery_news', array() );
        if ( empty( $news ) || !is_array( $news ) ) {
            return;
        }
        $state_labels = array(
            'recovered' => __( 'Recovered', 'content-performance-analyzer' ),
            'improving' => __( 'Improving', 'content-performance-analyzer' ),
            'no_lift'   => __( 'No lift after 60 days', 'content-performance-analyzer' ),
        );
        $items = array();
        $has_recovered = false;
        foreach ( array_slice( array_reverse( $news ), 0, 5 ) as $item ) {
            if ( !is_array( $item ) || empty( $item['url'] ) || empty( $state_labels[$item['state'] ?? ''] ) ) {
                continue;
            }
            if ( 'recovered' === $item['state'] ) {
                $has_recovered = true;
            }
            $items[] = '<strong>' . esc_html( $state_labels[$item['state']] ) . '</strong> — ' . esc_html( $item['url'] );
        }
        if ( empty( $items ) ) {
            return;
        }
        $review_cta = '';
        $review_state = get_option( 'cpanalyzer_review_ask_state', array() );
        if ( $has_recovered && is_array( $review_state ) && 'armed' === ($review_state['state'] ?? '') ) {
            $go_url = wp_nonce_url( add_query_arg( 'cpanalyzer_review_ask', 'go' ), 'cpanalyzer_review_ask' );
            $dismiss_url = wp_nonce_url( add_query_arg( 'cpanalyzer_review_ask', 'dismiss' ), 'cpanalyzer_review_ask' );
            $review_cta = '<p>' . esc_html__( 'Good to see a fix pay off. If the plugin earned it, a quick review helps other site owners find it — this is the only time it will ask.', 'content-performance-analyzer' ) . ' <a href="' . esc_url( $go_url ) . '">' . esc_html__( 'Leave a review', 'content-performance-analyzer' ) . '</a>' . ' · <a href="' . esc_url( $dismiss_url ) . '">' . esc_html__( 'No thanks', 'content-performance-analyzer' ) . '</a></p>';
        }
        printf(
            '<div class="notice notice-success is-dismissible"><p><strong>%s</strong> %s</p><p>%s</p>%s<p><a href="%s">%s</a></p></div>',
            esc_html__( 'Content Performance Analyzer:', 'content-performance-analyzer' ),
            esc_html__( 'verdicts are in for pages you marked as fixed.', 'content-performance-analyzer' ),
            wp_kses_post( implode( '<br>', $items ) ),
            wp_kses_post( $review_cta ),
            esc_url( admin_url( 'admin.php?page=content-insights' ) ),
            esc_html__( 'See the details on Content Insights', 'content-performance-analyzer' )
        );
    }

    /**
     * The early review ask: fires once the audit has visibly done its job —
     * pages analyzed, problems flagged, AI recommendations written — instead
     * of waiting the 28+ days a recovery verdict takes. Feeds the SAME
     * one-time state machine as the recovery ask, so whichever moment comes
     * first asks, and the plugin never asks twice.
     */
    public function render_first_value_review_ask() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        // Plugin screens only — this never follows the user around wp-admin.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page check for notice display.
        $current_page = ( isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '' );
        if ( '' === $current_page || 0 !== strpos( $current_page, 'content-insights' ) && 0 !== strpos( $current_page, 'cpanalyzer' ) ) {
            return;
        }
        // The recovery notice carries its own copy of this ask — never both.
        $news = get_option( 'cpanalyzer_recovery_news', array() );
        if ( !empty( $news ) ) {
            return;
        }
        $state = get_option( 'cpanalyzer_review_ask_state', array() );
        if ( !empty( $state ) && 'armed' !== ($state['state'] ?? '') ) {
            return;
            // Already asked, one way or the other.
        }
        if ( empty( $state ) ) {
            // Not armed yet — arm only at a genuine value moment.
            $installed = strtotime( (string) get_option( 'cpanalyzer_install_date', '' ) );
            if ( !$installed || time() - $installed < 3 * DAY_IN_SECONDS ) {
                return;
            }
            $flags = get_option( 'cpanalyzer_page_flags', array() );
            $actions = get_option( 'cpanalyzer_page_actions', array() );
            if ( !is_array( $flags ) || count( $flags ) < 5 || !is_array( $actions ) || empty( $actions ) ) {
                return;
            }
            update_option( 'cpanalyzer_review_ask_state', array(
                'state'  => 'armed',
                'time'   => time(),
                'source' => 'first_value',
            ), false );
        }
        $go_url = wp_nonce_url( add_query_arg( 'cpanalyzer_review_ask', 'go' ), 'cpanalyzer_review_ask' );
        $dismiss_url = wp_nonce_url( add_query_arg( 'cpanalyzer_review_ask', 'dismiss' ), 'cpanalyzer_review_ask' );
        printf(
            '<div class="notice notice-info"><p><strong>%s</strong> %s</p><p><a class="button button-primary" href="%s">%s</a> <a href="%s" style="margin-left:8px;">%s</a></p></div>',
            esc_html__( 'Content Performance Analyzer:', 'content-performance-analyzer' ),
            esc_html__( 'your audit has flagged real pages and written fix plans for them. If the plugin is proving useful, a quick review helps other site owners find it — this is the only time it will ask.', 'content-performance-analyzer' ),
            esc_url( $go_url ),
            esc_html__( 'Leave a review', 'content-performance-analyzer' ),
            esc_url( $dismiss_url ),
            esc_html__( 'No thanks', 'content-performance-analyzer' )
        );
    }

    /**
     * Persist the outcome of the one-time review ask.
     *
     * @param string $outcome 'done' or 'dismissed'.
     */
    public function mark_review_ask( $outcome ) {
        update_option( 'cpanalyzer_review_ask_state', array(
            'state' => ( 'done' === $outcome ? 'done' : 'dismissed' ),
            'time'  => time(),
        ), false );
    }

    /**
     * Handle the review-ask links (admin_init): record the outcome, then
     * send the user to the wordpress.org review form or back to the page
     * they were on. Either way the ask never renders again.
     */
    public function handle_review_ask_action() {
        if ( empty( $_GET['cpanalyzer_review_ask'] ) || !current_user_can( 'manage_options' ) ) {
            return;
        }
        check_admin_referer( 'cpanalyzer_review_ask' );
        $action = sanitize_key( wp_unslash( $_GET['cpanalyzer_review_ask'] ) );
        if ( 'go' === $action ) {
            $this->mark_review_ask( 'done' );
            wp_redirect( 'https://wordpress.org/support/plugin/content-performance-analyzer/reviews/#new-post' );
            // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Fixed wordpress.org URL.
            exit;
        }
        $this->mark_review_ask( 'dismissed' );
        wp_safe_redirect( remove_query_arg( array('cpanalyzer_review_ask', '_wpnonce') ) );
        exit;
    }

    /**
     * AJAX: mark a page as fixed, or undo the mark.
     */
    public function ajax_mark_page_fixed() {
        check_ajax_referer( 'cpanalyzer_mark_fixed', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Permission denied.', 'content-performance-analyzer' ),
            ), 403 );
        }
        $url = ( isset( $_POST['url'] ) ? sanitize_text_field( wp_unslash( $_POST['url'] ) ) : '' );
        $mode = ( isset( $_POST['mode'] ) && 'unmark' === $_POST['mode'] ? 'unmark' : 'mark' );
        if ( 'unmark' === $mode ) {
            $this->remove_page_fixed( $url );
            wp_send_json_success( array(
                'state' => 'none',
            ) );
        }
        $entry = $this->record_page_fixed( $url );
        if ( is_wp_error( $entry ) ) {
            wp_send_json_error( array(
                'message' => $entry->get_error_message(),
            ) );
        }
        wp_send_json_success( array(
            'state' => 'watching',
        ) );
    }

    /**
     * Create a draft copy of a post so the refresh can be written without
     * touching the live page: same title, content, excerpt, terms, and
     * featured image, prefixed with the stored fix plan as an editorial
     * note. The original post is never modified beyond a meta pointer used
     * to reuse the draft on repeat clicks.
     *
     * @param int    $post_id Resolved post ID.
     * @param string $url     URL key, for pulling the stored fix plan.
     * @return array|WP_Error {draft_id, edit_link, reused} or error.
     */
    public function create_refresh_draft_for_post( $post_id, $url ) {
        $post_id = (int) $post_id;
        $post = ( $post_id > 0 ? get_post( $post_id ) : null );
        if ( !$post || !current_user_can( 'edit_post', $post_id ) ) {
            return new WP_Error('not_editable', __( 'This page could not be resolved to an editable post.', 'content-performance-analyzer' ));
        }
        // Reuse an existing refresh draft instead of stacking duplicates.
        $existing_id = (int) get_post_meta( $post_id, '_cpanalyzer_refresh_draft', true );
        if ( $existing_id > 0 ) {
            $existing = get_post( $existing_id );
            if ( $existing && 'draft' === $existing->post_status ) {
                return array(
                    'draft_id'  => $existing_id,
                    'edit_link' => admin_url( 'post.php?post=' . $existing_id . '&action=edit' ),
                    'reused'    => true,
                );
            }
        }
        // Build the editorial fix-plan note from everything the plugin
        // already knows about this page.
        $page_actions = (array) get_option( 'cpanalyzer_page_actions', array() );
        $action = ( isset( $page_actions[$url] ) && is_array( $page_actions[$url] ) ? $page_actions[$url] : array() );
        $gsc_rows = (array) get_option( 'cpanalyzer_gsc_rows', array() );
        $diagnosis = ( isset( $gsc_rows[$url]['decay_diagnosis']['label'] ) ? (string) $gsc_rows[$url]['decay_diagnosis']['label'] : '' );
        $plan_items = array();
        if ( '' !== $diagnosis ) {
            /* translators: %s: decay diagnosis label */
            $plan_items[] = sprintf( __( 'Diagnosis: %s', 'content-performance-analyzer' ), $diagnosis );
        }
        foreach ( array(
            'issue_summary',
            'seo_recommendation',
            'aeo_recommendation',
            'engagement_recommendation',
            'content_refresh_brief'
        ) as $key ) {
            if ( !empty( $action[$key] ) && is_string( $action[$key] ) ) {
                $plan_items[] = wp_strip_all_tags( $action[$key] );
            }
        }
        $plan_html = '';
        if ( !empty( $plan_items ) ) {
            $plan_html = '<p><strong>' . esc_html__( 'Refresh plan from Content Performance Analyzer — delete this block before publishing.', 'content-performance-analyzer' ) . '</strong></p>';
            $plan_html .= '<ul>';
            foreach ( $plan_items as $item ) {
                $plan_html .= '<li>' . esc_html( $item ) . '</li>';
            }
            $plan_html .= '</ul>';
        }
        $content = (string) $post->post_content;
        if ( '' !== $plan_html ) {
            if ( function_exists( 'has_blocks' ) && has_blocks( $content ) ) {
                $content = "<!-- wp:html -->\n" . $plan_html . "\n<!-- /wp:html -->\n\n" . $content;
            } else {
                $content = $plan_html . "\n\n" . $content;
            }
        }
        $draft_id = wp_insert_post( array(
            'post_title'   => $post->post_title,
            'post_content' => wp_slash( $content ),
            'post_excerpt' => wp_slash( (string) $post->post_excerpt ),
            'post_status'  => 'draft',
            'post_type'    => $post->post_type,
            'post_author'  => ( get_current_user_id() ? get_current_user_id() : (int) $post->post_author ),
        ), true );
        if ( is_wp_error( $draft_id ) ) {
            return $draft_id;
        }
        // Carry over the organizational bits a writer expects.
        foreach ( get_object_taxonomies( $post->post_type ) as $taxonomy ) {
            $terms = wp_get_object_terms( $post_id, $taxonomy, array(
                'fields' => 'ids',
            ) );
            if ( !is_wp_error( $terms ) && !empty( $terms ) ) {
                wp_set_object_terms( $draft_id, $terms, $taxonomy );
            }
        }
        $thumbnail_id = get_post_thumbnail_id( $post_id );
        if ( $thumbnail_id ) {
            set_post_thumbnail( $draft_id, $thumbnail_id );
        }
        update_post_meta( $draft_id, '_cpanalyzer_refresh_of', $post_id );
        update_post_meta( $post_id, '_cpanalyzer_refresh_draft', $draft_id );
        return array(
            'draft_id'  => (int) $draft_id,
            'edit_link' => admin_url( 'post.php?post=' . (int) $draft_id . '&action=edit' ),
            'reused'    => false,
        );
    }

    /**
     * AJAX: one-click "Create refresh draft" from the insights table.
     */
    public function ajax_create_refresh_draft() {
        check_ajax_referer( 'cpanalyzer_refresh_draft', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Permission denied.', 'content-performance-analyzer' ),
            ), 403 );
        }
        $path = ( isset( $_POST['url'] ) ? sanitize_text_field( wp_unslash( $_POST['url'] ) ) : '' );
        if ( '' === $path ) {
            wp_send_json_error( array(
                'message' => __( 'Missing page URL.', 'content-performance-analyzer' ),
            ) );
        }
        $post_id = 0;
        if ( '/' === $path ) {
            if ( 'page' === get_option( 'show_on_front' ) ) {
                $post_id = (int) get_option( 'page_on_front' );
            }
        } else {
            $post_id = (int) url_to_postid( home_url( $path ) );
        }
        $result = $this->create_refresh_draft_for_post( $post_id, $path );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array(
                'message' => $result->get_error_message(),
            ) );
        }
        $result['message'] = ( $result['reused'] ? __( 'This page already has a refresh draft — opening it.', 'content-performance-analyzer' ) : __( 'Refresh draft created.', 'content-performance-analyzer' ) );
        wp_send_json_success( $result );
    }

    /**
     * AJAX: write an AI answer for a detected question gap and append it to
     * the page as a Q&A block. The query must be one of the page's actual
     * detected gaps (server-side check), the answer is grounded in the
     * page's own content, and existing content is never modified — only a
     * new heading + paragraph is appended.
     */
    public function ajax_generate_answer_block() {
        check_ajax_referer( 'cpanalyzer_answer_block', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'You do not have permission to do this.', 'content-performance-analyzer' ),
            ), 403 );
        }
        $path = ( isset( $_POST['url'] ) ? cpanalyzer_normalize_path( sanitize_text_field( wp_unslash( $_POST['url'] ) ) ) : '' );
        $query = ( isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '' );
        if ( '' === $path || '' === $query ) {
            wp_send_json_error( array(
                'message' => __( 'Missing page or query.', 'content-performance-analyzer' ),
            ) );
        }
        // The query must be one of this page's real detected gaps — this is
        // not a free-form content generator.
        $gaps = cpanalyzer_get_question_gaps( $path );
        $is_gap = false;
        foreach ( $gaps as $gap ) {
            if ( strtolower( $gap['query'] ) === strtolower( $query ) ) {
                $is_gap = true;
                break;
            }
        }
        if ( !$is_gap ) {
            wp_send_json_error( array(
                'message' => __( 'This query is not a detected question gap for the page. Rescan and try again.', 'content-performance-analyzer' ),
            ) );
        }
        // Resolve the page to an editable local post.
        $post_id = 0;
        if ( '/' === $path ) {
            if ( 'page' === get_option( 'show_on_front' ) ) {
                $post_id = (int) get_option( 'page_on_front' );
            }
        } else {
            $post_id = (int) url_to_postid( home_url( $path ) );
        }
        $post = ( $post_id > 0 ? get_post( $post_id ) : null );
        if ( !$post || !current_user_can( 'edit_post', $post_id ) ) {
            wp_send_json_error( array(
                'message' => __( 'This page isn\'t an editable post, so the answer can\'t be inserted automatically.', 'content-performance-analyzer' ),
            ) );
        }
        $content_data = (array) get_option( 'cpanalyzer_content_data', array() );
        $ai = new CPAP_AI_Processor();
        $answer = $ai->generate_answer_block( $query, array(
            'page_title' => get_the_title( $post ),
            'body_text'  => (string) ($content_data[$path]['body_text'] ?? ''),
        ) );
        if ( is_wp_error( $answer ) ) {
            wp_send_json_error( array(
                'message' => $answer->get_error_message(),
            ) );
        }
        $question = ucfirst( $query ) . (( preg_match( '/[?.!]$/', $query ) ? '' : '?' ));
        // Append as a new Q&A block — block markup when the post uses the
        // block editor, plain HTML otherwise.
        if ( function_exists( 'has_blocks' ) && has_blocks( $post->post_content ) ) {
            $block = "\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">" . esc_html( $question ) . "</h3>\n<!-- /wp:heading -->\n\n<!-- wp:paragraph -->\n<p>" . esc_html( $answer ) . "</p>\n<!-- /wp:paragraph -->";
        } else {
            $block = "\n\n<h3>" . esc_html( $question ) . "</h3>\n<p>" . esc_html( $answer ) . "</p>";
        }
        $updated = wp_update_post( array(
            'ID'           => $post_id,
            'post_content' => wp_slash( $post->post_content . $block ),
        ), true );
        if ( is_wp_error( $updated ) ) {
            wp_send_json_error( array(
                'message' => $updated->get_error_message(),
            ) );
        }
        // Record the pair for FAQPage schema output. On first use, remember
        // whether the page already had FAQPage markup from another source so
        // we never emit a duplicate.
        $pairs = get_post_meta( $post_id, '_cpanalyzer_faq', true );
        $pairs = ( is_array( $pairs ) ? $pairs : array() );
        if ( empty( $pairs ) ) {
            $scan_types = array_map( 'strval', (array) ($content_data[$path]['schema_types'] ?? array()) );
            update_post_meta( $post_id, '_cpanalyzer_faq_external', ( in_array( 'FAQPage', $scan_types, true ) ? 1 : 0 ) );
        }
        $pairs[] = array(
            'question' => $question,
            'answer'   => $answer,
        );
        update_post_meta( $post_id, '_cpanalyzer_faq', $pairs );
        wp_send_json_success( array(
            'question' => $question,
            'answer'   => $answer,
            'message'  => __( 'Answer added to the end of the page. Review it in the editor and move it where it fits best.', 'content-performance-analyzer' ),
        ) );
    }

    /**
     * Map an image URL to a Media Library attachment ID, tolerating
     * intermediate-size suffixes (-300x198 etc.).
     *
     * @param  string $src Image URL.
     * @return int Attachment ID, or 0 when the image isn't local.
     */
    private function cpanalyzer_find_attachment_id( $src ) {
        $attachment_id = (int) attachment_url_to_postid( $src );
        if ( 0 === $attachment_id ) {
            $stripped = preg_replace( '/-\\d+x\\d+(\\.[a-z0-9]+)($|\\?)/i', '$1$2', $src );
            if ( is_string( $stripped ) && $stripped !== $src ) {
                $attachment_id = (int) attachment_url_to_postid( $stripped );
            }
        }
        return $attachment_id;
    }

    /**
     * Set alt text on <img> tags in post content that reference the given
     * image and currently have a missing or empty alt attribute. Matching is
     * by file name (size-suffix-insensitive); tags that already carry alt
     * text are never touched.
     *
     * @param  string $content Post content.
     * @param  string $src     Image URL as scraped from the front end.
     * @param  string $alt     Alt text to apply.
     * @return string|null Updated content, or null when nothing changed.
     */
    private function cpanalyzer_apply_alt_to_content( $content, $src, $alt ) {
        $file = wp_basename( (string) wp_parse_url( $src, PHP_URL_PATH ) );
        $file = preg_replace( '/-\\d+x\\d+(\\.[a-z0-9]+)$/i', '$1', $file );
        $needle = pathinfo( (string) $file, PATHINFO_FILENAME );
        if ( '' === $needle ) {
            return null;
        }
        $changed = false;
        $updated_content = preg_replace_callback( '/<img\\b[^>]*>/i', static function ( $matches ) use($needle, $alt, &$changed) {
            $tag = $matches[0];
            if ( false === stripos( $tag, $needle ) ) {
                return $tag;
            }
            if ( preg_match( '/\\balt\\s*=\\s*("|\')(.*?)\\1/is', $tag, $alt_match ) ) {
                if ( '' !== trim( $alt_match[2] ) ) {
                    return $tag;
                    // Existing alt text is never overwritten.
                }
                $new_tag = preg_replace(
                    '/\\balt\\s*=\\s*("|\')\\s*\\1/i',
                    'alt="' . esc_attr( $alt ) . '"',
                    $tag,
                    1
                );
            } else {
                $new_tag = preg_replace(
                    '/^<img\\b/i',
                    '<img alt="' . esc_attr( $alt ) . '"',
                    $tag,
                    1
                );
            }
            if ( is_string( $new_tag ) && $new_tag !== $tag ) {
                $changed = true;
                return $new_tag;
            }
            return $tag;
        }, $content );
        return ( $changed && is_string( $updated_content ) ? $updated_content : null );
    }

    /**
     * URL Management page
     */
    public function url_management_page() {
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // Get statistics
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $stats = array(
            'total'       => $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) ),
            'pending'     => $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'pending'" ),
            'in_progress' => $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'in_progress'" ),
            'success'     => $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'success'" ),
            'failed'      => $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'failed'" ),
        );
        // Pagination
        $per_page = 50;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $current_page = ( isset( $_GET['paged'] ) ? max( 1, intval( sanitize_text_field( wp_unslash( $_GET['paged'] ) ) ) ) : 1 );
        $offset = ($current_page - 1) * $per_page;
        // Get URLs with pagination
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $urls = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM " . esc_sql( $table_name ) . " \n             ORDER BY id DESC \n             LIMIT %d OFFSET %d", $per_page, $offset ) );
        $total_pages = ceil( $stats['total'] / $per_page );
        include CPANALYZER_PLUGIN_DIR . 'templates/url-management-page.php';
    }

    /**
     * AJAX: Rescan all URLs
     */
    public function ajax_rescan_all_urls() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Unauthorized', 'content-performance-analyzer' ) );
        }
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $total_urls = intval( $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) ) );
        if ( $total_urls < 1 ) {
            wp_send_json_error( array(
                'message' => __( 'No existing URLs were found to rescan.', 'content-performance-analyzer' ),
            ) );
        }
        $this->clear_rescan_runtime_state();
        // Reset all stored scan outputs while preserving the existing URL list and order.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $reset_result = $wpdb->query( "UPDATE " . esc_sql( $table_name ) . "\n             SET status = 'pending',\n                 last_scanned = NULL,\n                 scan_result = NULL,\n                 error_message = NULL,\n                 ga_data = NULL,\n                 gsc_data = NULL,\n                 content_data = NULL,\n                 ai_suggestions = NULL,\n                 retry_count = 0" );
        if ( $reset_result === false ) {
            wp_send_json_error( array(
                'message' => __( 'Failed to reset existing URLs for rescanning.', 'content-performance-analyzer' ),
            ) );
        }
        $this->clear_insights_display_cache();
        // Trigger batch processing manually (bypass auto-scan check)
        if ( !class_exists( 'CPAP_Batch_Processor' ) ) {
            require_once CPANALYZER_PLUGIN_DIR . 'includes/class-cpa-batch-processor.php';
        }
        $batch_processor = new CPAP_Batch_Processor();
        $pending_count = $batch_processor->start_processing_now();
        update_option( 'cpanalyzer_last_full_scan', current_time( 'mysql' ) );
        update_option( 'cpanalyzer_status_msg', sprintf( 
            /* translators: %d: number of URLs being rescanned */
            __( 'Fresh rescan started for %d existing URLs. Previous analysis data was cleared and the first batch is running now.', 'content-performance-analyzer' ),
            $total_urls
         ) );
        wp_send_json_success( array(
            'message' => sprintf( 
                /* translators: 1: total URLs reset for rescan; 2: pending URLs queued */
                __( 'Fresh rescan started for %1$d existing URLs. %2$d URLs are queued for processing.', 'content-performance-analyzer' ),
                $total_urls,
                intval( $pending_count )
             ),
        ) );
    }

    /**
     * Clear cached option-based insights data so a rescan starts from the table only.
     */
    private function clear_insights_display_cache() {
        update_option( 'cpanalyzer_page_flags', array(), false );
        update_option( 'cpanalyzer_ga_rows', array(), false );
        update_option( 'cpanalyzer_gsc_rows', array(), false );
        update_option( 'cpanalyzer_ga_prev_rows', array(), false );
        update_option( 'cpanalyzer_content_data', array(), false );
        update_option( 'cpanalyzer_page_actions', array(), false );
    }

    /**
     * Remove a single URL from the option-based Insights cache before targeted reprocessing.
     *
     * @param string $url Normalized uploaded URL path.
     */
    private function clear_insights_display_cache_for_url( $url ) {
        $flags = (array) get_option( 'cpanalyzer_page_flags', array() );
        unset($flags[$url]);
        update_option( 'cpanalyzer_page_flags', $flags, false );
        $ga_rows = (array) get_option( 'cpanalyzer_ga_rows', array() );
        unset($ga_rows[$url]);
        update_option( 'cpanalyzer_ga_rows', $ga_rows, false );
        $gsc_rows = (array) get_option( 'cpanalyzer_gsc_rows', array() );
        unset($gsc_rows[$url]);
        update_option( 'cpanalyzer_gsc_rows', $gsc_rows, false );
        $ga_prev_rows = (array) get_option( 'cpanalyzer_ga_prev_rows', array() );
        unset($ga_prev_rows[$url]);
        update_option( 'cpanalyzer_ga_prev_rows', $ga_prev_rows, false );
        $content_data = (array) get_option( 'cpanalyzer_content_data', array() );
        unset($content_data[$url]);
        update_option( 'cpanalyzer_content_data', $content_data, false );
        $page_actions = array_values( array_filter( (array) get_option( 'cpanalyzer_page_actions', array() ), static function ( $action ) use($url) {
            return empty( $action['url'] ) || $action['url'] !== $url;
        } ) );
        update_option( 'cpanalyzer_page_actions', $page_actions, false );
    }

    /**
     * Reset batch runtime state so a manual rescan cannot inherit stale locks.
     */
    private function clear_rescan_runtime_state() {
        delete_transient( 'cpanalyzer_batch_running' );
        delete_transient( 'cpanalyzer_batch_start_time' );
        delete_transient( 'cpanalyzer_pending_content_analysis' );
        delete_transient( 'cpanalyzer_pending_ai_payload' );
        wp_clear_scheduled_hook( 'cpanalyzer_batch_process' );
    }

    /**
     * Sanitize Search Console site URL
     */
    /**
     * Sanitize the custom conversion events list.
     *
     * Accepts a comma-separated list of GA4 event names (letters, digits,
     * underscores, max 40 chars each per GA4 naming rules).
     *
     * @param string $value Raw input.
     * @return string Normalized comma-separated list.
     */
    public function sanitize_conversion_events( $value ) {
        $events = array_filter( array_map( 'trim', explode( ',', (string) $value ) ) );
        $events = array_filter( $events, function ( $event ) {
            return (bool) preg_match( '/^[A-Za-z][A-Za-z0-9_]{0,39}$/', $event );
        } );
        return implode( ', ', array_unique( $events ) );
    }

    public function sanitize_site_url( $url ) {
        $url = trim( $url );
        // Handle sc-domain: format
        if ( strpos( $url, 'sc-domain:' ) === 0 ) {
            $domain = substr( $url, 10 );
            // Remove 'sc-domain:' prefix
            $domain = sanitize_text_field( $domain );
            // Remove protocol if accidentally included
            $domain = preg_replace( '/^https?:\\/\\//', '', $domain );
            return 'sc-domain:' . $domain;
        }
        // Handle regular URLs - ensure proper format
        $url = esc_url_raw( $url );
        // Ensure trailing slash for consistency with GSC requirements
        if ( !empty( $url ) && substr( $url, -1 ) !== '/' ) {
            $url .= '/';
        }
        // Validate URL format for GSC compatibility
        if ( !empty( $url ) && !filter_var( $url, FILTER_VALIDATE_URL ) ) {
            add_settings_error( 'cpanalyzer_settings', 'invalid_site_url', __( 'Please enter a valid URL format for Search Console (e.g., https://example.com/ or sc-domain:example.com)', 'content-performance-analyzer' ) );
        }
        return $url;
    }

    /**
     * Sanitize PSI strategy setting
     */
    public function sanitize_psi_strategy( $strategy ) {
        $valid_strategies = array('mobile', 'desktop', 'both');
        return ( in_array( $strategy, $valid_strategies ) ? $strategy : 'mobile' );
    }

    /**
     * Sanitize AI provider setting
     */
    /**
     * Sanitize the optional AI model override: model IDs are short slugs
     * like gpt-5.1 or openai/gpt-oss-120b.
     */
    public function sanitize_ai_model( $value ) {
        $value = trim( (string) $value );
        if ( '' === $value || !preg_match( '/^[A-Za-z0-9._\\/-]{1,100}$/', $value ) ) {
            return '';
        }
        return $value;
    }

    /**
     * Sanitize a yes/no checkbox setting.
     */
    public function sanitize_yes_no( $value ) {
        return ( 'yes' === $value || true === $value ? 'yes' : 'no' );
    }

    /**
     * Enable llms.txt only when nothing else already serves it — a physical
     * file in the site root or another plugin both take precedence.
     */
    public function sanitize_llms_txt_enable( $value ) {
        $value = $this->sanitize_yes_no( $value );
        if ( 'yes' !== $value ) {
            return 'no';
        }
        // Already on — no need to re-probe on every save.
        if ( 'yes' === get_option( 'cpanalyzer_llms_txt_enable', 'no' ) ) {
            return 'yes';
        }
        $exists = file_exists( ABSPATH . 'llms.txt' );
        if ( !$exists ) {
            // While our setting is off, any 200 from /llms.txt means someone
            // else (a plugin, a CDN rule) is already serving one.
            $response = wp_remote_get( home_url( '/llms.txt' ), array(
                'timeout'   => 8,
                'sslverify' => get_option( 'cpanalyzer_verify_ssl', 'yes' ) !== 'no',
            ) );
            $exists = !is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response );
        }
        if ( $exists ) {
            add_settings_error(
                'cpanalyzer_messages',
                'cpanalyzer_llms_exists',
                __( 'This site already has an llms.txt (a file or another plugin provides it), so the setting stays off to avoid publishing two conflicting versions.', 'content-performance-analyzer' ),
                'warning'
            );
            return 'no';
        }
        return 'yes';
    }

    /**
     * Sanitize the PageSpeed page selection: normalized unique paths, capped
     * at cpanalyzer_psi_page_limit() (15 free, 30 with Pro).
     */
    public function sanitize_psi_pages( $pages ) {
        $pages = ( is_array( $pages ) ? $pages : array() );
        $clean = array();
        $limit = cpanalyzer_psi_page_limit();
        foreach ( $pages as $page ) {
            if ( !is_string( $page ) || '' === trim( $page ) ) {
                continue;
            }
            $path = cpanalyzer_normalize_path( sanitize_text_field( $page ) );
            if ( '' !== $path && !isset( $clean[$path] ) ) {
                $clean[$path] = $path;
            }
            if ( count( $clean ) >= $limit ) {
                break;
            }
        }
        return array_values( $clean );
    }

    public function sanitize_ai_provider( $provider ) {
        $provider = ( is_string( $provider ) ? strtolower( $provider ) : 'openai' );
        $allowed = array('openai', 'groq', 'gemini');
        return ( in_array( $provider, $allowed, true ) ? $provider : 'openai' );
    }

    /**
     * Sanitize the AI API key and warn when its prefix doesn't match the
     * selected provider (e.g. a Google AIza… key with provider set to
     * OpenAI) — that mistake otherwise only surfaces as a failed batch.
     */
    public function sanitize_ai_api_key( $key ) {
        $key = sanitize_text_field( (string) $key );
        if ( '' === $key ) {
            return $key;
        }
        // Provider value from this same submission (falls back to saved).
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- register_setting sanitize callback; the Settings API has already verified the nonce.
        $provider_raw = ( isset( $_POST['cpanalyzer_ai_provider'] ) ? sanitize_text_field( wp_unslash( $_POST['cpanalyzer_ai_provider'] ) ) : get_option( 'cpanalyzer_ai_provider', 'openai' ) );
        $provider = $this->sanitize_ai_provider( $provider_raw );
        $expected = array(
            'openai' => array(
                'prefix' => 'sk-',
                'label'  => 'OpenAI',
            ),
            'groq'   => array(
                'prefix' => 'gsk_',
                'label'  => 'Groq',
            ),
            'gemini' => array(
                'prefix' => 'AIza',
                'label'  => 'Google Gemini',
            ),
        );
        if ( isset( $expected[$provider] ) && 0 !== strpos( $key, $expected[$provider]['prefix'] ) ) {
            $hint = '';
            foreach ( $expected as $candidate ) {
                if ( 0 === strpos( $key, $candidate['prefix'] ) ) {
                    $hint = ' ' . sprintf( 
                        /* translators: %s: AI provider name matching the key format */
                        __( 'This key looks like a %s key — switch the provider to match.', 'content-performance-analyzer' ),
                        $candidate['label']
                     );
                    break;
                }
            }
            add_settings_error(
                'cpanalyzer_messages',
                'cpanalyzer_ai_key_mismatch',
                sprintf( 
                    /* translators: 1: provider name; 2: expected key prefix */
                    __( 'The API key was saved, but it doesn\'t look like a %1$s key (expected to start with "%2$s").', 'content-performance-analyzer' ),
                    $expected[$provider]['label'],
                    $expected[$provider]['prefix']
                 ) . $hint,
                'warning'
            );
        }
        return $key;
    }

    /**
     * Sanitize PSI categories setting
     */
    public function sanitize_psi_categories( $categories ) {
        if ( is_string( $categories ) ) {
            $categories = explode( ',', $categories );
        }
        if ( !is_array( $categories ) ) {
            return array('PERFORMANCE');
        }
        $valid_categories = array(
            'PERFORMANCE',
            'SEO',
            'ACCESSIBILITY',
            'BEST_PRACTICES'
        );
        $sanitized = array();
        foreach ( $categories as $category ) {
            $category = strtoupper( trim( $category ) );
            if ( in_array( $category, $valid_categories ) ) {
                $sanitized[] = $category;
            }
        }
        return ( empty( $sanitized ) ? array('PERFORMANCE') : array_unique( $sanitized ) );
    }

    /**
     * Handle manual trigger
     */
    public function handle_manual_trigger() {
        if ( current_user_can( 'manage_options' ) && isset( $_GET['cpanalyzer_run'] ) ) {
            check_admin_referer( 'cpanalyzer_manual_run', 'cpanalyzer_nonce' );
            // Schedule background processing instead of running immediately
            if ( !wp_next_scheduled( 'cpanalyzer_daily_refresh' ) ) {
                wp_schedule_single_event( time() + 5, 'cpanalyzer_daily_refresh' );
            }
            update_option( 'cpanalyzer_status_msg', __( 'Analysis started. Processing in background...', 'content-performance-analyzer' ) );
            wp_die( sprintf( 
                /* translators: Manual analysis message */
                esc_html__( 'Manual analysis initiated! Processing will run in the background. Check back in a few minutes. %s', 'content-performance-analyzer' ),
                '<a href="' . esc_url( admin_url( 'admin.php?page=content-insights' ) ) . '">' . esc_html__( 'View Results', 'content-performance-analyzer' ) . '</a>'
             ), esc_html__( 'Manual Run Complete', 'content-performance-analyzer' ) );
        }
    }

    /**
     * AJAX handler for retrying missing AI suggestions
     */
    public function ajax_retry_missing_ai() {
        // Verify nonce
        if ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'cpanalyzer_retry_nonce' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Security check failed', 'content-performance-analyzer' ),
            ) );
        }
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        // Refresh the option model first so retry discovery works across all uploaded URLs,
        // not only the rows currently visible in the browser.
        $this->sync_batch_data_to_insights();
        $missing_payload = ( isset( $_POST['missing_payload'] ) ? json_decode( sanitize_textarea_field( wp_unslash( $_POST['missing_payload'] ) ), true ) : array() );
        if ( !is_array( $missing_payload ) || empty( $missing_payload ) ) {
            $missing_payload = $this->get_missing_ai_payload();
        }
        if ( empty( $missing_payload ) ) {
            wp_send_json_error( array(
                'message' => __( 'No missing or expired AI suggestions were found for processed URLs.', 'content-performance-analyzer' ),
                'code'    => 'no_missing',
            ) );
        }
        $ai_processor = new CPAP_AI_Processor();
        $result = $ai_processor->process_missing_suggestions( $missing_payload );
        if ( $result ) {
            $this->sync_batch_data_to_insights();
            wp_send_json_success( array(
                'message' => sprintf( 
                    /* translators: %d: number of URLs refreshed */
                    __( 'AI suggestions refreshed for %d URLs.', 'content-performance-analyzer' ),
                    count( $missing_payload )
                 ),
            ) );
        } else {
            $status_msg = get_option( 'cpanalyzer_status_msg', '' );
            wp_send_json_error( array(
                'message' => ( !empty( $status_msg ) ? wp_strip_all_tags( $status_msg ) : __( 'Failed to update suggestions', 'content-performance-analyzer' ) ),
            ) );
        }
    }

    /**
     * Enqueue admin scripts and styles
     */
    public function enqueue_admin_scripts( $hook ) {
        // Enqueue on all CPA pages
        if ( in_array( $hook, $this->page_hooks, true ) ) {
            $admin_js_path = CPANALYZER_PLUGIN_DIR . 'assets/js/admin.js';
            $admin_js_version = ( file_exists( $admin_js_path ) ? filemtime( $admin_js_path ) : CPANALYZER_VERSION );
            wp_enqueue_script(
                'cpanalyzer-admin',
                CPANALYZER_PLUGIN_URL . 'assets/js/admin.js',
                array('jquery'),
                $admin_js_version,
                true
            );
            // Add batch processing scripts for the actual URL Management page.
            if ( $hook === ($this->page_hooks['url_management_page'] ?? '') ) {
                $batch_js_path = CPANALYZER_PLUGIN_DIR . 'assets/js/batch-processor.js';
                $batch_js_version = ( file_exists( $batch_js_path ) ? filemtime( $batch_js_path ) : CPANALYZER_VERSION );
                wp_enqueue_script(
                    'cpanalyzer-batch',
                    CPANALYZER_PLUGIN_URL . 'assets/js/batch-processor.js',
                    array('jquery'),
                    $batch_js_version,
                    true
                );
                wp_localize_script( 'cpanalyzer-batch', 'cpanalyzer_batch', array(
                    'ajax_url'    => admin_url( 'admin-ajax.php' ),
                    'batch_nonce' => wp_create_nonce( 'cpanalyzer_batch_nonce' ),
                    'strings'     => array(
                        'starting'       => __( 'Starting batch process...', 'content-performance-analyzer' ),
                        'processing'     => __( 'Processing...', 'content-performance-analyzer' ),
                        'completed'      => __( 'Batch processing completed!', 'content-performance-analyzer' ),
                        'error'          => __( 'An error occurred.', 'content-performance-analyzer' ),
                        'confirm_rescan' => __( 'Are you sure you want to rescan the existing URLs? Previous GA4, GSC, content, and AI results will be cleared and rebuilt.', 'content-performance-analyzer' ),
                    ),
                ) );
                // Content Refresh Brief modal handlers
                $briefs_js_path = CPANALYZER_PLUGIN_DIR . 'assets/js/url-management-page.js';
                wp_enqueue_script(
                    'cpanalyzer-url-management',
                    CPANALYZER_PLUGIN_URL . 'assets/js/url-management-page.js',
                    array('jquery'),
                    ( file_exists( $briefs_js_path ) ? filemtime( $briefs_js_path ) : CPANALYZER_VERSION ),
                    true
                );
                wp_localize_script( 'cpanalyzer-url-management', 'cpanalyzer_briefs', array(
                    'ajax_url' => admin_url( 'admin-ajax.php' ),
                    'strings'  => array(
                        'generating'     => __( 'Generating…', 'content-performance-analyzer' ),
                        'view_brief'     => __( 'View Brief', 'content-performance-analyzer' ),
                        'generate_brief' => __( 'Generate Brief', 'content-performance-analyzer' ),
                        'gen_failed'     => __( 'Failed to generate brief.', 'content-performance-analyzer' ),
                        'load_failed'    => __( 'Could not load brief data.', 'content-performance-analyzer' ),
                        'server_error'   => __( 'Server error. Please try again.', 'content-performance-analyzer' ),
                    ),
                ) );
                // Site-page picker (import URLs from site content).
                $picker_js_path = CPANALYZER_PLUGIN_DIR . 'assets/js/site-page-picker.js';
                wp_enqueue_script(
                    'cpanalyzer-site-page-picker',
                    CPANALYZER_PLUGIN_URL . 'assets/js/site-page-picker.js',
                    array('jquery'),
                    ( file_exists( $picker_js_path ) ? filemtime( $picker_js_path ) : CPANALYZER_VERSION ),
                    true
                );
                wp_localize_script( 'cpanalyzer-site-page-picker', 'cpanalyzer_site_pages', array(
                    'ajax_url' => admin_url( 'admin-ajax.php' ),
                    'nonce'    => wp_create_nonce( 'cpanalyzer_site_pages' ),
                    'strings'  => array(
                        'loading'         => __( 'Loading pages…', 'content-performance-analyzer' ),
                        'load_button'     => __( 'Load Pages From Your Site', 'content-performance-analyzer' ),
                        'reload_button'   => __( 'Reload Pages', 'content-performance-analyzer' ),
                        'load_failed'     => __( 'Could not load site pages. Please try again.', 'content-performance-analyzer' ),
                        'importing'       => __( 'Importing…', 'content-performance-analyzer' ),
                        'import_button'   => __( 'Import Selected Pages', 'content-performance-analyzer' ),
                        'import_failed'   => __( 'Import failed. Please try again.', 'content-performance-analyzer' ),
                        'none_selected'   => __( 'Select at least one page to import.', 'content-performance-analyzer' ),
                        'no_pages'        => __( 'No published pages or posts found.', 'content-performance-analyzer' ),
                        'selected_of'     => __( '%1$s of %2$s pages selected', 'content-performance-analyzer' ),
                        'confirm_replace' => __( 'Replace mode removes all existing URLs and their results before importing. Continue?', 'content-performance-analyzer' ),
                    ),
                ) );
            }
            // Add smart interlinking scripts
            if ( $hook === ($this->page_hooks['interlinking'] ?? '') ) {
                wp_enqueue_script(
                    'cpanalyzer-interlinking',
                    CPANALYZER_PLUGIN_URL . 'assets/js/cpa-interlinking.js',
                    array('jquery'),
                    CPANALYZER_VERSION,
                    true
                );
                wp_localize_script( 'cpanalyzer-interlinking', 'cpanalyzer_interlinking', array(
                    'ajaxurl' => admin_url( 'admin-ajax.php' ),
                    'nonce'   => wp_create_nonce( 'cpanalyzer_interlinking_nonce' ),
                    'strings' => array(
                        'loading'        => __( 'Loading suggestions...', 'content-performance-analyzer' ),
                        'applying'       => __( 'Applying links...', 'content-performance-analyzer' ),
                        'success'        => __( 'Links applied successfully!', 'content-performance-analyzer' ),
                        'error'          => __( 'An error occurred.', 'content-performance-analyzer' ),
                        'select_both'    => __( 'Please select both source and target posts.', 'content-performance-analyzer' ),
                        'no_suggestions' => __( 'No suitable linking opportunities found.', 'content-performance-analyzer' ),
                    ),
                ) );
            }
            $cpanalyzer_provider_labels = array(
                'openai' => 'OpenAI',
                'groq'   => 'Groq',
                'gemini' => 'Google Gemini',
            );
            $cpanalyzer_ajax_data = array(
                'ajax_url'            => admin_url( 'admin-ajax.php' ),
                'retry_nonce'         => wp_create_nonce( 'cpanalyzer_retry_nonce' ),
                'batch_nonce'         => wp_create_nonce( 'cpanalyzer_batch_nonce' ),
                'manual_nonce'        => wp_create_nonce( 'cpanalyzer_manual_run' ),
                'serper_debug'        => $this->get_recent_serper_debug_logs(),
                'psi_limit'           => cpanalyzer_psi_page_limit(),
                'pro_ready'           => false,
                'strings'             => array(
                    'contacting_ai'            => sprintf( 
                        /* translators: %s: configured AI provider name */
                        __( 'Contacting %s…', 'content-performance-analyzer' ),
                        $cpanalyzer_provider_labels[get_option( 'cpanalyzer_ai_provider', 'openai' )] ?? __( 'AI provider', 'content-performance-analyzer' )
                     ),
                    'done_refresh'             => __( 'Done! Refresh to see results.', 'content-performance-analyzer' ),
                    'retry_failed'             => __( 'Retry failed.', 'content-performance-analyzer' ),
                    'no_missing'               => __( 'No missing/expired AI suggestions found!', 'content-performance-analyzer' ),
                    'retry_scan_confirm'       => __( 'Retry scan for this URL? The current PSI and AI result for that page will be rebuilt.', 'content-performance-analyzer' ),
                    'retry_scan_working'       => __( 'Reprocessing URL…', 'content-performance-analyzer' ),
                    'retry_scan_done'          => __( 'URL rescanned successfully.', 'content-performance-analyzer' ),
                    'oauth_disconnect_confirm' => __( 'Disconnect Google account? You will need to reconnect to collect GA4/GSC data.', 'content-performance-analyzer' ),
                    'oauth_disconnecting'      => __( 'Disconnecting…', 'content-performance-analyzer' ),
                    'oauth_disconnect'         => __( 'Disconnect', 'content-performance-analyzer' ),
                    'fix_alt_confirm'          => __( 'Generate alt text with AI and apply it to these images? Existing alt text is never overwritten.', 'content-performance-analyzer' ),
                    'fix_alt_working'          => __( 'Generating alt text…', 'content-performance-analyzer' ),
                    'fix_alt_button'           => __( 'Generate and apply alt text with AI', 'content-performance-analyzer' ),
                    'fix_alt_failed'           => __( 'Alt text fix failed. Please try again.', 'content-performance-analyzer' ),
                    'fix_alt_ai_missing'       => __( 'Add an OpenAI, Groq, or Gemini API key in Settings to use this.', 'content-performance-analyzer' ),
                    'write_answer'             => __( 'Write answer & add to page', 'content-performance-analyzer' ),
                    'writing_answer'           => __( 'Writing…', 'content-performance-analyzer' ),
                    'answer_confirm'           => __( 'AI will write a 40-60 word answer based on this page\'s content and append it as a Q&A block at the end of the page. Continue?', 'content-performance-analyzer' ),
                    'answer_failed'            => __( 'Could not generate the answer. Please try again.', 'content-performance-analyzer' ),
                    'mark_working'             => __( 'Saving…', 'content-performance-analyzer' ),
                    'mark_failed'              => __( 'Could not save. Please try again.', 'content-performance-analyzer' ),
                    'draft_working'            => __( 'Creating draft…', 'content-performance-analyzer' ),
                    'draft_open'               => __( 'Open draft', 'content-performance-analyzer' ),
                    'draft_failed'             => __( 'Could not create the draft.', 'content-performance-analyzer' ),
                    'copy_label'               => __( 'Copy', 'content-performance-analyzer' ),
                    'copy_done'                => __( 'Copied', 'content-performance-analyzer' ),
                    'tab_empty'                => __( 'Nothing to show here for this page.', 'content-performance-analyzer' ),
                ),
                'fix_alt_nonce'       => wp_create_nonce( 'cpanalyzer_fix_alt' ),
                'answer_nonce'        => wp_create_nonce( 'cpanalyzer_answer_block' ),
                'mark_fixed_nonce'    => wp_create_nonce( 'cpanalyzer_mark_fixed' ),
                'refresh_draft_nonce' => wp_create_nonce( 'cpanalyzer_refresh_draft' ),
                'ai_ready'            => class_exists( 'CPAP_AI_Processor' ) && ( new CPAP_AI_Processor() )->is_configured(),
            );
            wp_localize_script( 'cpanalyzer-admin', 'cpanalyzer_ajax', $cpanalyzer_ajax_data );
            wp_enqueue_style(
                'cpanalyzer-admin',
                CPANALYZER_PLUGIN_URL . 'assets/css/admin.css',
                array(),
                ( file_exists( CPANALYZER_PLUGIN_DIR . 'assets/css/admin.css' ) ? filemtime( CPANALYZER_PLUGIN_DIR . 'assets/css/admin.css' ) : CPANALYZER_VERSION )
            );
            // Page-specific CSS
            if ( $hook === ($this->page_hooks['insights'] ?? '') ) {
                $insights_css_path = CPANALYZER_PLUGIN_DIR . 'assets/css/insights-page.css';
                wp_enqueue_style(
                    'cpanalyzer-insights-page',
                    CPANALYZER_PLUGIN_URL . 'assets/css/insights-page.css',
                    array('cpanalyzer-admin'),
                    ( file_exists( $insights_css_path ) ? filemtime( $insights_css_path ) : CPANALYZER_VERSION )
                );
            }
            if ( $hook === ($this->page_hooks['interlinking'] ?? '') ) {
                $interlinking_css_path = CPANALYZER_PLUGIN_DIR . 'assets/css/interlinking-page.css';
                wp_enqueue_style(
                    'cpanalyzer-interlinking-page',
                    CPANALYZER_PLUGIN_URL . 'assets/css/interlinking-page.css',
                    array('cpanalyzer-admin'),
                    ( file_exists( $interlinking_css_path ) ? filemtime( $interlinking_css_path ) : CPANALYZER_VERSION )
                );
            }
            if ( $hook === ($this->page_hooks['internal_links'] ?? '') ) {
                $internal_links_css_path = CPANALYZER_PLUGIN_DIR . 'assets/css/internal-links-page.css';
                wp_enqueue_style(
                    'cpanalyzer-internal-links-page',
                    CPANALYZER_PLUGIN_URL . 'assets/css/internal-links-page.css',
                    array('cpanalyzer-admin'),
                    ( file_exists( $internal_links_css_path ) ? filemtime( $internal_links_css_path ) : CPANALYZER_VERSION )
                );
            }
            if ( $hook === ($this->page_hooks['url_management_page'] ?? '') ) {
                $url_management_css_path = CPANALYZER_PLUGIN_DIR . 'assets/css/url-management-page.css';
                wp_enqueue_style(
                    'cpanalyzer-url-management-page',
                    CPANALYZER_PLUGIN_URL . 'assets/css/url-management-page.css',
                    array('cpanalyzer-admin'),
                    ( file_exists( $url_management_css_path ) ? filemtime( $url_management_css_path ) : CPANALYZER_VERSION )
                );
            }
        }
    }

    /**
     * Return recent Serper debug entries for temporary browser-console inspection.
     *
     * @param int $limit
     * @return array
     */
    private function get_recent_serper_debug_logs( $limit = 20 ) {
        $logs = get_transient( 'cpanalyzer_event_logs' );
        if ( !is_array( $logs ) || empty( $logs ) ) {
            return array();
        }
        $serper_logs = array();
        foreach ( $logs as $log ) {
            $event = ( isset( $log['event'] ) ? (string) $log['event'] : '' );
            if ( strpos( $event, 'serper_' ) !== 0 ) {
                continue;
            }
            $serper_logs[] = array(
                'timestamp' => ( isset( $log['timestamp'] ) ? (string) $log['timestamp'] : '' ),
                'event'     => $event,
                'message'   => ( isset( $log['message'] ) ? (string) $log['message'] : '' ),
                'data'      => ( isset( $log['data'] ) && is_array( $log['data'] ) ? $log['data'] : array() ),
            );
            if ( count( $serper_logs ) >= $limit ) {
                break;
            }
        }
        return $serper_logs;
    }

    /**
     * Display insights page
     */
    public function insights_page() {
        include CPANALYZER_PLUGIN_DIR . 'templates/insights-page.php';
    }

    /**
     * Export insights data to CSV file
     */
    private function export_insights_to_csv() {
        // Clean any existing output
        if ( ob_get_level() ) {
            ob_end_clean();
        }
        // Prevent any caching
        nocache_headers();
        // Get all data
        $flags = (array) get_option( 'cpanalyzer_page_flags', array() );
        $ga_rows = (array) get_option( 'cpanalyzer_ga_rows', array() );
        $gsc_rows = (array) get_option( 'cpanalyzer_gsc_rows', array() );
        $content_data = (array) get_option( 'cpanalyzer_content_data', array() );
        $actions = (array) get_option( 'cpanalyzer_page_actions', array() );
        // Create filename with timestamp
        $filename = 'cpanalyzer-insights-' . gmdate( 'Y-m-d-H-i-s' ) . '.csv';
        // Set headers for download
        header( 'Content-Type: text/csv; charset=utf-8' );
        header( 'Content-Disposition: attachment; filename=' . $filename );
        header( 'Cache-Control: must-revalidate' );
        // Initialize WP_Filesystem
        global $wp_filesystem;
        if ( !function_exists( 'WP_Filesystem' ) ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        WP_Filesystem();
        // CSV Headers
        $headers = array(
            'URL',
            'Flag',
            'GA4 Views',
            'GA4 Users',
            'GA4 Engagement Rate',
            'GA4 Avg Session Duration',
            'GA4 Conversions',
            'GA4 Previous Views',
            'GSC Impressions',
            'GSC Clicks',
            'GSC CTR (%)',
            'GSC Position',
            'GSC Previous Impressions',
            'GSC Keywords',
            'Content Word Count',
            'Content Images',
            'Content H1 Tags',
            'Content H2 Tags',
            'Content Internal Links',
            'Content Has Forms',
            'Content CTA Buttons',
            'AI Action',
            'AI Priority',
            'AI Issue Summary',
            'AI Why Now',
            'AI SEO Recommendation',
            'AI AEO Recommendation',
            'AI GEO Recommendation',
            'AI Engagement Recommendation',
            'AI Measurement Plan',
            'AI Model Used',
            'AI Keywords',
            'AI Technical Issues',
            'AI Timestamp',
            'Decay Type',
            'Decay Evidence',
            'Decay Fix Hint',
            'AEO Readiness Score',
            'AI Referral Sessions',
            'Inbound Internal Links'
        );
        // Start building CSV content in memory
        $csv_content = chr( 0xef ) . chr( 0xbb ) . chr( 0xbf );
        // BOM for Excel compatibility
        // Create CSV data using temp file to avoid memory issues
        global $wp_filesystem;
        if ( empty( $wp_filesystem ) ) {
            require_once ABSPATH . '/wp-admin/includes/file.php';
            WP_Filesystem();
        }
        $temp_file = wp_tempnam( 'cpanalyzer-export' );
        // Start building CSV content
        $csv_content = '';
        // Add headers
        $csv_content .= implode( ',', array_map( function ( $header ) {
            return '"' . str_replace( '"', '""', $header ) . '"';
        }, $headers ) ) . "\n";
        // Get all unique URLs from all data sources
        $all_urls = array_unique( array_merge(
            array_keys( $flags ),
            array_keys( $ga_rows ),
            array_keys( $gsc_rows ),
            array_keys( $content_data )
        ) );
        // Export data for each URL
        foreach ( $all_urls as $url ) {
            $flag = $flags[$url] ?? 'none';
            $ga = $ga_rows[$url] ?? array();
            $gsc = $gsc_rows[$url] ?? array();
            $content = $content_data[$url] ?? array();
            // Find AI action for this URL
            $ai_action = '';
            $ai_priority = '';
            $ai_issue_summary = '';
            $ai_why_now = '';
            $ai_seo_rec = '';
            $ai_aeo_rec = '';
            $ai_geo_rec = '';
            $ai_engagement_rec = '';
            $ai_measurement_plan = '';
            $ai_model_used = '';
            $ai_keywords = '';
            $ai_technical_issues = '';
            $ai_timestamp = '';
            foreach ( $actions as $action ) {
                if ( isset( $action['url'] ) && $action['url'] === $url ) {
                    $ai_action = $action['action'] ?? '';
                    $ai_priority = $action['content_priority'] ?? '';
                    $ai_issue_summary = $action['issue_summary'] ?? '';
                    $ai_why_now = $action['why_now'] ?? '';
                    $ai_seo_rec = $action['seo_recommendation'] ?? '';
                    $ai_aeo_rec = $action['aeo_recommendation'] ?? '';
                    $ai_geo_rec = $action['geo_recommendation'] ?? '';
                    $ai_engagement_rec = $action['engagement_recommendation'] ?? '';
                    $ai_measurement_plan = $action['measurement_plan'] ?? '';
                    $ai_model_used = $action['model_used'] ?? '';
                    $ai_keywords = ( isset( $action['suggested_keywords'] ) ? implode( ', ', (array) $action['suggested_keywords'] ) : '' );
                    $ai_technical_issues = ( isset( $action['technical_issues'] ) ? implode( ', ', (array) $action['technical_issues'] ) : '' );
                    $ai_timestamp = ( isset( $action['timestamp'] ) ? gmdate( 'Y-m-d H:i:s', $action['timestamp'] ) : '' );
                    break;
                }
            }
            // Decay diagnosis + AEO readiness — deterministic values.
            $decay = (array) ($gsc['decay_diagnosis'] ?? array());
            $decay_type = ( 'none' !== ($decay['type'] ?? 'none') ? (string) ($decay['type'] ?? '') : '' );
            $decay_evidence = ( '' !== $decay_type ? trim( ($decay['evidence'] ?? '') . ' ' . ($decay['trend_note'] ?? '') ) : '' );
            $decay_fix = ( '' !== $decay_type ? (string) ($decay['fix_hint'] ?? '') : '' );
            $aeo_score = '';
            if ( function_exists( 'cpanalyzer_aeo_readiness' ) && !empty( $content ) ) {
                $aeo = cpanalyzer_aeo_readiness( $content );
                $aeo_score = ( is_array( $aeo ) ? (int) $aeo['score'] : '' );
            }
            // Format GSC keywords
            $gsc_keywords = '';
            if ( isset( $gsc['keywords'] ) && is_array( $gsc['keywords'] ) ) {
                $keyword_list = array();
                foreach ( $gsc['keywords'] as $kw ) {
                    if ( isset( $kw['keyword'] ) ) {
                        $keyword_list[] = $kw['keyword'] . ' (' . ($kw['impressions'] ?? 0) . ')';
                    }
                }
                $gsc_keywords = implode( '; ', $keyword_list );
            }
            $row = array(
                $url,
                $flag,
                $ga['views'] ?? 0,
                $ga['uniques'] ?? 0,
                round( ($ga['engagement'] ?? 0) * 100, 2 ),
                round( $ga['duration'] ?? 0, 2 ),
                $ga['form_fills'] ?? 0,
                $ga['views_prev'] ?? 0,
                $gsc['impressions'] ?? 0,
                $gsc['clicks'] ?? 0,
                $gsc['ctr'] ?? 0,
                $gsc['position'] ?? 0,
                $gsc['impressions_prev'] ?? 0,
                $gsc_keywords,
                $content['word_count'] ?? 0,
                $content['image_count'] ?? 0,
                count( $content['h1_tags'] ?? array() ),
                count( $content['h2_tags'] ?? array() ),
                $content['internal_links'] ?? 0,
                ( isset( $content['has_forms']['count'] ) && $content['has_forms']['count'] > 0 ? 'Yes' : 'No' ),
                $content['has_cta_buttons']['count'] ?? 0,
                $ai_action,
                $ai_priority,
                $ai_issue_summary,
                $ai_why_now,
                $ai_seo_rec,
                $ai_aeo_rec,
                $ai_geo_rec,
                $ai_engagement_rec,
                $ai_measurement_plan,
                $ai_model_used,
                $ai_keywords,
                $ai_technical_issues,
                $ai_timestamp,
                $decay_type,
                $decay_evidence,
                $decay_fix,
                $aeo_score,
                $ga['ai_sessions'] ?? 0,
                ( isset( $content['inbound_internal_links'] ) ? (int) $content['inbound_internal_links'] : '' )
            );
            // Add CSV row to content — prefix formula-triggering characters so
            // scraped/AI text can't execute as a spreadsheet formula
            $csv_content .= implode( ',', array_map( function ( $field ) {
                $field = (string) $field;
                if ( $field !== '' && !is_numeric( $field ) && strpbrk( $field[0], '=+-@' ) !== false ) {
                    $field = "'" . $field;
                }
                return '"' . str_replace( '"', '""', $field ) . '"';
            }, $row ) ) . "\n";
        }
        // Write final content to temp file and read back
        $wp_filesystem->put_contents( $temp_file, $csv_content );
        $csv_content = $wp_filesystem->get_contents( $temp_file );
        // Clean up temporary file
        $wp_filesystem->delete( $temp_file );
        // Output the content - CSV data is already properly formatted
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo $csv_content;
        exit;
    }

    /**
     * Display settings page
     */
    public function settings_page() {
        include CPANALYZER_PLUGIN_DIR . 'templates/settings-page.php';
    }

    /**
     * Display internal links page
     */
    public function internal_links_page() {
        include CPANALYZER_PLUGIN_DIR . 'templates/internal-links-page.php';
    }

    /**
     * Display smart interlinking page
     */
    public function interlinking_page() {
        include CPANALYZER_PLUGIN_DIR . 'templates/interlinking-page.php';
    }

    /**
     * AJAX handler for manual sync of batch data
     */
    public function ajax_sync_batch_data() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $this->sync_batch_data_to_insights();
        wp_send_json_success( array(
            'message' => __( 'Batch data synced to insights display successfully!', 'content-performance-analyzer' ),
        ) );
    }

    /**
     * Ensure only uploaded URLs are displayed (clear any auto-discovered data)
     */
    public function ensure_uploaded_urls_only() {
        // Only run this on insights page to avoid performance issues
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if ( !isset( $_GET['page'] ) || $_GET['page'] !== 'content-insights' ) {
            return;
        }
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // Check if table exists and has uploaded URLs
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) ) === $table_name ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            $uploaded_count = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) );
            // If no uploaded URLs, clear all data
            if ( $uploaded_count == 0 ) {
                update_option( 'cpanalyzer_page_flags', array(), false );
                update_option( 'cpanalyzer_ga_rows', array(), false );
                update_option( 'cpanalyzer_gsc_rows', array(), false );
                update_option( 'cpanalyzer_content_data', array(), false );
                update_option( 'cpanalyzer_page_actions', array(), false );
                update_option( 'cpanalyzer_status_msg', __( 'No uploaded URLs found. Please upload URLs via URL Management to analyze specific pages.', 'content-performance-analyzer' ) );
            }
        }
    }

    /**
     * Sync batch processed URLs to insights page display options
     */
    public function sync_batch_data_to_insights() {
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // Get ALL URLs from batch table (including pending ones for display) in upload order
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $processed_urls = $wpdb->get_results( "SELECT url, ga_data, gsc_data, content_data, ai_suggestions, status, priority\n             FROM " . esc_sql( $table_name ) . " \n             ORDER BY priority ASC, id ASC", ARRAY_A );
        // Existing option actions can be fresher than table data if they were created by
        // the legacy retry path before canonical table persistence is restored.
        $existing_actions = (array) get_option( 'cpanalyzer_page_actions', array() );
        $existing_actions_by_url = $this->index_actions_by_url( $existing_actions );
        if ( empty( $processed_urls ) ) {
            // No uploaded URLs - clear all data and show empty state
            update_option( 'cpanalyzer_page_flags', array(), false );
            update_option( 'cpanalyzer_ga_rows', array(), false );
            update_option( 'cpanalyzer_gsc_rows', array(), false );
            update_option( 'cpanalyzer_content_data', array(), false );
            update_option( 'cpanalyzer_page_actions', array(), false );
            update_option( 'cpanalyzer_status_msg', __( 'No uploaded URLs found. Please upload URLs via URL Management to analyze specific pages.', 'content-performance-analyzer' ) );
            return;
        }
        // Initialize arrays for options
        $ga_rows = array();
        $gsc_rows = array();
        $content_data = array();
        $page_flags = array();
        $page_actions = array();
        // Process each URL
        foreach ( $processed_urls as $row ) {
            $url = $row['url'];
            $status = $row['status'];
            // Decode JSON data (handle both successful and pending URLs)
            $ga_data = ( !empty( $row['ga_data'] ) ? json_decode( $row['ga_data'], true ) : array() );
            $gsc_data = ( !empty( $row['gsc_data'] ) ? json_decode( $row['gsc_data'], true ) : array() );
            $content = ( !empty( $row['content_data'] ) ? json_decode( $row['content_data'], true ) : array() );
            $stored_ai_data = ( !empty( $row['ai_suggestions'] ) ? json_decode( $row['ai_suggestions'], true ) : array() );
            if ( !is_array( $stored_ai_data ) ) {
                $stored_ai_data = array();
            }
            $existing_action = $existing_actions_by_url[$url] ?? array();
            $stored_timestamp = intval( $stored_ai_data['timestamp'] ?? 0 );
            $option_timestamp = intval( $existing_action['timestamp'] ?? 0 );
            if ( !empty( $existing_action ) && (empty( $stored_ai_data ) || $option_timestamp > $stored_timestamp) ) {
                $ai_data = array_merge( $stored_ai_data, $existing_action );
                $this->sync_ai_action_to_table( $table_name, $url, $ai_data );
            } else {
                $ai_data = array_merge( $existing_action, $stored_ai_data );
            }
            // Skip URLs with no data at all (just uploaded, never processed)
            if ( empty( $ga_data ) && empty( $gsc_data ) && empty( $content ) && empty( $ai_data ) && $status === 'pending' ) {
                // Still add to flags so it shows up as awaiting processing
                $page_flags[$url] = 'none';
                continue;
            }
            // Add GA data
            if ( !empty( $ga_data ) ) {
                $ga_rows[$url] = array(
                    'views'           => $ga_data['views'] ?? 0,
                    'uniques'         => $ga_data['uniques'] ?? 0,
                    'engagement'      => $ga_data['engagement'] ?? 0,
                    'engagement_prev' => $ga_data['engagement_prev'] ?? 0,
                    'duration'        => $ga_data['duration'] ?? 0,
                    'form_fills'      => $ga_data['form_fills'] ?? 0,
                    'views_prev'      => $ga_data['views_prev'] ?? 0,
                    'ai_sessions'     => $ga_data['ai_sessions'] ?? 0,
                );
            }
            // Add GSC data
            if ( !empty( $gsc_data ) ) {
                $gsc_rows[$url] = array(
                    'impressions'      => $gsc_data['impressions'] ?? 0,
                    'clicks'           => $gsc_data['clicks'] ?? 0,
                    'ctr'              => $gsc_data['ctr'] ?? 0,
                    'position'         => $gsc_data['position'] ?? 0,
                    'impressions_prev' => $gsc_data['impressions_prev'] ?? 0,
                    'clicks_prev'      => $gsc_data['clicks_prev'] ?? 0,
                    'ctr_prev'         => $gsc_data['ctr_prev'] ?? 0,
                    'position_prev'    => $gsc_data['position_prev'] ?? 0,
                    'decay_diagnosis'  => $gsc_data['decay_diagnosis'] ?? array(),
                    'serp_features'    => $gsc_data['serp_features'] ?? array(),
                    'trend'            => $gsc_data['trend'] ?? array(),
                    'cannibalization'  => $gsc_data['cannibalization'] ?? array(),
                    'keywords'         => $gsc_data['keywords'] ?? array(),
                );
            }
            // Add content data
            if ( !empty( $content ) ) {
                $content_data[$url] = $content;
            }
            // Determine flag based on available data
            $flag = $this->determine_flag_from_batch_data( $ga_data, $gsc_data, $content );
            $page_flags[$url] = $flag;
            // Add AI suggestions if available
            if ( !empty( $ai_data ) ) {
                $page_actions[] = $this->build_page_action_from_ai_data( $url, $flag, $ai_data );
            }
        }
        // Replace all data with uploaded URLs only (no merging with existing data)
        update_option( 'cpanalyzer_page_flags', $page_flags, false );
        update_option( 'cpanalyzer_ga_rows', $ga_rows, false );
        update_option( 'cpanalyzer_gsc_rows', $gsc_rows, false );
        update_option( 'cpanalyzer_content_data', $content_data, false );
        update_option( 'cpanalyzer_page_actions', $page_actions, false );
        // Store URL upload order for display sorting
        $url_order = array();
        foreach ( $processed_urls as $row ) {
            $priority = ( isset( $row['priority'] ) ? intval( $row['priority'] ) : 999 );
            $url_order[$row['url']] = $priority;
        }
        update_option( 'cpanalyzer_url_upload_order', $url_order );
        // Track newly flagged pages so scheduled/off-peak runs surface fresh
        // findings without anyone watching the run.
        $this->maybe_record_new_flags( $page_flags );
        CPAP_Snapshots::maybe_record(
            $page_flags,
            $ga_rows,
            $gsc_rows,
            $content_data
        );
        $this->maybe_schedule_ai_drain();
        // Log sync event with detailed counts
        $this->log_sync_event(
            count( $processed_urls ),
            count( $page_flags ),
            count( $ga_rows ),
            count( $content_data )
        );
    }

    /**
     * Diff the current flag set against the last known snapshot and remember
     * pages that JUST became flagged. Powers the "new issues found" admin
     * notice after automatic scans. Newly flagged URLs are merged (not
     * replaced) so multi-chunk batch runs accumulate correctly.
     */
    private function maybe_record_new_flags( $page_flags ) {
        $snapshot = get_option( 'cpanalyzer_flag_snapshot', null );
        // First sync ever: set the baseline silently — everything would
        // otherwise read as "new".
        if ( !is_array( $snapshot ) ) {
            update_option( 'cpanalyzer_flag_snapshot', $page_flags, false );
            return;
        }
        if ( $snapshot === $page_flags ) {
            return;
        }
        $newly_flagged = array();
        foreach ( $page_flags as $url => $flag ) {
            if ( 'none' === $flag ) {
                continue;
            }
            if ( 'none' === ($snapshot[$url] ?? 'none') ) {
                $newly_flagged[$url] = $flag;
            }
        }
        update_option( 'cpanalyzer_flag_snapshot', $page_flags, false );
        if ( empty( $newly_flagged ) ) {
            return;
        }
        $existing = get_option( 'cpanalyzer_new_flags', array() );
        $merged = array_merge( (array) ($existing['flags'] ?? array()), $newly_flagged );
        update_option( 'cpanalyzer_new_flags', array(
            'flags' => $merged,
            'time'  => time(),
        ), false );
    }

    /**
     * Admin notice for pages newly flagged since the last review. Cleared
     * when the Insights page is opened — reviewing the findings dismisses it.
     */
    public function render_new_flags_notice() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        // The Insights page both shows the detail and clears the notice.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page check for notice display.
        $current_page = ( isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '' );
        if ( 'content-insights' === $current_page ) {
            return;
        }
        $new_flags = get_option( 'cpanalyzer_new_flags', array() );
        if ( empty( $new_flags['flags'] ) || !is_array( $new_flags['flags'] ) ) {
            return;
        }
        $counts = array_count_values( array_map( 'strval', $new_flags['flags'] ) );
        $parts = array();
        foreach ( $counts as $flag => $count ) {
            $parts[] = sprintf( '%d %s', $count, $flag );
        }
        printf(
            '<div class="notice notice-warning"><p><strong>%s</strong> %s <a href="%s">%s</a></p></div>',
            esc_html__( 'Content Performance Analyzer:', 'content-performance-analyzer' ),
            esc_html( sprintf( 
                /* translators: 1: number of newly flagged pages; 2: breakdown by flag type */
                _n(
                    'the latest analysis flagged %1$d page that was previously healthy (%2$s).',
                    'the latest analysis flagged %1$d pages that were previously healthy (%2$s).',
                    count( $new_flags['flags'] ),
                    'content-performance-analyzer'
                ),
                count( $new_flags['flags'] ),
                implode( ', ', $parts )
             ) ),
            esc_url( admin_url( 'admin.php?page=content-insights' ) ),
            esc_html__( 'Review in Insights', 'content-performance-analyzer' )
        );
    }

    /**
     * Index action rows by URL for fast lookups during sync.
     */
    private function index_actions_by_url( $actions ) {
        $indexed = array();
        foreach ( (array) $actions as $action ) {
            if ( !is_array( $action ) || empty( $action['url'] ) ) {
                continue;
            }
            $indexed[$action['url']] = $action;
        }
        return $indexed;
    }

    /**
     * Persist an AI action back to the uploaded URL row so table storage stays canonical.
     */
    private function sync_ai_action_to_table( $table_name, $url, $ai_data ) {
        global $wpdb;
        if ( empty( $url ) || empty( $ai_data ) || !is_array( $ai_data ) ) {
            return;
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update(
            $table_name,
            array(
                'ai_suggestions' => wp_json_encode( $ai_data ),
            ),
            array(
                'url' => $url,
            ),
            array('%s'),
            array('%s')
        );
    }

    /**
     * Preserve richer AI fields while guaranteeing the schema the Insights page expects.
     */
    private function build_page_action_from_ai_data( $url, $flag, $ai_data ) {
        $suggested_keywords = $ai_data['suggested_keywords'] ?? $ai_data['keywords'] ?? array();
        $technical_issues = $ai_data['technical_issues'] ?? array();
        if ( is_string( $suggested_keywords ) ) {
            $suggested_keywords = preg_split( '/[,;\\n]+/', $suggested_keywords );
        }
        if ( is_string( $technical_issues ) ) {
            $technical_issues = preg_split( '/[,;\\n]+/', $technical_issues );
        }
        $suggested_keywords = array_values( array_filter( (array) $suggested_keywords ) );
        $technical_issues = array_values( array_filter( (array) $technical_issues ) );
        $primary_keyword = $ai_data['primary_keyword'] ?? $suggested_keywords[0] ?? $ai_data['keywords'][0] ?? '';
        $normalized = array(
            'url'                       => $url,
            'flag'                      => $ai_data['flag'] ?? $flag,
            'action'                    => $ai_data['action'] ?? '',
            'issue_summary'             => $ai_data['issue_summary'] ?? '',
            'why_now'                   => $ai_data['why_now'] ?? '',
            'seo_recommendation'        => $ai_data['seo_recommendation'] ?? '',
            'aeo_recommendation'        => $ai_data['aeo_recommendation'] ?? '',
            'geo_recommendation'        => $ai_data['geo_recommendation'] ?? '',
            'engagement_recommendation' => $ai_data['engagement_recommendation'] ?? '',
            'measurement_plan'          => $ai_data['measurement_plan'] ?? '',
            'primary_keyword'           => $primary_keyword,
            'suggested_keywords'        => $suggested_keywords,
            'content_priority'          => $ai_data['content_priority'] ?? $ai_data['priority'] ?? 'medium',
            'technical_issues'          => $technical_issues,
            'action_type'               => $ai_data['action_type'] ?? '',
            'expected_ctr_delta'        => $ai_data['expected_ctr_delta'] ?? '',
            'expected_conv_uplift'      => $ai_data['expected_conv_uplift'] ?? '',
            'opportunity_score'         => intval( $ai_data['opportunity_score'] ?? 0 ),
            'confidence'                => floatval( $ai_data['confidence'] ?? 0 ),
            'effort_hours'              => floatval( $ai_data['effort_hours'] ?? 0 ),
            'estimated_impact'          => $ai_data['estimated_impact'] ?? '',
            'timestamp'                 => intval( $ai_data['timestamp'] ?? time() ),
        );
        return array_merge( $ai_data, $normalized );
    }

    /**
     * Find processed URLs that still need AI suggestions or have expired ones.
     */
    private function get_missing_ai_payload() {
        $flags = (array) get_option( 'cpanalyzer_page_flags', array() );
        $ga_rows = (array) get_option( 'cpanalyzer_ga_rows', array() );
        $gsc_rows = (array) get_option( 'cpanalyzer_gsc_rows', array() );
        $content_rows = (array) get_option( 'cpanalyzer_content_data', array() );
        $actions_by_url = $this->index_actions_by_url( (array) get_option( 'cpanalyzer_page_actions', array() ) );
        $missing_payload = array();
        foreach ( $flags as $url => $flag ) {
            $has_processed_data = !empty( $ga_rows[$url] ) || !empty( $gsc_rows[$url] ) || !empty( $content_rows[$url] );
            if ( !$has_processed_data ) {
                continue;
            }
            $action = $actions_by_url[$url] ?? array();
            $timestamp = intval( $action['timestamp'] ?? 0 );
            $is_expired = ( !empty( $action ) && function_exists( 'cpanalyzer_is_suggestion_expired' ) ? cpanalyzer_is_suggestion_expired( $timestamp ) : false );
            if ( empty( $action ) || $is_expired ) {
                $missing_payload[] = array(
                    'url'  => $url,
                    'flag' => $flag,
                );
            }
        }
        return $missing_payload;
    }

    /**
     * Pages the auto-drain may process: flagged pages with NO AI action at
     * all. Expired-but-present suggestions are deliberately excluded — the
     * drain fills gaps from interrupted runs; it must never silently
     * re-spend the user's API credits refreshing suggestions that already
     * exist. The manual "Retry missing/expired" button still covers expired
     * ones on demand.
     *
     * @param mixed $skipped URLs permanently skipped this drain cycle.
     * @return array
     */
    private function get_ai_drain_targets( $skipped ) {
        $actions_by_url = $this->index_actions_by_url( (array) get_option( 'cpanalyzer_page_actions', array() ) );
        $skipped = ( is_array( $skipped ) ? $skipped : array() );
        return array_values( array_filter( $this->get_missing_ai_payload(), function ( $item ) use($skipped, $actions_by_url) {
            $url = $item['url'] ?? '';
            return '' !== $url && !in_array( $url, $skipped, true ) && empty( $actions_by_url[$url] );
        } ) );
    }

    /**
     * Schedule slow background recovery for flagged pages missing AI actions.
     */
    private function maybe_schedule_ai_drain() {
        if ( !apply_filters( 'cpanalyzer_ai_auto_drain', true ) ) {
            return;
        }
        if ( !cpanalyzer_ai_is_configured() ) {
            return;
        }
        if ( get_transient( 'cpanalyzer_batch_running' ) ) {
            return;
        }
        $state = array_merge( array(
            'active'      => false,
            'remaining'   => 0,
            'attempts'    => 0,
            'daily_count' => 0,
            'daily_date'  => '',
            'next_at'     => 0,
            'last_error'  => '',
            'stopped'     => '',
            'skipped'     => array(),
        ), (array) get_option( 'cpanalyzer_ai_drain_state', array() ) );
        $today = gmdate( 'Y-m-d' );
        if ( $state['daily_date'] !== $today ) {
            $state['daily_count'] = 0;
            $state['daily_date'] = $today;
            if ( 'daily_cap' === $state['stopped'] ) {
                $state['stopped'] = '';
                $state['last_error'] = '';
            }
        } elseif ( 'daily_cap' === $state['stopped'] ) {
            return;
        }
        $missing = $this->get_ai_drain_targets( $state['skipped'] );
        if ( empty( $missing ) || wp_next_scheduled( 'cpanalyzer_ai_drain' ) ) {
            return;
        }
        $next_at = time() + 60;
        wp_schedule_single_event( $next_at, 'cpanalyzer_ai_drain' );
        $state['active'] = true;
        $state['remaining'] = count( $missing );
        $state['next_at'] = $next_at;
        $state['stopped'] = '';
        $state['last_error'] = '';
        update_option( 'cpanalyzer_ai_drain_state', $state, false );
    }

    /**
     * Process exactly one missing AI suggestion and schedule the next tick.
     */
    public function run_ai_drain() {
        if ( get_transient( 'cpanalyzer_ai_drain_lock' ) ) {
            return;
        }
        set_transient( 'cpanalyzer_ai_drain_lock', 1, 120 );
        try {
            $state = array_merge( array(
                'active'      => true,
                'remaining'   => 0,
                'attempts'    => 0,
                'daily_count' => 0,
                'daily_date'  => '',
                'next_at'     => 0,
                'last_error'  => '',
                'stopped'     => '',
                'skipped'     => array(),
            ), (array) get_option( 'cpanalyzer_ai_drain_state', array() ) );
            $today = gmdate( 'Y-m-d' );
            if ( get_transient( 'cpanalyzer_batch_running' ) ) {
                $next_at = time() + 180;
                wp_schedule_single_event( $next_at, 'cpanalyzer_ai_drain' );
                $state['active'] = true;
                $state['next_at'] = $next_at;
                update_option( 'cpanalyzer_ai_drain_state', $state, false );
                return;
            }
            if ( $state['daily_date'] !== $today ) {
                $state['daily_count'] = 0;
                $state['daily_date'] = $today;
                if ( 'daily_cap' === $state['stopped'] ) {
                    $state['stopped'] = '';
                    $state['last_error'] = '';
                }
            }
            if ( $state['daily_count'] >= 60 ) {
                $state['active'] = false;
                $state['next_at'] = 0;
                $state['stopped'] = 'daily_cap';
                update_option( 'cpanalyzer_ai_drain_state', $state, false );
                return;
            }
            $skipped = ( is_array( $state['skipped'] ) ? $state['skipped'] : array() );
            $missing = $this->get_ai_drain_targets( $skipped );
            if ( empty( $missing ) ) {
                delete_option( 'cpanalyzer_ai_drain_state' );
                wp_clear_scheduled_hook( 'cpanalyzer_ai_drain' );
                return;
            }
            $item = $missing[0];
            $url = $item['url'] ?? '';
            $ai = new CPAP_AI_Processor();
            $result = $ai->process_missing_suggestions( array($item) );
            $error = trim( (string) $ai->get_last_generation_error() );
            $actions = $this->index_actions_by_url( (array) get_option( 'cpanalyzer_page_actions', array() ) );
            $action_produced = $result && !empty( $actions[$url] );
            $state['daily_count']++;
            $state['daily_date'] = $today;
            if ( $action_produced && '' === $error ) {
                $state['attempts'] = 0;
                $state['last_error'] = '';
                $remaining = $this->get_ai_drain_targets( $skipped );
                if ( empty( $remaining ) ) {
                    delete_option( 'cpanalyzer_ai_drain_state' );
                    wp_clear_scheduled_hook( 'cpanalyzer_ai_drain' );
                    return;
                }
                $delay = (int) apply_filters( 'cpanalyzer_ai_drain_interval', 120 );
                $next_at = time() + $delay;
                wp_schedule_single_event( $next_at, 'cpanalyzer_ai_drain' );
                $state['active'] = true;
                $state['remaining'] = count( $remaining );
                $state['next_at'] = $next_at;
                $state['stopped'] = '';
                update_option( 'cpanalyzer_ai_drain_state', $state, false );
                return;
            }
            if ( false !== stripos( $error, 'rate limit' ) ) {
                $state['attempts']++;
                $delay = (int) min( 1800, 300 * pow( 2, min( $state['attempts'] - 1, 2 ) ) );
                $next_at = time() + $delay;
                wp_schedule_single_event( $next_at, 'cpanalyzer_ai_drain' );
                $state['active'] = true;
                $state['remaining'] = count( $missing );
                $state['next_at'] = $next_at;
                $state['last_error'] = $error;
                $state['stopped'] = '';
                update_option( 'cpanalyzer_ai_drain_state', $state, false );
                return;
            }
            if ( false !== stripos( $error, 'too large' ) || false !== stripos( $error, 'maximum context' ) ) {
                if ( '' !== $url && !in_array( $url, $skipped, true ) ) {
                    $skipped[] = $url;
                }
                $state['attempts'] = 0;
                $state['skipped'] = $skipped;
                $state['last_error'] = $error;
                $remaining = $this->get_ai_drain_targets( $skipped );
                if ( empty( $remaining ) ) {
                    delete_option( 'cpanalyzer_ai_drain_state' );
                    wp_clear_scheduled_hook( 'cpanalyzer_ai_drain' );
                    return;
                }
                $delay = (int) apply_filters( 'cpanalyzer_ai_drain_interval', 120 );
                $next_at = time() + $delay;
                wp_schedule_single_event( $next_at, 'cpanalyzer_ai_drain' );
                $state['active'] = true;
                $state['remaining'] = count( $remaining );
                $state['next_at'] = $next_at;
                $state['stopped'] = '';
                update_option( 'cpanalyzer_ai_drain_state', $state, false );
                return;
            }
            if ( '' === $error ) {
                $error = wp_strip_all_tags( (string) get_option( 'cpanalyzer_status_msg', '' ) );
            }
            if ( '' === $error ) {
                $error = __( 'AI suggestions could not be generated.', 'content-performance-analyzer' );
            }
            $state['active'] = false;
            $state['attempts']++;
            $state['remaining'] = count( $missing );
            $state['next_at'] = 0;
            $state['last_error'] = $error;
            $state['stopped'] = 'fatal';
            update_option( 'cpanalyzer_ai_drain_state', $state, false );
        } finally {
            delete_transient( 'cpanalyzer_ai_drain_lock' );
        }
    }

    /**
     * Determine flag type from batch data
     */
    private function determine_flag_from_batch_data( $ga_data, $gsc_data, $content_data ) {
        // Check for decay (declining performance) - this is HIGHEST priority.
        // The classifier already judged this page's own period deltas at
        // processing time — trust it first. Seasonal patterns are expected
        // and never flag.
        $diagnosis_type = $gsc_data['decay_diagnosis']['type'] ?? 'none';
        if ( !in_array( $diagnosis_type, array('none', 'unconfirmed', 'seasonal'), true ) ) {
            return 'decay';
        }
        // A seasonal diagnosis also suppresses the raw GA views-drop decay
        // check below — the drop is explained.
        if ( 'seasonal' === $diagnosis_type ) {
            $ga_data['views_prev'] = 0;
        }
        if ( !empty( $ga_data ) ) {
            $current_views = $ga_data['views'] ?? 0;
            $engagement = $ga_data['engagement'] ?? 0;
            // Compare this page against ITS OWN previous period. (The old code
            // read the first entry of a site-wide array regardless of URL.)
            $views_prev = intval( $ga_data['views_prev'] ?? 0 );
            // Traffic decline detection
            if ( $views_prev > 0 && $current_views > 0 ) {
                $decline_percent = ($views_prev - $current_views) / $views_prev * 100;
                // Flag as decay if traffic dropped by more than 30%
                if ( $decline_percent > 30 ) {
                    return 'decay';
                }
            }
            // Alternative decay indicators when no previous data
            if ( $current_views > 100 && $engagement < 0.15 ) {
                return 'decay';
            }
        }
        // Check for SEO issues (high impressions, low CTR)
        if ( !empty( $gsc_data ) ) {
            $impressions = $gsc_data['impressions'] ?? 0;
            $ctr = $gsc_data['ctr'] ?? 0;
            if ( $impressions > 500 && $ctr < 2 ) {
                return 'seo';
            }
        }
        // Check for conversion issues
        if ( !empty( $content_data ) && !empty( $ga_data ) ) {
            $has_cta = ($content_data['has_cta_buttons']['count'] ?? 0) > 0;
            $has_forms = ($content_data['has_forms']['count'] ?? 0) > 0;
            $views = $ga_data['views'] ?? 0;
            // Pages with decent traffic but no conversion elements
            if ( !$has_cta && !$has_forms && $views > 200 ) {
                return 'conversion';
            }
        }
        // Check for performance issues (PSI-based) - priority after conversion
        if ( !empty( $content_data['psi'] ) ) {
            $psi_data = $content_data['psi'];
            $primary_strategy = $psi_data['primary_strategy'] ?? 'mobile';
            $strategy_data = $psi_data[$primary_strategy] ?? null;
            if ( $strategy_data ) {
                $score = $strategy_data['score'] ?? null;
                $cwv_pass = $strategy_data['core_web_vitals_pass'] ?? true;
                // Flag if fails Core Web Vitals OR score is below 50
                if ( !$cwv_pass || $score !== null && $score < 50 ) {
                    return 'performance';
                }
            }
        }
        // Check for content quality issues
        if ( !empty( $content_data ) ) {
            $word_count = $content_data['word_count'] ?? 0;
            $title_length = $content_data['title']['length'] ?? 0;
            // Thin content or missing title
            if ( $word_count < 300 || $title_length < 10 ) {
                return 'seo';
            }
        }
        return 'none';
    }

    /**
     * AJAX handler to force restart stuck processing
     */
    public function ajax_force_restart_processing() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        // Clear all transients that might be causing stuck processing
        delete_transient( 'cpanalyzer_batch_running' );
        delete_transient( 'cpanalyzer_batch_start_time' );
        delete_transient( 'cpanalyzer_pending_content_analysis' );
        delete_transient( 'cpanalyzer_pending_ai_payload' );
        // Clear any scheduled hooks and reschedule
        wp_clear_scheduled_hook( 'cpanalyzer_batch_process' );
        // Reset any 'in_progress' URLs back to 'pending'
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $reset_count = $wpdb->update( $table_name, array(
            'status' => 'pending',
        ), array(
            'status' => 'in_progress',
        ) );
        if ( !class_exists( 'CPAP_Batch_Processor' ) ) {
            require_once CPANALYZER_PLUGIN_DIR . 'includes/class-cpa-batch-processor.php';
        }
        $batch_processor = new CPAP_Batch_Processor();
        $pending_count = $batch_processor->start_processing_now();
        if ( $pending_count > 0 ) {
            wp_send_json_success( array(
                'message' => sprintf( 
                    /* translators: 1: number of URLs reset; 2: number of pending URLs */
                    __( 'Processing restarted! Reset %1$d stuck URLs and resumed %2$d pending URLs.', 'content-performance-analyzer' ),
                    intval( $reset_count ),
                    intval( $pending_count )
                 ),
            ) );
        }
        update_option( 'cpanalyzer_batch_status', 'idle' );
        wp_send_json_success( array(
            'message' => __( 'No pending URLs were found to restart.', 'content-performance-analyzer' ),
        ) );
    }

    /**
     * AJAX handler to reset all plugin data and start fresh
     */
    public function ajax_reset_all_data() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        // Verify the confirmation code
        $confirmation = ( isset( $_POST['confirmation'] ) ? sanitize_text_field( wp_unslash( $_POST['confirmation'] ) ) : '' );
        if ( $confirmation !== 'RESET-ALL-DATA' ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid confirmation code', 'content-performance-analyzer' ),
            ) );
        }
        global $wpdb;
        try {
            // 1. Clear wp_cpanalyzer_urls table completely
            $table_name = $wpdb->prefix . 'cpanalyzer_urls';
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->query( "TRUNCATE TABLE " . esc_sql( $table_name ) );
            // 2. Clear all WordPress options with cpa_ prefix
            $options_to_clear = array(
                'cpanalyzer_page_flags',
                'cpanalyzer_page_actions',
                'cpanalyzer_ga_rows',
                'cpanalyzer_ga_prev_rows',
                'cpanalyzer_gsc_rows',
                'cpanalyzer_content_data',
                'cpanalyzer_status_msg',
                'cpanalyzer_batch_status',
                'cpanalyzer_last_full_scan',
                'cpanalyzer_url_upload_order'
            );
            foreach ( $options_to_clear as $option ) {
                delete_option( $option );
            }
            // 3. Clear all transients
            $transients_to_clear = array(
                'cpanalyzer_batch_running',
                'cpanalyzer_batch_start_time',
                'cpanalyzer_pending_content_analysis',
                'cpanalyzer_pending_ai_payload'
            );
            foreach ( $transients_to_clear as $transient ) {
                delete_transient( $transient );
            }
            // 4. Clear all scheduled cron jobs
            wp_clear_scheduled_hook( 'cpanalyzer_batch_process' );
            wp_clear_scheduled_hook( 'cpanalyzer_daily_refresh' );
            wp_clear_scheduled_hook( 'cpanalyzer_off_peak_scan' );
            wp_clear_scheduled_hook( 'cpanalyzer_background_content_analysis' );
            wp_clear_scheduled_hook( 'cpanalyzer_cleanup_stuck_processes' );
            // 5. Reschedule essential cron jobs
            if ( !wp_next_scheduled( 'cpanalyzer_daily_refresh' ) ) {
                $timestamp = strtotime( 'tomorrow midnight' );
                wp_schedule_event( $timestamp, 'daily', 'cpanalyzer_daily_refresh' );
            }
            // 6. Set clean status
            update_option( 'cpanalyzer_status_msg', __( 'All data reset successfully. Plugin is ready for fresh processing.', 'content-performance-analyzer' ) );
            update_option( 'cpanalyzer_batch_status', 'idle' );
            wp_send_json_success( array(
                'message' => __( '✅ Complete reset successful! All processed data cleared. You can now upload URLs and start fresh processing.', 'content-performance-analyzer' ),
            ) );
        } catch ( Exception $e ) {
            wp_send_json_error( array(
                'message' => sprintf( __( 'Reset failed: %s', 'content-performance-analyzer' ), $e->getMessage() ),
            ) );
        }
    }

    /**
     * Handle the Google OAuth 2.0 callback (redirected from Google after consent).
     * Hooked to admin_init at priority 1.
     */
    public function handle_oauth_callback() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- State param IS the CSRF token
        if ( !isset( $_GET['cpanalyzer_oauth_callback'] ) || '1' !== $_GET['cpanalyzer_oauth_callback'] ) {
            return;
        }
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Not authorized.', 'content-performance-analyzer' ) );
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- state IS the nonce
        $code = ( isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '' );
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $state = ( isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '' );
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $error = ( isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : '' );
        $redirect = admin_url( 'admin.php?page=cpanalyzer-settings' );
        if ( !empty( $error ) ) {
            wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_error', rawurlencode( $error ), $redirect ) );
            exit;
        }
        if ( empty( $code ) ) {
            wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_error', 'no_code', $redirect ) );
            exit;
        }
        $oauth = new CPAP_Google_OAuth();
        $result = $oauth->exchange_code_for_tokens( $code, $state );
        if ( is_wp_error( $result ) ) {
            wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_error', rawurlencode( $result->get_error_message() ), $redirect ) );
            exit;
        }
        wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_success', '1', $redirect ) );
        exit;
    }

    /**
     * Hosted one-click Google connect: the start leg redirects to the OAuth
     * proxy; the return leg collects the one-time handoff code and stores
     * the tokens. Runs on admin_init before any output.
     */
    public function handle_hosted_oauth() {
        $redirect = admin_url( 'admin.php?page=cpanalyzer-settings' );
        // --- Start leg -------------------------------------------------
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checked via check_admin_referer below.
        if ( isset( $_GET['cpanalyzer_hosted_start'] ) && '1' === $_GET['cpanalyzer_hosted_start'] ) {
            if ( !current_user_can( 'manage_options' ) ) {
                wp_die( esc_html__( 'Not authorized.', 'content-performance-analyzer' ) );
            }
            check_admin_referer( 'cpanalyzer_hosted_start' );
            $oauth = new CPAP_Google_OAuth();
            // External redirect to the proxy — deliberate, so not wp_safe_redirect().
            // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
            wp_redirect( $oauth->get_hosted_start_url() );
            exit;
        }
        // --- Return leg --------------------------------------------------
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- cpa_state IS the CSRF token, verified below.
        if ( !isset( $_GET['cpanalyzer_hosted_callback'] ) || '1' !== $_GET['cpanalyzer_hosted_callback'] ) {
            return;
        }
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Not authorized.', 'content-performance-analyzer' ) );
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $state = ( isset( $_GET['cpa_state'] ) ? sanitize_text_field( wp_unslash( $_GET['cpa_state'] ) ) : '' );
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $code = ( isset( $_GET['cpa_code'] ) ? sanitize_text_field( wp_unslash( $_GET['cpa_code'] ) ) : '' );
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $error = ( isset( $_GET['cpa_error'] ) ? sanitize_text_field( wp_unslash( $_GET['cpa_error'] ) ) : '' );
        $oauth = new CPAP_Google_OAuth();
        if ( !$oauth->verify_hosted_state( $state ) ) {
            wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_error', 'state_mismatch', $redirect ) );
            exit;
        }
        if ( '' !== $error ) {
            wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_error', rawurlencode( $error ), $redirect ) );
            exit;
        }
        if ( '' === $code ) {
            wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_error', 'no_code', $redirect ) );
            exit;
        }
        $result = $oauth->exchange_hosted_code( $code );
        if ( is_wp_error( $result ) ) {
            wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_error', rawurlencode( $result->get_error_message() ), $redirect ) );
            exit;
        }
        wp_safe_redirect( add_query_arg( 'cpanalyzer_oauth_success', '1', $redirect ) );
        exit;
    }

    /**
     * AJAX: list the GA4 properties and Search Console sites the connected
     * Google account can access, so users pick from dropdowns instead of
     * hand-typing a property ID and site URL.
     */
    public function ajax_list_google_properties() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $oauth = new CPAP_Google_OAuth();
        $token = $oauth->get_valid_access_token();
        if ( '' === $token ) {
            wp_send_json_error( array(
                'message' => __( 'Connect your Google account first.', 'content-performance-analyzer' ),
            ) );
        }
        $headers = array(
            'Authorization' => 'Bearer ' . $token,
        );
        // GA4 properties via Admin API account summaries (paginated).
        $properties = array();
        $page_token = '';
        do {
            $endpoint = 'https://analyticsadmin.googleapis.com/v1beta/accountSummaries?pageSize=200';
            if ( '' !== $page_token ) {
                $endpoint .= '&pageToken=' . rawurlencode( $page_token );
            }
            $response = wp_remote_get( $endpoint, array(
                'timeout' => 20,
                'headers' => $headers,
            ) );
            if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
                break;
            }
            $body = json_decode( wp_remote_retrieve_body( $response ), true );
            foreach ( (array) ($body['accountSummaries'] ?? array()) as $account ) {
                $account_name = sanitize_text_field( $account['displayName'] ?? '' );
                foreach ( (array) ($account['propertySummaries'] ?? array()) as $property ) {
                    $property_id = str_replace( 'properties/', '', (string) ($property['property'] ?? '') );
                    if ( '' === $property_id ) {
                        continue;
                    }
                    $properties[] = array(
                        'id'    => sanitize_text_field( $property_id ),
                        'label' => sprintf(
                            '%s — %s (%s)',
                            sanitize_text_field( $property['displayName'] ?? $property_id ),
                            $account_name,
                            $property_id
                        ),
                    );
                }
            }
            $page_token = (string) ($body['nextPageToken'] ?? '');
        } while ( '' !== $page_token );
        // Search Console sites.
        $sites = array();
        $response = wp_remote_get( 'https://www.googleapis.com/webmasters/v3/sites', array(
            'timeout' => 20,
            'headers' => $headers,
        ) );
        if ( !is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
            $body = json_decode( wp_remote_retrieve_body( $response ), true );
            foreach ( (array) ($body['siteEntry'] ?? array()) as $site ) {
                $site_url = sanitize_text_field( $site['siteUrl'] ?? '' );
                if ( '' === $site_url || 'siteUnverifiedUser' === ($site['permissionLevel'] ?? '') ) {
                    continue;
                }
                $sites[] = array(
                    'url'   => $site_url,
                    'label' => $site_url,
                );
            }
        }
        if ( empty( $properties ) && empty( $sites ) ) {
            wp_send_json_error( array(
                'message' => __( 'No GA4 properties or Search Console sites found for the connected account.', 'content-performance-analyzer' ),
            ) );
        }
        wp_send_json_success( array(
            'properties' => $properties,
            'sites'      => $sites,
        ) );
    }

    /**
     * AJAX handler to test Google OAuth connection.
     */
    public function ajax_test_google_connection() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $oauth = new CPAP_Google_OAuth();
        $result = $oauth->test_connection();
        $missing = array();
        if ( empty( get_option( 'cpanalyzer_property_id', '' ) ) ) {
            $missing[] = 'GA4 Property ID';
        }
        if ( empty( get_option( 'cpanalyzer_site_url', '' ) ) ) {
            $missing[] = 'Search Console Site URL';
        }
        if ( $result['success'] && empty( $missing ) ) {
            wp_send_json_success( array(
                'message' => $result['message'],
                'email'   => esc_html( $result['email'] ),
            ) );
        } elseif ( $result['success'] && !empty( $missing ) ) {
            wp_send_json_error( array(
                'message' => sprintf( __( 'Connected as %1$s but missing settings: %2$s', 'content-performance-analyzer' ), esc_html( $result['email'] ), implode( ', ', $missing ) ),
            ) );
        } else {
            wp_send_json_error( array(
                'message' => esc_html( $result['message'] ),
            ) );
        }
    }

    /**
     * AJAX handler: disconnect Google OAuth (revoke tokens).
     */
    public function ajax_google_oauth_disconnect() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $oauth = new CPAP_Google_OAuth();
        $oauth->revoke();
        wp_send_json_success( array(
            'message' => __( 'Google account disconnected successfully.', 'content-performance-analyzer' ),
        ) );
    }

    /**
     * AJAX handler: generate a Content Refresh Brief for a URL (background-safe).
     * Sets URL status to 'in_progress', schedules real work via WP-Cron, returns immediately.
     */
    public function ajax_generate_refresh_brief() {
        check_ajax_referer( 'cpanalyzer_generate_brief', '_wpnonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $url_id = ( isset( $_POST['url_id'] ) ? absint( $_POST['url_id'] ) : 0 );
        if ( !$url_id ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid URL ID.', 'content-performance-analyzer' ),
            ) );
        }
        global $wpdb;
        $table = $wpdb->prefix . 'cpanalyzer_urls';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $url_id ) );
        if ( !$row ) {
            wp_send_json_error( array(
                'message' => __( 'URL not found.', 'content-performance-analyzer' ),
            ) );
        }
        // Run synchronously (PHP execution time is typically 30 s on most hosts).
        // For very slow hosts the cron approach can be adopted later.
        $ai = new CPAP_AI_Processor();
        $url_data = array(
            'url'          => $row->url,
            'ga_data'      => json_decode( $row->ga_data ?? '{}', true ),
            'gsc_data'     => json_decode( $row->gsc_data ?? '{}', true ),
            'content_data' => json_decode( $row->content_data ?? '{}', true ),
        );
        $brief = $ai->generate_content_refresh_brief( $url_data );
        if ( !$brief ) {
            wp_send_json_error( array(
                'message' => __( 'Brief generation failed. Ensure the AI API key is configured and the URL has been scanned.', 'content-performance-analyzer' ),
            ) );
        }
        // Merge brief into existing ai_suggestions preserving legacy data.
        $existing = json_decode( $row->ai_suggestions ?? '{}', true );
        if ( !is_array( $existing ) ) {
            $existing = array();
        }
        $existing['content_refresh_brief'] = $brief;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update( $table, array(
            'ai_suggestions' => wp_json_encode( $existing ),
        ), array(
            'id' => $url_id,
        ) );
        wp_send_json_success( array(
            'message'    => __( 'Content Refresh Brief generated!', 'content-performance-analyzer' ),
            'brief'      => $brief,
            'url'        => $row->url,
            'view_nonce' => wp_create_nonce( 'cpanalyzer_get_brief' ),
        ) );
    }

    /**
     * AJAX handler: retrieve a stored Content Refresh Brief.
     */
    public function ajax_get_refresh_brief() {
        check_ajax_referer( 'cpanalyzer_get_brief', '_wpnonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $url_id = ( isset( $_POST['url_id'] ) ? absint( $_POST['url_id'] ) : 0 );
        if ( !$url_id ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid URL ID.', 'content-performance-analyzer' ),
            ) );
        }
        global $wpdb;
        $table = $wpdb->prefix . 'cpanalyzer_urls';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT ai_suggestions, url FROM {$table} WHERE id = %d", $url_id ) );
        if ( !$row ) {
            wp_send_json_error( array(
                'message' => __( 'URL not found.', 'content-performance-analyzer' ),
            ) );
        }
        $suggestions = json_decode( $row->ai_suggestions ?? '{}', true );
        $brief = $suggestions['content_refresh_brief'] ?? null;
        if ( !$brief ) {
            wp_send_json_error( array(
                'message' => __( 'No brief found for this URL. Generate one first.', 'content-performance-analyzer' ),
            ) );
        }
        wp_send_json_success( array(
            'brief' => $brief,
            'url'   => esc_url( $row->url ),
        ) );
    }

    /**
     * AJAX handler to validate PSI API key
     */
    public function ajax_validate_psi_key() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $api_key = ( isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : '' );
        if ( empty( $api_key ) ) {
            wp_send_json_error( array(
                'message' => __( 'API key is required', 'content-performance-analyzer' ),
            ) );
        }
        try {
            $psi_collector = new CPAP_PageSpeed_Collector();
            $result = $psi_collector->validate_api_key( $api_key );
            if ( $result['valid'] ) {
                wp_send_json_success( array(
                    'message' => $result['message'],
                ) );
            } else {
                wp_send_json_error( array(
                    'message' => $result['message'],
                ) );
            }
        } catch ( Exception $e ) {
            wp_send_json_error( array(
                'message' => sprintf( __( 'Validation failed: %s', 'content-performance-analyzer' ), $e->getMessage() ),
            ) );
        }
    }

    /**
     * AJAX handler to manually trigger data collection
     */
    public function ajax_trigger_data_collection() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        try {
            // Load data collector
            if ( !class_exists( 'CPAP_Data_Collector' ) ) {
                require_once CPANALYZER_PLUGIN_DIR . 'includes/class-cpa-data-collector.php';
            }
            $data_collector = new CPAP_Data_Collector();
            $data_collector->init();
            // Run data collection directly
            $result = $data_collector->collect_and_analyse();
            if ( $result ) {
                wp_send_json_success( array(
                    'message' => __( 'Data collection completed successfully! GA4 and GSC data should now be available.', 'content-performance-analyzer' ),
                ) );
            } else {
                wp_send_json_error( array(
                    'message' => __( 'Data collection failed. Check the debug log for details.', 'content-performance-analyzer' ),
                ) );
            }
        } catch ( Exception $e ) {
            wp_send_json_error( array(
                'message' => sprintf( __( 'Data collection failed: %s', 'content-performance-analyzer' ), $e->getMessage() ),
            ) );
        }
    }

    /**
     * Log sync event
     */
    /**
     * Test PSI API with first available URL
     */
    public function ajax_test_psi_api() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        // Load PSI collector
        if ( !class_exists( 'CPAP_PageSpeed_Collector' ) ) {
            require_once CPANALYZER_PLUGIN_DIR . 'includes/class-cpa-pagespeed-collector.php';
        }
        $psi_collector = new CPAP_PageSpeed_Collector();
        // Check if PSI is enabled
        if ( !$psi_collector->is_enabled() ) {
            wp_send_json_error( array(
                'message' => __( 'PSI is not enabled or configured properly', 'content-performance-analyzer' ),
            ) );
        }
        // Test with homepage first as it's usually faster than product pages
        $flags = get_option( 'cpanalyzer_page_flags', array() );
        if ( empty( $flags ) ) {
            // Fallback to Google if no URLs uploaded
            $test_url = 'https://www.google.com';
            $full_url = $test_url;
        } else {
            // Try to find homepage first, otherwise use first URL
            $homepage_found = false;
            foreach ( $flags as $url => $flag ) {
                if ( $url === '/' || $url === '/index.php' || $url === '/home' ) {
                    $test_url = $url;
                    $full_url = home_url( $test_url );
                    $homepage_found = true;
                    break;
                }
            }
            if ( !$homepage_found ) {
                // Use first URL as fallback
                $test_url = array_keys( $flags )[0];
                $full_url = home_url( $test_url );
            }
        }
        // Add debug info about PSI configuration
        $psi_settings = array(
            'enabled'        => get_option( 'cpanalyzer_pagespeed_enable', 'no' ),
            'api_key_length' => strlen( get_option( 'cpanalyzer_pagespeed_api_key', '' ) ),
            'strategy'       => get_option( 'cpanalyzer_pagespeed_strategy', 'mobile' ),
            'categories'     => get_option( 'cpanalyzer_pagespeed_categories', ['PERFORMANCE'] ),
            'rate_limit'     => get_option( 'cpanalyzer_pagespeed_rate_limit', 30 ),
            'test_url'       => $test_url,
            'ssl_verify'     => get_option( 'cpanalyzer_verify_ssl', 'yes' ) !== 'no',
        );
        // Test basic connectivity to PSI API first
        $connectivity_test = wp_remote_get( 'https://www.googleapis.com/pagespeedonline/v5/', array(
            'timeout'   => 5,
            'sslverify' => get_option( 'cpanalyzer_verify_ssl', 'yes' ) !== 'no',
        ) );
        if ( is_wp_error( $connectivity_test ) ) {
            wp_send_json_error( array(
                'message'            => sprintf( 
                    /* translators: %s: WordPress HTTP API error message */
                    __( 'Cannot connect to PSI API: %s', 'content-performance-analyzer' ),
                    $connectivity_test->get_error_message()
                 ),
                'settings'           => $psi_settings,
                'connectivity_issue' => true,
            ) );
        }
        // Test PSI API
        $start_time = microtime( true );
        // Use the FULL URL for PSI API call
        $psi_data = $psi_collector->collect_pagespeed_data( $full_url );
        $end_time = microtime( true );
        if ( $psi_data && !isset( $psi_data['error'] ) ) {
            // Success - return data preview
            $primary_strategy = $psi_data['primary_strategy'] ?? 'mobile';
            $strategy_data = $psi_data[$primary_strategy] ?? array();
            $score = $strategy_data['score'] ?? null;
            $cwv_pass = $strategy_data['core_web_vitals_pass'] ?? null;
            $metrics = $strategy_data['metrics'] ?? array();
            wp_send_json_success( array(
                'message'        => __( 'PSI API test successful!', 'content-performance-analyzer' ),
                'url'            => $full_url,
                'response_time'  => round( $end_time - $start_time, 2 ) . 's',
                'score'          => $score,
                'cwv_pass'       => $cwv_pass,
                'metrics'        => $metrics,
                'settings'       => $psi_settings,
                'data_structure' => __( 'PSI data collected and formatted correctly', 'content-performance-analyzer' ),
            ) );
        } else {
            $error_msg = ( isset( $psi_data['error'] ) ? $psi_data['error'] : 'Unknown error' );
            wp_send_json_error( array(
                'message'       => sprintf( 
                    /* translators: %s: PSI API error message */
                    __( 'PSI API test failed: %s', 'content-performance-analyzer' ),
                    $error_msg
                 ),
                'url'           => $full_url,
                'response_time' => round( $end_time - $start_time, 2 ) . 's',
                'settings'      => $psi_settings,
            ) );
        }
    }

    /**
     * Check current batch processing and PSI status
     */
    public function ajax_check_status() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // Check batch table
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $batch_total = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $batch_pending = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'pending'" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $batch_success = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'success'" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $batch_failed = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'failed'" );
        // Check PSI settings
        $psi_enabled = get_option( 'cpanalyzer_pagespeed_enable', 'no' );
        $psi_api_key = get_option( 'cpanalyzer_pagespeed_api_key', '' );
        // Check insights display data
        $flags = get_option( 'cpanalyzer_page_flags', array() );
        $content_data = get_option( 'cpanalyzer_content_data', array() );
        $psi_count = 0;
        foreach ( $content_data as $data ) {
            if ( !empty( $data['psi'] ) ) {
                $psi_count++;
            }
        }
        wp_send_json_success( array(
            'batch_table'      => array(
                'total'   => intval( $batch_total ),
                'pending' => intval( $batch_pending ),
                'success' => intval( $batch_success ),
                'failed'  => intval( $batch_failed ),
            ),
            'psi_settings'     => array(
                'enabled'    => $psi_enabled,
                'has_key'    => !empty( $psi_api_key ),
                'key_length' => strlen( $psi_api_key ),
            ),
            'insights_display' => array(
                'total_urls'    => count( $flags ),
                'urls_with_psi' => $psi_count,
            ),
        ) );
    }

    /**
     * Reprocess a specific uploaded URL and rebuild GA4, GSC, content, PSI, and AI for that row only.
     */
    public function ajax_reprocess_one_url() {
        check_ajax_referer( 'cpanalyzer_batch_nonce', 'nonce' );
        if ( !current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Not authorized', 'content-performance-analyzer' ),
            ) );
        }
        $url = ( isset( $_POST['url'] ) ? cpanalyzer_normalize_path( sanitize_text_field( wp_unslash( $_POST['url'] ) ) ) : '' );
        if ( empty( $url ) ) {
            wp_send_json_error( array(
                'message' => __( 'No URL was provided for reprocessing.', 'content-performance-analyzer' ),
            ) );
        }
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $existing_url = $wpdb->get_var( $wpdb->prepare( "SELECT url FROM " . esc_sql( $table_name ) . " WHERE url = %s LIMIT 1", $url ) );
        if ( !$existing_url ) {
            wp_send_json_error( array(
                'message' => __( 'The selected URL was not found in the uploaded URL list.', 'content-performance-analyzer' ),
            ) );
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $reset_result = $wpdb->update(
            $table_name,
            array(
                'status'         => 'pending',
                'last_scanned'   => null,
                'scan_result'    => null,
                'error_message'  => null,
                'ga_data'        => null,
                'gsc_data'       => null,
                'content_data'   => null,
                'ai_suggestions' => null,
                'retry_count'    => 0,
            ),
            array(
                'url' => $url,
            ),
            array(
                '%s',
                '%s',
                '%s',
                '%s',
                '%s',
                '%s',
                '%s',
                '%s',
                '%d'
            ),
            array('%s')
        );
        if ( $reset_result === false ) {
            wp_send_json_error( array(
                'message' => __( 'Failed to reset the selected URL for reprocessing.', 'content-performance-analyzer' ),
            ) );
        }
        $this->clear_insights_display_cache_for_url( $url );
        if ( !class_exists( 'CPAP_Batch_Processor' ) ) {
            require_once CPANALYZER_PLUGIN_DIR . 'includes/class-cpa-batch-processor.php';
        }
        $batch_processor = new CPAP_Batch_Processor();
        $result = $batch_processor->reprocess_url_now( $url );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array(
                'message' => sprintf( 
                    /* translators: %s: error message from row reprocess */
                    __( 'Reprocessing failed: %s', 'content-performance-analyzer' ),
                    $result->get_error_message()
                 ),
            ) );
        }
        $message = ( !empty( $result['has_psi'] ) ? __( 'URL rescanned successfully. PSI and AI were refreshed for this page.', 'content-performance-analyzer' ) : __( 'URL rescanned, but PSI is still unavailable for this page. Review the PSI error and try again if needed.', 'content-performance-analyzer' ) );
        wp_send_json_success( array(
            'message'   => $message,
            'url'       => $result['url'] ?? $url,
            'has_psi'   => !empty( $result['has_psi'] ),
            'psi_error' => $result['psi_error'] ?? '',
        ) );
    }

    private function log_sync_event(
        $total_urls,
        $flags_count,
        $ga_count,
        $content_count
    ) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'cpanalyzer_urls';
        // Get current counts from database
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $db_total = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $db_success = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'success'" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $db_pending = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'pending'" );
        // Report the WHOLE job, not just data collection. A green "Sync
        // Complete" while every flagged page is still missing its AI plan
        // reads as "done" when it isn't — the run is only complete when the
        // AI step is done too (or was never going to run).
        $data_summary = sprintf(
            /* translators: 1: total URLs; 2: pending; 3: processed; 4: visible in insights; 5: GA count; 6: content count */
            __( '%1$d total URLs in database (%2$d pending, %3$d processed) → %4$d visible in insights | GA: %5$d | Content: %6$d', 'content-performance-analyzer' ),
            $db_total,
            $db_pending,
            $db_success,
            $flags_count,
            $ga_count,
            $content_count
        );
        $awaiting_ai = count( $this->get_missing_ai_payload() );
        $ai_ready = ( function_exists( 'cpanalyzer_ai_is_configured' ) ? cpanalyzer_ai_is_configured() : false );
        if ( $awaiting_ai < 1 ) {
            $message = sprintf( 
                /* translators: %s: data collection summary */
                __( '✅ Sync complete — data and AI recommendations are both done: %s', 'content-performance-analyzer' ),
                $data_summary
             );
        } elseif ( !$ai_ready ) {
            $message = sprintf( 
                /* translators: 1: number of pages without AI plans; 2: data collection summary */
                _n(
                    '⚠️ Traffic data collected, but AI recommendations are off, so %1$d flagged page has no fix plan. %2$s',
                    '⚠️ Traffic data collected, but AI recommendations are off, so %1$d flagged pages have no fix plan. %2$s',
                    $awaiting_ai,
                    'content-performance-analyzer'
                ),
                $awaiting_ai,
                $data_summary
             );
        } else {
            $message = sprintf( 
                /* translators: 1: number of pages awaiting AI; 2: data collection summary */
                _n(
                    '⏳ Traffic data collected. %1$d flagged page is still waiting on its AI fix plan. %2$s',
                    '⏳ Traffic data collected. %1$d flagged pages are still waiting on their AI fix plans. %2$s',
                    $awaiting_ai,
                    'content-performance-analyzer'
                ),
                $awaiting_ai,
                $data_summary
             );
        }
        update_option( 'cpanalyzer_status_msg', $message );
    }

}
