<?php
if (!defined('ABSPATH')) { exit; }

/**
 * SAFELY REPLACEABLE FILE
 *
 * Backward compatible with your existing class:
 * - class name unchanged: MYAIO_SEO_Migrator
 * - public methods unchanged: migrate_all($limit=0), get_report()
 *
 * Adds:
 * - sitemap settings migration (Rank Math / Yoast / AIOSEO best-effort)
 * - MYAIO sitemap server so sitemap keeps working after provider deactivation:
 *   - /sitemap_index.xml
 *   - /post-sitemap.xml
 *   - /post-sitemap1.xml (Yoast-style paging)
 */
class MYAIO_SEO_Migrator {

    /** Sanitize a DB identifier for safe interpolation (tables/columns). */
    private static function sanitize_db_identifier( $identifier ) {
        $identifier = (string) $identifier;
        return preg_match( '/^[A-Za-z0-9_]+$/', $identifier ) ? $identifier : '';
    }

    private static function normalize_robots_value( $value ) {
        $allowed = [
            'index', 'noindex', 'follow', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex',
        ];

        $push_tokens = static function( &$tokens, $raw ) use ( $allowed ) {
            if ( ! is_scalar( $raw ) ) {
                return;
            }

            $raw = strtolower( trim( wp_strip_all_tags( (string) $raw ) ) );
            if ( $raw === '' ) {
                return;
            }

            foreach ( preg_split( '/\s*,\s*|\s+/', $raw ) as $part ) {
                $part = trim( (string) $part );
                if ( $part === '' ) {
                    continue;
                }

                if ( in_array( $part, $allowed, true ) || preg_match( '/^(?:max-snippet:-?\d+|max-image-preview:(?:none|standard|large)|max-video-preview:-?\d+|unavailable_after:.+)$/', $part ) ) {
                    $tokens[] = $part;
                }
            }
        };

        $is_truthy = static function( $raw ) {
            if ( is_bool( $raw ) ) {
                return $raw;
            }
            if ( is_int( $raw ) || is_float( $raw ) ) {
                return (int) $raw === 1;
            }
            if ( is_string( $raw ) ) {
                $raw = strtolower( trim( $raw ) );
                return in_array( $raw, [ '1', 'true', 'yes', 'on', 'default', 'inherit', 'index', 'follow', 'enabled' ], true );
            }
            return false;
        };

        $walk = null;
        $walk = static function( $item, &$tokens ) use ( &$walk, $push_tokens, $is_truthy, $allowed ) {
            $item = maybe_unserialize( $item );
            if ( is_string( $item ) ) {
                $json = json_decode( $item, true );
                if ( json_last_error() === JSON_ERROR_NONE && is_array( $json ) ) {
                    $item = $json;
                }
            }

            if ( is_array( $item ) ) {
                foreach ( $item as $k => $v ) {
                    $key = strtolower( trim( (string) $k ) );

                    if ( ! is_int( $k ) && in_array( $key, $allowed, true ) ) {
                        if ( is_array( $v ) ) {
                            $before = count( $tokens );
                            $walk( $v, $tokens );
                            if ( count( $tokens ) === $before && isset( $v['value'] ) && $is_truthy( $v['value'] ) ) {
                                $tokens[] = $key;
                            }
                        } elseif ( $is_truthy( $v ) || strtolower( trim( (string) $v ) ) === $key ) {
                            $tokens[] = $key;
                        }
                        continue;
                    }

                    $walk( $v, $tokens );
                }
                return;
            }

            $push_tokens( $tokens, $item );
        };

        $tokens = [];
        $walk( $value, $tokens );
        $tokens = array_values( array_unique( array_filter( array_map( 'trim', $tokens ) ) ) );

        if ( in_array( 'noindex', $tokens, true ) ) {
            $tokens = array_values( array_diff( $tokens, [ 'index' ] ) );
        } elseif ( in_array( 'index', $tokens, true ) && ! in_array( 'follow', $tokens, true ) && ! in_array( 'nofollow', $tokens, true ) ) {
            $tokens[] = 'follow';
        }

        if ( in_array( 'nofollow', $tokens, true ) ) {
            $tokens = array_values( array_diff( $tokens, [ 'follow' ] ) );
        } elseif ( in_array( 'follow', $tokens, true ) === false && ( in_array( 'index', $tokens, true ) || in_array( 'noindex', $tokens, true ) ) ) {
            $tokens[] = 'follow';
        }

        if ( ! in_array( 'index', $tokens, true ) && ! in_array( 'noindex', $tokens, true ) && ( in_array( 'follow', $tokens, true ) || in_array( 'nofollow', $tokens, true ) ) ) {
            array_unshift( $tokens, 'index' );
        }

        $preferred_order = [ 'noindex', 'index', 'nofollow', 'follow', 'noarchive', 'nosnippet', 'noimageindex' ];
        usort( $tokens, static function( $a, $b ) use ( $preferred_order ) {
            $pa = array_search( $a, $preferred_order, true );
            $pb = array_search( $b, $preferred_order, true );
            $pa = ( $pa === false ) ? 999 : $pa;
            $pb = ( $pb === false ) ? 999 : $pb;
            if ( $pa === $pb ) {
                return strcmp( $a, $b );
            }
            return $pa <=> $pb;
        } );

        return implode( ',', $tokens );
    }

const REPORT_OPTION = 'myaio_seo_migration_report';

    // NEW: MYAIO sitemap settings (used by the sitemap server in this file)
    const SITEMAP_OPTION = 'myaio_sitemap_settings';

    // NEW: internal flag to flush rewrite rules once after migration
    const SITEMAP_FLUSH_FLAG = 'myaio_sitemap_flush_needed';

    /**
     * Detect + migrate SEO data into MYAIO-owned meta keys / tables.
     * ALSO migrates sitemap settings into MYAIO and enables MYAIO sitemap server.
     */
    public static function migrate_all($limit = 0) {
        global $wpdb;

        $report = [
            'detected' => [],
            'counts' => [
                'posts_scanned' => 0,
                'meta_migrated' => 0,
                'redirects_migrated' => 0,
                'sitemap_settings_migrated' => 0, // NEW
            ],
            'warnings' => [],
            'details' => [
                'providers_meta' => [],
                'providers_redirects' => [],
                'providers_sitemaps' => [],       // NEW
                'sitemap_effective' => null,      // NEW
            ],
            'ran_at' => gmdate('c'),
        ];

        // 1) Detect providers by meta-key footprint.
        $providers = self::detect_providers();
        $report['detected'] = $providers;

        if (empty($providers)) {
            $report['warnings'][] = 'No known SEO plugin footprints found in postmeta (Rank Math / Yoast / AIOSEO).';
        }

        // 2) Migrate per-post meta. We scan only posts that actually have SEO meta.
        $post_ids = self::get_candidate_post_ids($limit);
        $report['counts']['posts_scanned'] = count($post_ids);

        foreach ($post_ids as $post_id) {
            $migrated = 0;

            // Rank Math
            $migrated += self::migrate_rank_math_meta($post_id, $report);

            // Yoast
            $migrated += self::migrate_yoast_meta($post_id, $report);

            // AIOSEO
            $migrated += self::migrate_aioseo_meta($post_id, $report);

            $report['counts']['meta_migrated'] += $migrated;
        }

        // 3) Migrate redirects (best-effort). Only Rank Math is implemented robustly here (kept same).
        $redirects_result = 0;
        $redirects_result += self::migrate_rank_math_redirects($report);
        $redirects_result += self::migrate_aioseo_redirects($report);
        $redirects_result += self::migrate_seopress_redirects($report);
        $report['counts']['redirects_migrated'] = $redirects_result;

        // 4) NEW: Migrate sitemap settings into MYAIO + enable MYAIO sitemap server.
        $sitemap_migrated = self::migrate_sitemap_settings($providers, $report);
        $report['counts']['sitemap_settings_migrated'] = $sitemap_migrated;

        // 5) Persist report for UI + for gating uninstall.
        update_option(self::REPORT_OPTION, $report, false);

        // If sitemap settings were migrated/changed, flush rewrites once (safe; migration is not called every request).
        if ($sitemap_migrated > 0) {
            update_option(self::SITEMAP_FLUSH_FLAG, 1, false);
            // If called in admin/CLI, flush now too.
            if (function_exists('flush_rewrite_rules')) {
                flush_rewrite_rules(false);
            }
        }

    return $report;
}

/**
 * Incremental meta migration for large sites / shared hosting.
 * Processes a batch of candidate posts (by post_id) and stores progress in options.
 *
 * Options used:
 *  - myaio_seo_migration_last_id (int)
 *  - myaio_seo_migration_providers (array)
 *  - myaio_seo_migration_counts (array)
 *  - myaio_seo_migration_meta_done (int 0/1)
 */
public static function migrate_meta_batch( $batch = 200, $max_seconds = 10 ) {
    global $wpdb;

    $batch = (int) $batch;
    if ( $batch <= 0 ) { $batch = 200; }

    $max_seconds = (int) $max_seconds;
    if ( $max_seconds <= 0 ) { $max_seconds = 10; }

    $started = microtime( true );

    // Providers (detected once)
    $providers = get_option( 'myaio_seo_migration_providers' );
    if ( ! is_array( $providers ) ) {
        $providers = self::detect_providers();
        update_option( 'myaio_seo_migration_providers', $providers, false );
    }

    // Cumulative counts
    $counts = get_option( 'myaio_seo_migration_counts' );
    if ( ! is_array( $counts ) ) {
        $counts = [
            'posts_scanned' => 0,
            'meta_migrated' => 0,
        ];
    }

    $last_id = (int) get_option( 'myaio_seo_migration_last_id', 0 );

    // Fetch a batch of candidate post IDs greater than last_id
    $cache_key = 'myaio_migr_meta_ids_' . md5( (string) $last_id . '|' . (string) $batch );
    $ids = wp_cache_get( $cache_key, 'myaio' );

    if ( false === $ids ) {
        $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $wpdb->prepare(
                "
        SELECT DISTINCT post_id
        FROM {$wpdb->postmeta}
        WHERE post_id > %d
          AND (
                meta_key LIKE %s
             OR meta_key LIKE %s
             OR meta_key LIKE %s
          )
        ORDER BY post_id ASC
        LIMIT %d
                ",
                $last_id,
                'rank_math_%',
                '_yoast_wpseo_%',
                '%aioseo%',
                $batch
            )
        );
        $ids = array_map( 'intval', $ids ?: [] );
        wp_cache_set( $cache_key, $ids, 'myaio', 60 );
    } else {
        $ids = array_map( 'intval', is_array( $ids ) ? $ids : [] );
    }
$ids = array_map( 'intval', $ids ?: [] );

    if ( empty( $ids ) ) {
        update_option( 'myaio_seo_migration_meta_done', 1, false );
        return [ 'done' => true, 'processed' => 0, 'last_id' => $last_id ];
    }

    $report_stub = [
        'details' => [ 'providers_meta' => [] ],
        'warnings' => [],
    ];

    $processed = 0;

    foreach ( $ids as $post_id ) {
        $migrated = 0;

        $migrated += self::migrate_rank_math_meta( $post_id, $report_stub );
        $migrated += self::migrate_yoast_meta( $post_id, $report_stub );
        $migrated += self::migrate_aioseo_meta( $post_id, $report_stub );

        $counts['posts_scanned'] += 1;
        $counts['meta_migrated'] += (int) $migrated;

        $last_id = $post_id;
        $processed++;

        if ( ( microtime( true ) - $started ) >= $max_seconds ) {
            break;
        }
    }

    update_option( 'myaio_seo_migration_last_id', $last_id, false );
    update_option( 'myaio_seo_migration_counts', $counts, false );

    return [ 'done' => false, 'processed' => $processed, 'last_id' => $last_id ];
}

/**
 * Finalize incremental migration (redirects + sitemap settings + report).
 * Safe to call multiple times.
 */
public static function finalize_migration() {

    // Avoid running twice
    if ( (int) get_option( 'myaio_seo_migration_final_done', 0 ) === 1 ) {
        return;
    }

    $providers = get_option( 'myaio_seo_migration_providers' );
    if ( ! is_array( $providers ) ) {
        $providers = self::detect_providers();
    }

    $counts = get_option( 'myaio_seo_migration_counts' );
    if ( ! is_array( $counts ) ) {
        $counts = [
            'posts_scanned' => 0,
            'meta_migrated' => 0,
        ];
    }

    $report = [
        'detected' => $providers,
        'counts' => [
            'posts_scanned' => (int) ( $counts['posts_scanned'] ?? 0 ),
            'meta_migrated' => (int) ( $counts['meta_migrated'] ?? 0 ),
            'redirects_migrated' => 0,
            'sitemap_settings_migrated' => 0,
        ],
        'warnings' => [],
        'details' => [
            'providers_meta' => [],
            'providers_redirects' => [],
            'providers_sitemaps' => [],
            'sitemap_effective' => null,
        ],
        'ran_at' => gmdate( 'c' ),
    ];

    // Redirects + sitemap settings (single run)
    $redirects_result = 0;
    $redirects_result += self::migrate_rank_math_redirects( $report );
    $redirects_result += self::migrate_aioseo_redirects( $report );
    $redirects_result += self::migrate_seopress_redirects( $report );
    $report['counts']['redirects_migrated'] = $redirects_result;

    $sitemap_migrated = self::migrate_sitemap_settings( $providers, $report );
    $report['counts']['sitemap_settings_migrated'] = $sitemap_migrated;

    update_option( self::REPORT_OPTION, $report, false );

    if ( $sitemap_migrated > 0 ) {
        update_option( self::SITEMAP_FLUSH_FLAG, 1, false );
        if ( function_exists( 'flush_rewrite_rules' ) ) {
            flush_rewrite_rules( false );
        }
    }

    update_option( 'myaio_seo_migration_final_done', 1, false );
}

public static function get_report() {
        $r = get_option(self::REPORT_OPTION);
        return is_array($r) ? $r : null;
    }

    /* =========================================================
     * Provider detection (unchanged)
     * ========================================================= */
    private static function detect_providers() {
        global $wpdb;
        $found = [];

        $patterns = [
            'rank_math' => 'rank_math_%',
            'yoast'     => '_yoast_wpseo_%',
            'aioseo'    => '%aioseo%',
        ];

        foreach ($patterns as $name => $like) {
            $cnt = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
                "SELECT COUNT(1) FROM {$wpdb->postmeta} WHERE meta_key LIKE %s",
                $like
            ));
            if ($cnt > 0) {
                $found[] = $name;
            }
        }

        return $found;
    }

    private static function get_candidate_post_ids( $limit = 0 ) {
        global $wpdb;

        $limit = ( $limit && is_numeric( $limit ) && (int) $limit > 0 ) ? (int) $limit : 0;

        if ( $limit > 0 ) {
            $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
                $wpdb->prepare(
                    "SELECT DISTINCT post_id
                     FROM {$wpdb->postmeta}
                     WHERE meta_key LIKE %s
                        OR meta_key LIKE %s
                        OR meta_key LIKE %s
                     ORDER BY post_id ASC
                     LIMIT %d",
                    'rank_math_%',
                    '_yoast_wpseo_%',
                    '%aioseo%',
                    $limit
                )
            );
        } else {
            $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
                $wpdb->prepare(
                    "SELECT DISTINCT post_id
                     FROM {$wpdb->postmeta}
                     WHERE meta_key LIKE %s
                        OR meta_key LIKE %s
                        OR meta_key LIKE %s
                     ORDER BY post_id ASC",
                    'rank_math_%',
                    '_yoast_wpseo_%',
                    '%aioseo%'
                )
            );
        }

        return array_map( 'intval', $ids ?: [] );
    }
    /* =========================================================
     * Meta migration (kept compatible; extended with social/schema raw capture)
     * ========================================================= */

    private static function migrate_rank_math_meta($post_id, &$report) {
        $migrated = 0;

        $map = [
            'rank_math_title'         => '_myaio_seo_title',
            'rank_math_description'   => '_myaio_seo_description',
            'rank_math_canonical_url' => '_myaio_seo_canonical',
        ];

        foreach ($map as $from => $to) {
            $val = get_post_meta($post_id, $from, true);
            if ($val !== '' && $val !== null) {
                update_post_meta($post_id, $to, $val);
                // Keep legacy compatibility keys in sync
                if ($to === '_myaio_seo_description') {
                    update_post_meta($post_id, '_myaio_meta_description', $val);
                }
                $migrated++;
                $report['details']['providers_meta']['rank_math'] = true;
            }
        }

        $rm_robots = self::normalize_robots_value( get_post_meta( $post_id, 'rank_math_robots', true ) );
        if ( $rm_robots !== '' ) {
            update_post_meta( $post_id, '_myaio_seo_robots', $rm_robots );
            $migrated++;
            $report['details']['providers_meta']['rank_math'] = true;
        }

        // Schema blob (existing behavior)
        $schema = get_post_meta($post_id, 'rank_math_schema_Builder', true);
        if (!empty($schema)) {
            update_post_meta($post_id, '_myaio_seo_schema_raw', $schema);
            $migrated++;
        }

        // Social (OG/Twitter) migration
        $rm_og_title = get_post_meta($post_id, 'rank_math_facebook_title', true);
        $rm_og_desc  = get_post_meta($post_id, 'rank_math_facebook_description', true);
        $rm_og_img   = get_post_meta($post_id, 'rank_math_facebook_image', true);

        $rm_tw_title = get_post_meta($post_id, 'rank_math_twitter_title', true);
        $rm_tw_desc  = get_post_meta($post_id, 'rank_math_twitter_description', true);
        $rm_tw_img   = get_post_meta($post_id, 'rank_math_twitter_image', true);
        $rm_tw_card  = get_post_meta($post_id, 'rank_math_twitter_card_type', true);

        if ($rm_og_title !== '' && $rm_og_title !== null) { update_post_meta($post_id, '_myaio_og_title', $rm_og_title); $migrated++; }
        if ($rm_og_desc  !== '' && $rm_og_desc  !== null) { update_post_meta($post_id, '_myaio_og_description', $rm_og_desc); $migrated++; }
        if ($rm_og_img   !== '' && $rm_og_img   !== null) { update_post_meta($post_id, '_myaio_og_image', $rm_og_img); $migrated++; }

        if ($rm_tw_title !== '' && $rm_tw_title !== null) { update_post_meta($post_id, '_myaio_twitter_title', $rm_tw_title); $migrated++; }
        if ($rm_tw_desc  !== '' && $rm_tw_desc  !== null) { update_post_meta($post_id, '_myaio_twitter_description', $rm_tw_desc); $migrated++; }
        if ($rm_tw_img   !== '' && $rm_tw_img   !== null) { update_post_meta($post_id, '_myaio_twitter_image', $rm_tw_img); $migrated++; }
        if ($rm_tw_card  !== '' && $rm_tw_card  !== null) { update_post_meta($post_id, '_myaio_twitter_card', $rm_tw_card); $migrated++; }

        // -----------------------------------------------------------------
        // Rank Math often outputs OG/Twitter tags even when *no* explicit
        // social overrides exist (it falls back to SEO title/description).
        // We want parity AFTER migration without doing runtime inheritance.
        // So we "materialize" those fallbacks ONCE during migration.
        // -----------------------------------------------------------------
        $seo_title = get_post_meta($post_id, '_myaio_seo_title', true);
        $seo_desc  = get_post_meta($post_id, '_myaio_seo_description', true);

        if ((get_post_meta($post_id, '_myaio_og_title', true) === '') && !empty($seo_title)) {
            update_post_meta($post_id, '_myaio_og_title', $seo_title);
            $migrated++;
        }
        if ((get_post_meta($post_id, '_myaio_og_description', true) === '') && !empty($seo_desc)) {
            update_post_meta($post_id, '_myaio_og_description', $seo_desc);
            $migrated++;
        }
        if ((get_post_meta($post_id, '_myaio_twitter_title', true) === '') && !empty($seo_title)) {
            update_post_meta($post_id, '_myaio_twitter_title', $seo_title);
            $migrated++;
        }
        if ((get_post_meta($post_id, '_myaio_twitter_description', true) === '') && !empty($seo_desc)) {
            update_post_meta($post_id, '_myaio_twitter_description', $seo_desc);
            $migrated++;
        }
        // Rank Math commonly defaults to summary_large_image.
        if ((get_post_meta($post_id, '_myaio_twitter_card', true) === '')) {
            update_post_meta($post_id, '_myaio_twitter_card', 'summary_large_image');
            $migrated++;
        }

        // Raw capture (debug/forensics)
        $social = [];
        foreach ([
            'rank_math_facebook_title','rank_math_facebook_description','rank_math_facebook_image',
            'rank_math_twitter_title','rank_math_twitter_description','rank_math_twitter_image','rank_math_twitter_card_type','rank_math_twitter_use_facebook',
        ] as $k) {
            $v = get_post_meta($post_id, $k, true);
            if ($v !== '' && $v !== null) $social[$k] = $v;
        }
        if (!empty($social)) {
            update_post_meta($post_id, '_myaio_seo_social_raw', ['rank_math' => $social]);
            $report['details']['providers_meta']['rank_math_social'] = true;
        }

        return $migrated;
    }

    private static function migrate_yoast_meta($post_id, &$report) {
        $migrated = 0;

        $map = [
            '_yoast_wpseo_title'     => '_myaio_seo_title',
            '_yoast_wpseo_metadesc'  => '_myaio_seo_description',
            '_yoast_wpseo_canonical' => '_myaio_seo_canonical',
        ];

        foreach ($map as $from => $to) {
            $val = get_post_meta($post_id, $from, true);
            if ($val !== '' && $val !== null) {
                update_post_meta($post_id, $to, $val);
                if ($to === '_myaio_seo_description') {
                    update_post_meta($post_id, '_myaio_meta_description', $val);
                }
                $migrated++;
                $report['details']['providers_meta']['yoast'] = true;
            }
        }

        // Robots/noindex (existing behavior)
        $robots = get_post_meta($post_id, '_yoast_wpseo_meta-robots-noindex', true);
        if ($robots !== '' && $robots !== null) {
            update_post_meta($post_id, '_myaio_seo_robots', 'noindex');
            $migrated++;
        }

        // Social (OG/Twitter) migration
        $yo_og_title = get_post_meta($post_id, '_yoast_wpseo_opengraph-title', true);
        $yo_og_desc  = get_post_meta($post_id, '_yoast_wpseo_opengraph-description', true);
        $yo_og_img   = get_post_meta($post_id, '_yoast_wpseo_opengraph-image', true);

        $yo_tw_title = get_post_meta($post_id, '_yoast_wpseo_twitter-title', true);
        $yo_tw_desc  = get_post_meta($post_id, '_yoast_wpseo_twitter-description', true);
        $yo_tw_img   = get_post_meta($post_id, '_yoast_wpseo_twitter-image', true);
        $yo_tw_card  = get_post_meta($post_id, '_yoast_wpseo_twitter-card', true);

        if ($yo_og_title !== '' && $yo_og_title !== null) { update_post_meta($post_id, '_myaio_og_title', $yo_og_title); $migrated++; }
        if ($yo_og_desc  !== '' && $yo_og_desc  !== null) { update_post_meta($post_id, '_myaio_og_description', $yo_og_desc); $migrated++; }
        if ($yo_og_img   !== '' && $yo_og_img   !== null) { update_post_meta($post_id, '_myaio_og_image', $yo_og_img); $migrated++; }

        if ($yo_tw_title !== '' && $yo_tw_title !== null) { update_post_meta($post_id, '_myaio_twitter_title', $yo_tw_title); $migrated++; }
        if ($yo_tw_desc  !== '' && $yo_tw_desc  !== null) { update_post_meta($post_id, '_myaio_twitter_description', $yo_tw_desc); $migrated++; }
        if ($yo_tw_img   !== '' && $yo_tw_img   !== null) { update_post_meta($post_id, '_myaio_twitter_image', $yo_tw_img); $migrated++; }
        if ($yo_tw_card  !== '' && $yo_tw_card  !== null) { update_post_meta($post_id, '_myaio_twitter_card', $yo_tw_card); $migrated++; }

        // Raw capture (debug/forensics)
        $social = [];
        foreach ([
            '_yoast_wpseo_opengraph-title',
            '_yoast_wpseo_opengraph-description',
            '_yoast_wpseo_opengraph-image',
            '_yoast_wpseo_opengraph-image-id',
            '_yoast_wpseo_twitter-title',
            '_yoast_wpseo_twitter-description',
            '_yoast_wpseo_twitter-image',
            '_yoast_wpseo_twitter-image-id',
            '_yoast_wpseo_twitter-card',
        ] as $k) {
            $v = get_post_meta($post_id, $k, true);
            if ($v !== '' && $v !== null) $social[$k] = $v;
        }
        if (!empty($social)) {
            update_post_meta($post_id, '_myaio_seo_social_raw', ['yoast' => $social]);
            $report['details']['providers_meta']['yoast_social'] = true;
        }

        return $migrated;
    }

    private static function migrate_aioseo_meta($post_id, &$report) {
        $migrated = 0;

        $candidates = [
            '_aioseo_title'         => '_myaio_seo_title',
            '_aioseo_description'   => '_myaio_seo_description',
            '_aioseo_canonical_url' => '_myaio_seo_canonical',
        ];

        foreach ($candidates as $from => $to) {
            $val = get_post_meta($post_id, $from, true);
            if ($val !== '' && $val !== null) {
                update_post_meta($post_id, $to, $val);
                if ($to === '_myaio_seo_description') {
                    update_post_meta($post_id, '_myaio_meta_description', $val);
                }
                $migrated++;
                $report['details']['providers_meta']['aioseo'] = true;
            }
        }

        $aio_robots = self::normalize_robots_value( get_post_meta( $post_id, '_aioseo_robots', true ) );
        if ( $aio_robots !== '' ) {
            update_post_meta( $post_id, '_myaio_seo_robots', $aio_robots );
            $migrated++;
            $report['details']['providers_meta']['aioseo'] = true;
        }

        // Social (AIOSEO) best-effort migration into MYAIO social meta keys
        $aio_og_title = get_post_meta($post_id, '_aioseo_og_title', true);
        $aio_og_desc  = get_post_meta($post_id, '_aioseo_og_description', true);
        $aio_og_img   = get_post_meta($post_id, '_aioseo_og_image', true);

        $aio_tw_title = get_post_meta($post_id, '_aioseo_twitter_title', true);
        $aio_tw_desc  = get_post_meta($post_id, '_aioseo_twitter_description', true);
        $aio_tw_img   = get_post_meta($post_id, '_aioseo_twitter_image', true);
        $aio_tw_card  = get_post_meta($post_id, '_aioseo_twitter_card', true);

        if ($aio_og_title !== '' && $aio_og_title !== null) { update_post_meta($post_id, '_myaio_og_title', $aio_og_title); $migrated++; }
        if ($aio_og_desc  !== '' && $aio_og_desc  !== null) { update_post_meta($post_id, '_myaio_og_description', $aio_og_desc); $migrated++; }
        if ($aio_og_img   !== '' && $aio_og_img   !== null) { update_post_meta($post_id, '_myaio_og_image', $aio_og_img); $migrated++; }

        if ($aio_tw_title !== '' && $aio_tw_title !== null) { update_post_meta($post_id, '_myaio_twitter_title', $aio_tw_title); $migrated++; }
        if ($aio_tw_desc  !== '' && $aio_tw_desc  !== null) { update_post_meta($post_id, '_myaio_twitter_description', $aio_tw_desc); $migrated++; }
        if ($aio_tw_img   !== '' && $aio_tw_img   !== null) { update_post_meta($post_id, '_myaio_twitter_image', $aio_tw_img); $migrated++; }
        if ($aio_tw_card  !== '' && $aio_tw_card  !== null) { update_post_meta($post_id, '_myaio_twitter_card', $aio_tw_card); $migrated++; }

        // NEW: store AIOSEO social/schema raw if present (safe)
        $social = [];
        foreach ([
            '_aioseo_og_title','_aioseo_og_description','_aioseo_og_image',
            '_aioseo_twitter_title','_aioseo_twitter_description','_aioseo_twitter_image',
            '_aioseo_social','_aioseo_social_meta',
        ] as $k) {
            $v = get_post_meta($post_id, $k, true);
            if ($v !== '' && $v !== null) $social[$k] = $v;
        }
        if (!empty($social)) {
            update_post_meta($post_id, '_myaio_seo_social_raw', ['aioseo' => $social]);
            $report['details']['providers_meta']['aioseo_social'] = true;
        }

        $schema = [];
        foreach (['_aioseo_schema','_aioseo_schema_type','_aioseo_schema_settings'] as $k) {
            $v = get_post_meta($post_id, $k, true);
            if ($v !== '' && $v !== null) $schema[$k] = $v;
        }
        if (!empty($schema)) {
            update_post_meta($post_id, '_myaio_seo_schema_raw', ['aioseo' => $schema]);
            $migrated++;
        }

        return $migrated;
    }

    /* =========================================================
     * Redirect migration (unchanged)
     * ========================================================= */
    private static function migrate_rank_math_redirects(&$report) {
        global $wpdb;

        $table = $wpdb->prefix . 'rank_math_redirections';
        $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        if ( $exists !== $table ) {
            $report['warnings'][] = 'Rank Math redirects table not found; redirect migration skipped.';
            return 0;
        }

        // Create our redirects table.
        $dest = $wpdb->prefix . 'myaio_redirects';
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        $charset = $wpdb->get_charset_collate(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $sql = "CREATE TABLE IF NOT EXISTS {$dest} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            from_url TEXT NOT NULL,
            to_url TEXT NULL,
            http_code SMALLINT NOT NULL DEFAULT 301,
            is_active TINYINT(1) NOT NULL DEFAULT 1,
            is_regex TINYINT(1) NOT NULL DEFAULT 0,
            match_query TINYINT(1) NOT NULL DEFAULT 0,
            regex_flags VARCHAR(32) NULL,
            source VARCHAR(32) NOT NULL DEFAULT 'rank_math',
            created_at DATETIME NULL,
            updated_at DATETIME NULL,
            PRIMARY KEY  (id)
        ) {$charset};";
        dbDelta( $sql );

        // Pull active redirects from Rank Math (fixed table name; prepared).
        $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}rank_math_redirections WHERE status = %s",
                'active'
            ),
            ARRAY_A
        );
        if ( empty( $rows ) ) {
            $report['warnings'][] = 'Rank Math redirects table exists but contains no active redirects.';
            return 0;
        }

        $inserted = 0;
        foreach ($rows as $row) {
            $from = isset($row['sources']) ? maybe_unserialize($row['sources']) : null;
            $to = isset($row['url_to']) ? $row['url_to'] : '';
            $code = isset($row['header_code']) ? (int)$row['header_code'] : 301;

            // Detect regex / query behavior (best-effort).
            $is_regex = 0;
            $match_query = 0;
            $regex_flags = '';
            if (is_array($from) && !empty($from[0])) {
                if (!empty($from[0]['comparison']) && strtolower((string)$from[0]['comparison']) === 'regex') {
                    $is_regex = 1;
                }
                if (isset($from[0]['ignore']) && strtolower((string)$from[0]['ignore']) === 'case') {
                    $regex_flags = 'i';
                }
                // Some versions store query matching rules.
                if (isset($from[0]['query']) && strtolower((string)$from[0]['query']) === 'exact') {
                    $match_query = 1;
                }
            }


            // Rank Math can store multiple sources; normalize to first.
            // Rank Math can store multiple sources; normalize to first.
$from_url = '';
if ( is_array( $from ) && ! empty( $from[0]['pattern'] ) ) {
	$from_url = $from[0]['pattern'];
} elseif ( is_string( $from ) ) {
	$from_url = $from;
}

$from_url = trim( (string) $from_url );
$to       = trim( (string) $to );
$code     = (int) $code;

if ( $from_url === '' ) {
	continue;
}

// Allow empty destination only for 410 and 451.
if ( ! in_array( $code, [ 301, 302, 307, 308, 410, 451 ], true ) ) {
	$code = 301;
}

if ( in_array( $code, [ 410, 451 ], true ) ) {
	$to = '';
} elseif ( $to === '' ) {
	continue;
}

// Normalize source to path-only format for MYAIO runtime matching.
// Examples:
// https://example.com/contact-us/ -> /contact-us/
// contact-us/                    -> /contact-us/
// /contact-us/                   -> /contact-us/
if ( preg_match( '~^https?://~i', $from_url ) ) {
	$parts = wp_parse_url( $from_url );
	$path  = isset( $parts['path'] ) ? $parts['path'] : '/';
	$query = isset( $parts['query'] ) ? $parts['query'] : '';

	if ( $path === '' || $path === null ) {
		$path = '/';
	}

	if ( $path[0] !== '/' ) {
		$path = '/' . $path;
	}

	$from_url = $query !== '' ? $path . '?' . $query : $path;
} else {
	if ( $from_url[0] !== '/' ) {
		$from_url = '/' . $from_url;
	}
}
// Skip duplicate redirect if same source/destination/settings already exist.
$dest = esc_sql( $dest );

$http_code = $code;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
$existing_id = $wpdb->get_var(
	$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		"SELECT id FROM {$dest} WHERE from_url = %s AND to_url = %s AND http_code = %d AND is_regex = %d AND match_query = %d AND source = %s LIMIT 1",
		$from_url,
		$to,
		$http_code,
		$is_regex,
		$match_query,
		'rank_math'
	)
);

if ( $existing_id ) {
    continue;
}

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->insert(
    $dest,
    [
        'from_url'     => $from_url,
        'to_url'       => $to,
        'http_code'    => $code,
        'is_active'    => 1,
        'is_regex'     => $is_regex,
        'match_query'  => $match_query,
        'regex_flags'  => $regex_flags,
        'source'       => 'rank_math',
        'created_at'   => current_time( 'mysql' ),
        'updated_at'   => current_time( 'mysql' ),
    ],
    [ '%s', '%s', '%d', '%d', '%d', '%d', '%s', '%s', '%s', '%s' ]
);
$inserted++;
        }

        $report['details']['providers_redirects']['rank_math'] = true;
        return $inserted;
    }

    
    /**
     * Best-effort: migrate AIOSEO redirects (if Redirects addon is present).
     * Stores into {$wpdb->prefix}myaio_redirects so MU-loader can keep applying them.
     */
    private static function migrate_aioseo_redirects(&$report) {
        global $wpdb;

        // AIOSEO redirects table name varies by addon/version.
        // Keep this whitelist small & safe; if none exist we just skip.
        $possible = [
            $wpdb->prefix . 'aioseo_redirects',
            $wpdb->prefix . 'aioseo_redirections',
            $wpdb->prefix . 'aioseo_redirects_301',
        ];

        $table = '';
        foreach ( $possible as $t ) {
            $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            if ( $exists === $t ) {
                $table = $t;
                break;
            }
        }

        if ( '' === $table ) {
            return 0;
        }

        $dest = $wpdb->prefix . 'myaio_redirects';
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        $charset = $wpdb->get_charset_collate(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $sql = "CREATE TABLE IF NOT EXISTS {$dest} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            from_url TEXT NOT NULL,
            to_url TEXT NULL,
            http_code SMALLINT NOT NULL DEFAULT 301,
            is_active TINYINT(1) NOT NULL DEFAULT 1,
            is_regex TINYINT(1) NOT NULL DEFAULT 0,
            match_query TINYINT(1) NOT NULL DEFAULT 0,
            regex_flags VARCHAR(32) NULL,
            source VARCHAR(32) NOT NULL DEFAULT 'aioseo',
            created_at DATETIME NULL,
            updated_at DATETIME NULL,
            PRIMARY KEY  (id)
        ) {$charset};";
        dbDelta( $sql );

        // Inspect columns and map best-effort.
        if ( $table === $wpdb->prefix . 'aioseo_redirects' ) {
            $cols = $wpdb->get_col( "DESC {$wpdb->prefix}aioseo_redirects", 0 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } elseif ( $table === $wpdb->prefix . 'aioseo_redirections' ) {
            $cols = $wpdb->get_col( "DESC {$wpdb->prefix}aioseo_redirections", 0 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } else {
            $cols = $wpdb->get_col( "DESC {$wpdb->prefix}aioseo_redirects_301", 0 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        }
        if ( empty( $cols ) ) {
            return 0;
        }

        $from_col = null;
        $to_col = null;
        $code_col = null;
        $active_col = null;
        $regex_col = null;

        foreach ( $cols as $c ) {
            $lc = strtolower( (string) $c );
            if ( ! $from_col && in_array( $lc, [ 'source_url', 'source', 'from_url', 'from', 'old_url', 'url_from' ], true ) ) {
                $from_col = $c;
            }
            if ( ! $to_col && in_array( $lc, [ 'target_url', 'target', 'to_url', 'to', 'new_url', 'url_to', 'destination_url' ], true ) ) {
                $to_col = $c;
            }
            if ( ! $code_col && in_array( $lc, [ 'type', 'http_code', 'status_code', 'code', 'header_code' ], true ) ) {
                $code_col = $c;
            }
            if ( ! $active_col && in_array( $lc, [ 'enabled', 'is_enabled', 'active', 'is_active', 'status' ], true ) ) {
                $active_col = $c;
            }
            if ( ! $regex_col && in_array( $lc, [ 'is_regex', 'regex' ], true ) ) {
                $regex_col = $c;
            }
        }

        if ( ! $from_col || ! $to_col ) {
            $report['warnings'][] = 'AIOSEO redirects table detected but column mapping failed; migration skipped.';
            return 0;
        }

        // Pull rows (no dynamic WHERE; filter in PHP to keep queries safe for PCP).
        if ( $table === $wpdb->prefix . 'aioseo_redirects' ) {
            $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}aioseo_redirects", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } elseif ( $table === $wpdb->prefix . 'aioseo_redirections' ) {
            $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}aioseo_redirections", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } else {
            $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}aioseo_redirects_301", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        }
        if ( empty( $rows ) ) {
            return 0;
        }

        if ( $active_col ) {
            $allowed_active = [ 1, '1', 'enabled', 'active', true, 'true' ];
            $rows = array_values( array_filter( $rows, function( $r ) use ( $active_col, $allowed_active ) {
                $v = isset( $r[ $active_col ] ) ? $r[ $active_col ] : null;
                return in_array( $v, $allowed_active, true );
            } ) );
            if ( empty( $rows ) ) {
                return 0;
            }
        }

        $inserted = 0;
        foreach ( $rows as $row ) {
            $from = isset( $row[ $from_col ] ) ? (string) $row[ $from_col ] : '';
            $to   = isset( $row[ $to_col ] ) ? (string) $row[ $to_col ] : '';
            if ( '' === $from || '' === $to ) {
                continue;
            }

            $code = 301;
            if ( $code_col && isset( $row[ $code_col ] ) ) {
                $v = $row[ $code_col ];
                if ( is_numeric( $v ) ) {
                    $code = (int) $v;
                } elseif ( is_string( $v ) && preg_match( '/\d{3}/', $v, $m ) ) {
                    $code = (int) $m[0];
                }
            }

            $is_regex = 0;
            if ( $regex_col && isset( $row[ $regex_col ] ) ) {
                $is_regex = (int) ( (string) $row[ $regex_col ] === '1' || (string) $row[ $regex_col ] === 'true' );
            }
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $wpdb->insert(
                $dest,
                [
                    'from_url'     => $from,
                    'to_url'       => $to,
                    'http_code'    => $code,
                    'is_active'    => 1,
                    'is_regex'     => $is_regex,
                    'match_query'  => 0,
                    'regex_flags'  => null,
                    'source'       => 'aioseo',
                    'created_at'   => current_time( 'mysql', 1 ),
                    'updated_at'   => current_time( 'mysql', 1 ),
                ],
                [ '%s', '%s', '%d', '%d', '%d', '%d', '%s', '%s', '%s' ]
            );
            $inserted++;
        }

        return $inserted;
    }

private static function migrate_seopress_redirects(&$report) {
        global $wpdb;

        $possible = [
            $wpdb->prefix . 'seopress_redirections',
            $wpdb->prefix . 'seopress_redirects',
        ];

        $table = '';
        foreach ( $possible as $t ) {
            $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            if ( $exists === $t ) {
                $table = $t;
                break;
            }
        }

        if ( '' === $table ) {
            return 0;
        }

        $dest = $wpdb->prefix . 'myaio_redirects';
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        $charset = $wpdb->get_charset_collate(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $sql = "CREATE TABLE IF NOT EXISTS {$dest} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            from_url TEXT NOT NULL,
            to_url TEXT NULL,
            http_code SMALLINT NOT NULL DEFAULT 301,
            is_active TINYINT(1) NOT NULL DEFAULT 1,
            is_regex TINYINT(1) NOT NULL DEFAULT 0,
            match_query TINYINT(1) NOT NULL DEFAULT 0,
            regex_flags VARCHAR(32) NULL,
            source VARCHAR(32) NOT NULL DEFAULT 'seopress',
            created_at DATETIME NULL,
            updated_at DATETIME NULL,
            PRIMARY KEY  (id)
        ) {$charset};";
        dbDelta( $sql );

        if ( $table === $wpdb->prefix . 'aioseo_redirects' ) {
            $cols = $wpdb->get_col( "DESC {$wpdb->prefix}aioseo_redirects", 0 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } elseif ( $table === $wpdb->prefix . 'aioseo_redirections' ) {
            $cols = $wpdb->get_col( "DESC {$wpdb->prefix}aioseo_redirections", 0 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } else {
            $cols = $wpdb->get_col( "DESC {$wpdb->prefix}aioseo_redirects_301", 0 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        }
        if ( empty( $cols ) ) {
            return 0;
        }

        $from_col = null;
        $to_col = null;
        $code_col = null;
        $active_col = null;

        foreach ( $cols as $c ) {
            $lc = strtolower( (string) $c );
            if ( ! $from_col && in_array( $lc, [ 'source', 'source_url', 'from_url', 'from', 'old_url', 'url_from' ], true ) ) {
                $from_col = $c;
            }
            if ( ! $to_col && in_array( $lc, [ 'target', 'target_url', 'to_url', 'to', 'new_url', 'url_to', 'destination' ], true ) ) {
                $to_col = $c;
            }
            if ( ! $code_col && in_array( $lc, [ 'type', 'http_code', 'status_code', 'code', 'header_code' ], true ) ) {
                $code_col = $c;
            }
            if ( ! $active_col && in_array( $lc, [ 'enabled', 'is_enabled', 'active', 'is_active', 'status' ], true ) ) {
                $active_col = $c;
            }
        }

        if ( ! $from_col || ! $to_col ) {
            return 0;
        }

        if ( $table === $wpdb->prefix . 'aioseo_redirects' ) {
            $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}aioseo_redirects", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } elseif ( $table === $wpdb->prefix . 'aioseo_redirections' ) {
            $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}aioseo_redirections", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        } else {
            $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}aioseo_redirects_301", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        }
        if ( empty( $rows ) ) {
            return 0;
        }

        if ( $active_col ) {
            $allowed_active = [ 1, '1', 'enabled', 'active', true, 'true' ];
            $rows = array_values( array_filter( $rows, function( $r ) use ( $active_col, $allowed_active ) {
                $v = isset( $r[ $active_col ] ) ? $r[ $active_col ] : null;
                return in_array( $v, $allowed_active, true );
            } ) );
            if ( empty( $rows ) ) {
                return 0;
            }
        }

        $inserted = 0;
        foreach ( $rows as $row ) {
            $from = isset( $row[ $from_col ] ) ? (string) $row[ $from_col ] : '';
            $to   = isset( $row[ $to_col ] ) ? (string) $row[ $to_col ] : '';
            if ( '' === $from || '' === $to ) {
                continue;
            }

            $code = 301;
            if ( $code_col && isset( $row[ $code_col ] ) ) {
                $v = $row[ $code_col ];
                if ( is_numeric( $v ) ) {
                    $code = (int) $v;
                } elseif ( is_string( $v ) && preg_match( '/\d{3}/', $v, $m ) ) {
                    $code = (int) $m[0];
                }
            }
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $wpdb->insert(
                $dest,
                [
                    'from_url'     => $from,
                    'to_url'       => $to,
                    'http_code'    => $code,
                    'is_active'    => 1,
                    'is_regex'     => 0,
                    'match_query'  => 0,
                    'regex_flags'  => null,
                    'source'       => 'seopress',
                    'created_at'   => current_time( 'mysql', 1 ),
                    'updated_at'   => current_time( 'mysql', 1 ),
                ],
                [ '%s', '%s', '%d', '%d', '%d', '%d', '%s', '%s', '%s' ]
            );
            $inserted++;
        }

        return $inserted;
    }

private static function migrate_sitemap_settings($providers, &$report) {
        // We store “effective” sitemap settings in MYAIO option that our sitemap server uses.
        $existing = get_option(self::SITEMAP_OPTION, []);
        if (!is_array($existing)) $existing = [];

        // Do not overwrite an already configured MYAIO sitemap unless it looks empty.
        $already_configured = isset($existing['configured']) && $existing['configured'] === true;

        // Determine provider priority (you can change this order):
        $source = null;
        if (in_array('rank_math', $providers, true)) $source = 'rank_math';
        elseif (in_array('yoast', $providers, true)) $source = 'yoast';
        elseif (in_array('aioseo', $providers, true)) $source = 'aioseo';

        // If no source found, still ensure MYAIO sitemap is enabled with sane defaults (so sitemap continues).
        $settings = [
            'enabled' => true,
            'configured' => true,
            'provider_source' => $source ?: 'myaio_default',
            'max_urls_per_sitemap' => 2000, // safe paging size; supports Yoast-like page numbering
            'include_lastmod' => true,
            'include_post_types' => self::default_public_post_types(),
            'exclude_post_ids' => [],
            'raw_provider_sitemap_options' => [],
            // NEW: redirect old /sitemap.xml to our effective sitemap index
            'root_sitemap_redirect' => true,
            'root_sitemap_target' => 'sitemap_index.xml',
            'migrated_at' => gmdate('c'),
        ];

        // Keep existing if configured (don’t break a running site)
        if ($already_configured) {
            $report['details']['sitemap_effective'] = $existing;
            return 0;
        }

        // Rank Math sitemap settings
        if ($source === 'rank_math') {
            $rm = get_option('rank-math-options-sitemap', null);
            if (is_array($rm)) {
                $settings['raw_provider_sitemap_options']['rank_math']['rank-math-options-sitemap'] = $rm;
                $report['details']['providers_sitemaps']['rank_math'] = true;

                // Best-effort keys (vary by version)
                if (isset($rm['sitemap_enable'])) {
                    $settings['enabled'] = (bool) $rm['sitemap_enable'];
                }

                // Exclude posts list (best-effort)
                foreach (['sitemap_exclude_posts', 'exclude_posts', 'sitemap_exclude_post_ids'] as $k) {
                    if (!empty($rm[$k])) {
                        $settings['exclude_post_ids'] = self::parse_id_list($rm[$k]);
                        break;
                    }
                }

                // Include post types (best-effort)
                $pts = [];
                foreach (['post_types', 'pt_sitemap', 'sitemap_post_types'] as $k) {
                    if (!empty($rm[$k]) && is_array($rm[$k])) {
                        foreach ($rm[$k] as $pt => $flag) {
                            if ($flag) $pts[] = (string)$pt;
                        }
                        break;
                    }
                }
                if (!empty($pts)) {
                    $settings['include_post_types'] = array_values(array_unique(array_filter($pts)));
                }
            }
        }

        // Yoast sitemap settings
        if ($source === 'yoast') {
            $yx = get_option('wpseo_xml', null);
            if (is_array($yx)) {
                $settings['raw_provider_sitemap_options']['yoast']['wpseo_xml'] = $yx;
                $report['details']['providers_sitemaps']['yoast'] = true;

                foreach (['enablexmlsitemap', 'enable_xml_sitemap'] as $k) {
                    if (isset($yx[$k])) {
                        $settings['enabled'] = (bool) $yx[$k];
                        break;
                    }
                }
                // Yoast mostly controls exclusions via noindex meta (already migrated),
                // so we keep include_post_types as all public post types by default.
            }
        }

        // AIOSEO sitemap settings (best-effort nested)
        if ($source === 'aioseo') {
            $ao = get_option('aioseo_options', null);
            if (is_array($ao)) {
                $settings['raw_provider_sitemap_options']['aioseo']['aioseo_options'] = $ao;
                $report['details']['providers_sitemaps']['aioseo'] = true;

                $enabled = self::deep_get($ao, ['sitemap','general','enable']);
                if ($enabled !== null) $settings['enabled'] = (bool) $enabled;

                $pts = self::deep_get($ao, ['sitemap','postTypes']);
                if (is_array($pts)) {
                    $settings['include_post_types'] = array_values(array_keys(array_filter($pts)));
                }
            }
        }

        update_option(self::SITEMAP_OPTION, $settings, false);
        $report['details']['sitemap_effective'] = $settings;

        return 1;
    }

    private static function default_public_post_types() {
        $pts = get_post_types(['public' => true], 'names');
        if (isset($pts['attachment'])) unset($pts['attachment']);
        return array_values($pts);
    }

    private static function parse_id_list($val) {
        if (is_string($val)) {
            $val = trim($val);
            if ($val === '') return [];
            $parts = preg_split('/[^0-9]+/', $val);
            return array_values(array_filter(array_map('intval', $parts)));
        }
        if (is_array($val)) {
            return array_values(array_filter(array_map('intval', $val)));
        }
        return [];
    }

    private static function deep_get($arr, $path) {
        if (!is_array($arr)) return null;
        $cur = $arr;
        foreach ($path as $k) {
            if (!is_array($cur) || !array_key_exists($k, $cur)) return null;
            $cur = $cur[$k];
        }
        return $cur;
    }

    /* =========================================================
     * NEW: MYAIO Sitemap Server (auto-enabled by this file)
     * ========================================================= */

    public static function boot() {
        // Register rewrite/query vars + serve XML
        add_action('init', [__CLASS__, 'register_sitemap_rewrites']);
        add_action('template_redirect', [__CLASS__, 'maybe_serve_sitemap']);

        // Flush rewrite rules once after migration
        add_action('init', function() {
            if ((int) get_option(self::SITEMAP_FLUSH_FLAG, 0) === 1) {
                delete_option(self::SITEMAP_FLUSH_FLAG);
                if (function_exists('flush_rewrite_rules')) {
                    flush_rewrite_rules(false);
                }
            }
        }, 99);
    }

    public static function register_sitemap_rewrites() {
        // Root sitemap.xml -> redirect to effective MYAIO sitemap URL
        add_rewrite_rule('^sitemap\.xml$', 'index.php?myaio_sitemap_redirect=1', 'top');

        // Index
        add_rewrite_rule('^sitemap_index\.xml$', 'index.php?myaio_sitemap=index', 'top');

        // Post type sitemap:
        // - post-sitemap.xml (Rank Math-style simple)
        // - post-sitemap1.xml (Yoast-style paging)
        add_rewrite_rule('^([a-z0-9\-_]+)-sitemap([0-9]+)?\.xml$', 'index.php?myaio_sitemap=$matches[1]&myaio_sitemap_page=$matches[2]', 'top');

        add_filter('query_vars', function($vars) {
            $vars[] = 'myaio_sitemap';
            $vars[] = 'myaio_sitemap_page';
            $vars[] = 'myaio_sitemap_redirect';
            return $vars;
        });
    }

    public static function maybe_serve_sitemap() {
        $which = get_query_var('myaio_sitemap');
        $do_redirect = (int) get_query_var('myaio_sitemap_redirect');
        if (!$which && !$do_redirect) return;

        $settings = get_option(self::SITEMAP_OPTION, []);
        if (!is_array($settings)) $settings = [];

        $enabled = isset($settings['enabled']) ? (bool)$settings['enabled'] : true;
        if (!$enabled) {
            status_header(404);
            exit;
        }

        // Redirect /sitemap.xml to the effective sitemap index when migrating from other SEO plugins.
        // This preserves old Search Console submissions without breaking the site.
        if ($do_redirect) {
            $root_redirect_enabled = !isset($settings['root_sitemap_redirect']) ? true : (bool) $settings['root_sitemap_redirect'];
            if ($root_redirect_enabled) {
                $target = isset($settings['root_sitemap_target']) && is_string($settings['root_sitemap_target']) && $settings['root_sitemap_target'] !== ''
                    ? $settings['root_sitemap_target']
                    : 'sitemap_index.xml';
                $target = ltrim($target, '/');
                $url = home_url('/' . $target);
                $url = apply_filters('myaio_sitemap_root_redirect_target', $url, $settings);
                 if ( ! headers_sent() && ! empty( $url ) ) {
					$safe_url = esc_url_raw( $url );
					wp_safe_redirect( $safe_url, 301 );
					exit;
				}
            }
            // If disabled, fall through to 404 so we don't create loops.
            status_header(404);
            exit;
        }

        nocache_headers();
        header('Content-Type: application/xml; charset=' . get_bloginfo('charset'), true);

        $xml_output = '';
        if ($which === 'index') {
            $xml_output = self::render_sitemap_index( $settings );
        } else {
            $page = get_query_var('myaio_sitemap_page');
            $page = ($page === '' || $page === null) ? 1 : (int)$page;
            if ($page < 1) $page = 1;
            $xml_output = self::render_post_type_sitemap($which, $page, $settings);
        }

        // Remove xml-stylesheet processing instruction if present (from RankMath or other SEO plugins)
        $xml_output = preg_replace('/<\?xml-stylesheet[^?]*\?>\s*/i', '', $xml_output);

        echo $xml_output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML is generated/escaped in renderer.
        exit;
    }

    private static function render_sitemap_index($settings) {
        $post_types = isset($settings['include_post_types']) && is_array($settings['include_post_types'])
            ? $settings['include_post_types']
            : self::default_public_post_types();

        $max = isset($settings['max_urls_per_sitemap']) ? max(1, (int)$settings['max_urls_per_sitemap']) : 2000;

        $xml  = '<?xml version="1.0" encoding="UTF-8"?>';
        $xml .= '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

        foreach ($post_types as $pt) {
            if (!post_type_exists($pt)) continue;

            $total = self::count_posts_for_sitemap($pt, $settings);
            $pages = max(1, (int) ceil($total / $max));
            $lastmod = self::latest_modified_for_post_type($pt, $settings);

            for ($p = 1; $p <= $pages; $p++) {
                $loc = home_url('/' . rawurlencode($pt) . '-sitemap' . ($pages > 1 ? $p : '') . '.xml');
                $xml .= '<sitemap>';
                $xml .= '<loc>' . esc_url($loc) . '</loc>';
                if (!empty($lastmod) && (!isset($settings['include_lastmod']) || $settings['include_lastmod'])) {
                    $xml .= '<lastmod>' . esc_html($lastmod) . '</lastmod>';
                }
                $xml .= '</sitemap>';
            }
        }

        $xml .= '</sitemapindex>';
        return $xml;
    }

    private static function render_post_type_sitemap($post_type, $page, $settings) {
        if (!post_type_exists($post_type)) {
            status_header(404);
            return '';
        }

        $post_types = isset($settings['include_post_types']) && is_array($settings['include_post_types'])
            ? $settings['include_post_types']
            : self::default_public_post_types();

        if (!in_array($post_type, $post_types, true)) {
            status_header(404);
            return '';
        }

        $exclude_ids = isset($settings['exclude_post_ids']) && is_array($settings['exclude_post_ids'])
            ? array_values(array_filter(array_map('intval', $settings['exclude_post_ids'])))
            : [];

        $max = isset($settings['max_urls_per_sitemap']) ? max(1, (int)$settings['max_urls_per_sitemap']) : 2000;

        $args = [
            'post_type' => $post_type,
            'post_status' => 'publish',
            'posts_per_page' => $max,
            'paged' => $page,
            'orderby' => 'modified',
            'order' => 'DESC',
            'fields' => 'ids',
            'no_found_rows' => false,
        ];
        if (!empty($exclude_ids)) {
            $args['post__not_in'] = $exclude_ids; // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
        }

        $q = new WP_Query($args);

        // If requested page beyond max pages, 404
        if ($q->max_num_pages > 0 && $page > (int)$q->max_num_pages) {
            status_header(404);
            return '';
        }

        $xml  = '<?xml version="1.0" encoding="UTF-8"?>';
        $xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

        if (!empty($q->posts)) {
            foreach ($q->posts as $pid) {
                $loc = get_permalink($pid);
                if (!$loc) continue;

                $xml .= '<url>';
                $xml .= '<loc>' . esc_url($loc) . '</loc>';

                if (!isset($settings['include_lastmod']) || $settings['include_lastmod']) {
                    $lm = get_post_modified_time('c', true, $pid);
                    if ($lm) $xml .= '<lastmod>' . esc_html($lm) . '</lastmod>';
                }

                $xml .= '</url>';
            }
        }

        $xml .= '</urlset>';
        return $xml;
    }

    private static function count_posts_for_sitemap( $post_type, $settings ) {
        $exclude_ids = isset( $settings['exclude_post_ids'] ) && is_array( $settings['exclude_post_ids'] )
            ? array_values( array_filter( array_map( 'intval', $settings['exclude_post_ids'] ) ) )
            : [];

        // Fast path when there is no exclusion list.
        if ( empty( $exclude_ids ) ) {
            $counts = wp_count_posts( $post_type );
            return isset( $counts->publish ) ? (int) $counts->publish : 0;
        }

        // When exclusions exist, use WP_Query so PCP doesn't flag dynamic SQL.
        $q = new WP_Query(
            [
                'post_type'      => $post_type,
                'post_status'    => 'publish',
                'fields'         => 'ids',
                'posts_per_page' => 1,
                'no_found_rows'  => false,
                'post__not_in'   => $exclude_ids, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
            ]
        );

        return (int) $q->found_posts;
    }
    private static function latest_modified_for_post_type( $post_type, $settings ) {
        $exclude_ids = isset( $settings['exclude_post_ids'] ) && is_array( $settings['exclude_post_ids'] )
            ? array_values( array_filter( array_map( 'intval', $settings['exclude_post_ids'] ) ) )
            : [];

        $args = [
            'post_type'           => $post_type,
            'post_status'         => 'publish',
            'fields'              => 'ids',
            'posts_per_page'      => 1,
            'orderby'             => 'modified',
            'order'               => 'DESC',
            'no_found_rows'       => true,
            'ignore_sticky_posts' => true,
        ];

        if ( ! empty( $exclude_ids ) ) {
            $args['post__not_in'] = $exclude_ids; // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
        }

        $ids = get_posts( $args );
        if ( empty( $ids ) ) {
            return null;
        }

        $id = (int) $ids[0];
        // 'c' gives ISO-8601; true for GMT.
        return get_post_modified_time( 'c', true, $id );
    }
}
/**
 * AUTO-BOOTSTRAP
 * This makes sitemap serving work just by replacing this file (no other code changes needed).
 */
MYAIO_SEO_Migrator::boot();
