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

// Disable RankMath sitemap hooks to prevent stylesheet injection
add_action('init', function() {
    // Remove RankMath sitemap hooks with high priority
    if (class_exists('RankMath')) {
        remove_all_actions('rank_math/sitemap/index');
        remove_all_actions('rank_math/sitemap/urlset');
        remove_all_filters('rank_math/sitemap/content');
        remove_all_filters('rank_math/sitemap/stylesheet_url');
    }
}, 1); // Priority 1 to run very early

// Filter sitemap output to remove stylesheet tags + adjust branding
add_action( 'template_redirect', function () {

    $request_uri = '';

    if ( isset( $_SERVER['REQUEST_URI'] ) ) {
        $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
    }

    // Only filter sitemap requests
    if (
        '' !== $request_uri &&
        ( false !== strpos( $request_uri, 'sitemap' ) || false !== strpos( $request_uri, '.xml' ) )
    ) {
        ob_start( function ( $buffer ) {

            // Remove xml-stylesheet processing instruction from any sitemap output
            $buffer = preg_replace( '/<\?xml-stylesheet\s+[^?]*\?>\s*/i', '', $buffer );

            // Replace RankMath branding with MYAIO branding
            $buffer = str_ireplace( 'rankmath.com', 'myaio.com', $buffer );
            $buffer = str_replace( 'Rank Math', 'MYAIO', $buffer );

            return $buffer;
        } );
    }

}, 0 ); // Priority 0 to run before everything

// Register sitemap routes
add_action('rest_api_init', function () {
    register_rest_route('myaio/v1', '/check-sitemap', [
        'methods'  => 'GET',
        'callback' => 'myaio_sac_check_sitemap',
        // validate via header or ?key= as a fallback
        'permission_callback' => 'myaio_sac_rest_permission_check',
    ]);

    register_rest_route('myaio/v1', '/generate-sitemap', [
        'methods'  => 'POST',
        'callback' => 'myaio_sac_generate_sitemap',
        'permission_callback' => 'myaio_sac_rest_permission_check',
    ]);

    register_rest_route('myaio/v1', '/sync-sitemap-urls', [
        'methods'  => 'POST',
        'callback' => 'myaio_sac_sync_sitemap_urls',
        'permission_callback' => 'myaio_sac_rest_permission_check',
    ]);
});

/** ========= Small utils ========= **/
function myaio_sac_norm_url($u) {
    // normalize for comparisons (remove trailing slash except root)
    $u = trim((string)$u);
    $parts = wp_parse_url($u);
    if (!$parts) return rtrim($u, '/');
    $scheme = isset($parts['scheme']) ? strtolower($parts['scheme']) : 'http';
    $host   = isset($parts['host']) ? strtolower($parts['host']) : '';
    $port   = isset($parts['port']) ? ':' . $parts['port'] : '';
    $path   = isset($parts['path']) ? $parts['path'] : '';
    $query  = isset($parts['query']) ? '?' . $parts['query'] : '';
    $frag   = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
    if ($path !== '/') $path = rtrim($path, '/');
    return "$scheme://$host$port$path$query$frag";
}

/** ========= Time helpers ========= **/
function myaio_sac_iso8601($ts) {
    $ts = (int) $ts;
    return $ts > 0 ? gmdate('c', $ts) : null;
}

/** Prefer HTTP Last-Modified; fall back to stored epoch if provided */
function myaio_sac_get_last_modified_for_url($url, $fallback_epoch = null) {
    $res = wp_remote_head($url, ['timeout' => 8, 'redirection' => 3]);
    if (!is_wp_error($res)) {
        $lm = wp_remote_retrieve_header($res, 'last-modified');
        if (!empty($lm)) {
            $t = strtotime($lm);
            if ($t !== false && $t > 0) return myaio_sac_iso8601($t);
        }
    }
    return $fallback_epoch ? myaio_sac_iso8601($fallback_epoch) : null;
}

/** ========= URL check helper (HEAD → GET fallback) ========= **/
function myaio_sac_url_exists($url) {
    $url = trim((string)$url);
    if ($url === '') return false;

    // 1) Try HEAD first (fast)
    $res = wp_remote_head($url, [
        'timeout'     => 4,
        'redirection' => 2,
        'sslverify'   => true,
        'headers'     => [
            'User-Agent' => 'Mozilla/5.0 (Sitemap Checker)',
            'Accept'     => '*/*',
        ],
    ]);

    if (!is_wp_error($res)) {
        $code = (int) wp_remote_retrieve_response_code($res);
        if ($code >= 200 && $code < 400) return true;

        // Some hosts block HEAD (403/405) or behave oddly — fall back to tiny GET
        if (!in_array($code, [0, 400, 401, 403, 405], true)) return false;
    }

    // 2) Tiny GET with Range fallback
    $res = wp_remote_get($url, [
        'timeout'     => 4,
        'redirection' => 2,
        'sslverify'   => true,
        'headers'     => [
            'User-Agent' => 'Mozilla/5.0 (Sitemap Checker)',
            'Accept'     => 'application/xml,text/xml,text/plain;q=0.9,*/*;q=0.8',
            'Range'      => 'bytes=0-2048',
        ],
    ]);

    if (is_wp_error($res)) return false;
    $code = (int) wp_remote_retrieve_response_code($res);
    return ($code >= 200 && $code < 400);
}


/** ========= Fetch & parse XML (index/urlset) ========= **/
function myaio_sac_fetch_xml($url) {
    $res = wp_remote_get($url, [
        'timeout'     => 8,
        'redirection' => 2,
        'sslverify'   => true,
        'headers'     => [
            'User-Agent' => 'Mozilla/5.0 (Sitemap XML Fetch)',
            'Accept'     => 'application/xml,text/xml,text/plain;q=0.9,*/*;q=0.8',
        ],
    ]);

    if (is_wp_error($res)) return null;

    $code = (int) wp_remote_retrieve_response_code($res);
    if ($code < 200 || $code >= 400) return null;

    $body = (string) wp_remote_retrieve_body($res);
    if ($body === '') return null;

    // Support .gz sitemaps (and servers that gzip even without .gz URL)
    $is_gz_url = (bool) preg_match('/\\.gz(\\?|$)/i', (string)$url);
    $looks_gz  = (strlen($body) >= 2 && ord($body[0]) === 0x1f && ord($body[1]) === 0x8b);

    if (($is_gz_url || $looks_gz) && function_exists('gzdecode')) {
        $decoded = @gzdecode($body);
        if ($decoded !== false && $decoded !== '') {
            $body = $decoded;
        }
    }

    libxml_use_internal_errors(true);
    $xml = simplexml_load_string($body);
    libxml_clear_errors();

    return $xml ?: null;
}

function myaio_sac_parse_sitemap_type($xml) {
    if (!$xml) return null;
    $is_index  = $xml->xpath('/*[local-name()="sitemapindex"]');
    $is_urlset = $xml->xpath('/*[local-name()="urlset"]');
    if ($is_index && !empty($is_index))  return 'index';
    if ($is_urlset && !empty($is_urlset)) return 'urlset';
    return null;
}
function myaio_sac_extract_index_children($xml) {
    if (!$xml) return [];
    $nodes = $xml->xpath('/*[local-name()="sitemapindex"]/*[local-name()="sitemap"]/*[local-name()="loc"]');
    $out = [];
    if (is_array($nodes)) foreach ($nodes as $n) {
        $loc = trim((string)$n);
        if ($loc) $out[] = $loc;
    }
    return array_values(array_unique($out));
}
function myaio_sac_extract_urlset_urls($xml) {
    if (!$xml) return [];
    $nodes = $xml->xpath('/*[local-name()="urlset"]/*[local-name()="url"]/*[local-name()="loc"]');
    $out = [];
    if (is_array($nodes)) foreach ($nodes as $n) {
        $loc = trim((string)$n);
        if ($loc) $out[] = $loc;
    }
    return array_values(array_unique($out));
}

/** ========= Folder helpers (we use /sitemaps/ for our generator) ========= **/

/** Prefer /sitemaps in webroot, fallback to uploads/sitemaps */
function myaio_sac_resolve_sitemaps_dir() {
    global $wp_filesystem;

    // Initialize the filesystem
    if ( ! function_exists( 'WP_Filesystem' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    WP_Filesystem();

    $root_dir = trailingslashit( ABSPATH ) . 'sitemaps/';
    $root_url = home_url( '/sitemaps/' );

    if ( ! $wp_filesystem->is_dir( $root_dir ) ) {
        $wp_filesystem->mkdir( $root_dir );
    }

    if ( $wp_filesystem->is_writable( $root_dir ) ) {
        return [
            'dir' => $root_dir,
            'url' => trailingslashit( $root_url ),
        ];
    }

    // Fallback to uploads
    $uploads = wp_get_upload_dir();
    $u_dir   = trailingslashit( $uploads['basedir'] ) . 'sitemaps/';
    $u_url   = trailingslashit( $uploads['baseurl'] ) . 'sitemaps/';

    if ( ! $wp_filesystem->is_dir( $u_dir ) ) {
        $wp_filesystem->mkdir( $u_dir );
    }

    return [
        'dir' => $u_dir,
        'url' => $u_url,
    ];
}


/** Write a file safely (WP_Filesystem if available, else file_put_contents) */
function myaio_sac_write_file($path, $contents) {
    // Remove xml-stylesheet processing instruction before writing (from RankMath or other SEO plugins)
    // This removes the entire xml-stylesheet tag regardless of the href URL
    $contents = preg_replace('/<\?xml-stylesheet\s+[^?]*\?>\s*/i', '', $contents);
    
    // Replace RankMath branding with Myaio
    $contents = str_ireplace('rankmath.com', 'Myaio.com', $contents);
    $contents = str_ireplace('Rank Math', 'Myaio', $contents);
    
    require_once ABSPATH . 'wp-admin/includes/file.php';
    WP_Filesystem();
    global $wp_filesystem;
    if ($wp_filesystem && $wp_filesystem->is_writable(dirname($path))) {
        return (bool) $wp_filesystem->put_contents($path, $contents, FS_CHMOD_FILE);
    }
    return (bool) @file_put_contents($path, $contents);
}

/** Render a <urlset> from items */
function myaio_sac_render_urlset(array $items) {
    $xml = new XMLWriter();
    $xml->openMemory();
    $xml->startDocument('1.0', 'UTF-8');
    $xml->setIndent(true);

    $xml->startElement('urlset');
    $xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
    $xml->writeAttribute('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1');

    $allowed_freq = ['always','hourly','daily','weekly','monthly','yearly','never'];

    foreach ($items as $it) {
        if (empty($it['loc'])) continue;

        $xml->startElement('url');
        $xml->writeElement('loc', esc_url_raw($it['loc']));

        if (!empty($it['lastmod'])) {
            $xml->writeElement('lastmod', (string)$it['lastmod']); // ISO-8601 already
        }

        // Optional fields
        if (!empty($it['freq'])) {
            $freq = myaio_sac_sitemap_normalize_changefreq( $it['freq'] );
            $freq = strtolower( trim( (string) $freq ) );
            if (in_array($freq, $allowed_freq, true)) {
                $xml->writeElement('changefreq', $freq);
            }
        }
        if (isset($it['priority']) && $it['priority'] !== '') {
            $p = floatval($it['priority']);
            if ($p < 0) $p = 0;
            if ($p > 1) $p = 1;
            $xml->writeElement('priority', number_format($p, 1, '.', ''));
        }

        // Preserve image sitemap entries for this URL.
        if ( ! empty( $it['images'] ) && is_array( $it['images'] ) ) {
            foreach ( $it['images'] as $image_url ) {
                $image_url = trim( (string) $image_url );
                if ( '' === $image_url ) {
                    continue;
                }

                $xml->startElement( 'image:image' );
                $xml->writeElement( 'image:loc', esc_url_raw( $image_url ) );
                $xml->endElement(); // image:image
            }
        }

        $xml->endElement(); // url
    }

    $xml->endElement(); // urlset
    $xml->endDocument();

    $output = $xml->outputMemory();

    // Remove xml-stylesheet processing instruction if present (from RankMath or other SEO plugins)
    // This removes the entire xml-stylesheet tag regardless of the href URL
    $output = preg_replace('/<\?xml-stylesheet\s+[^?]*\?>\s*/i', '', $output);

    // Replace RankMath branding with Myaio
    $output = str_ireplace('rankmath.com', 'Myaio.com', $output);
    $output = str_ireplace('Rank Math', 'Myaio', $output);

    return $output;
}

/** Render a <sitemapindex> from child sitemap URLs */
function myaio_sac_render_sitemap_index(array $child_urls) {
    $xml = new XMLWriter();
    $xml->openMemory();
    $xml->startDocument('1.0', 'UTF-8');
    $xml->setIndent(true);

    $xml->startElement('sitemapindex');
    $xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');

    $now = gmdate('c');

    foreach ($child_urls as $u) {
        if (!$u) continue;
        $xml->startElement('sitemap');
        $xml->writeElement('loc', esc_url_raw($u));
        $xml->writeElement('lastmod', $now);
        $xml->endElement(); // sitemap
    }

    $xml->endElement(); // sitemapindex
    $xml->endDocument();
    
    $output = $xml->outputMemory();
    
    // Remove xml-stylesheet processing instruction if present (from RankMath or other SEO plugins)
    // This removes the entire xml-stylesheet tag regardless of the href URL
    $output = preg_replace('/<\?xml-stylesheet\s+[^?]*\?>\s*/i', '', $output);
    
    // Replace RankMath branding with Myaio
    $output = str_ireplace('rankmath.com', 'Myaio.com', $output);
    $output = str_ireplace('Rank Math', 'Myaio', $output);
    
    return $output;
}

/** ========= Collectors (used by generator) ========= **/

/** ========= Indexability helpers (only include URLs that are indexable) ========= **/

function myaio_sac_meta_contains_noindex($val) {
    if (is_array($val)) {
        foreach ($val as $v) {
            if (myaio_sac_meta_contains_noindex($v)) return true;
        }
        return false;
    }
    if ($val === null) return false;

    $v = maybe_unserialize($val);
    if (is_array($v)) return myaio_sac_meta_contains_noindex($v);

    $s = strtolower(trim((string)$v));
    if ($s === '') return false;

    // common forms
    if ($s === '1' || $s === 'true' || $s === 'noindex') return true;
    return (strpos($s, 'noindex') !== false);
}

function myaio_sac_is_indexable_post($post_id) {
    // If site is set to discourage search engines, treat as non-indexable
    if ((int) get_option('blog_public') === 0) return false;

    $post = get_post($post_id);
    if (!$post) return false;
    if ($post->post_status !== 'publish') return false;
    if (!empty($post->post_password)) return false;

    // Yoast
    $yoast_noindex = get_post_meta($post_id, '_yoast_wpseo_meta-robots-noindex', true);
    if (myaio_sac_meta_contains_noindex($yoast_noindex)) return false;
    $yoast_adv = get_post_meta($post_id, '_yoast_wpseo_meta-robots-adv', true);
    if (myaio_sac_meta_contains_noindex($yoast_adv)) return false;

    // Rank Math
    $rm_robots = get_post_meta($post_id, '_rank_math_robots', true);
    if (myaio_sac_meta_contains_noindex($rm_robots)) return false;
    $rm_robots2 = get_post_meta($post_id, 'rank_math_robots', true);
    if (myaio_sac_meta_contains_noindex($rm_robots2)) return false;

    // AIOSEO (common flags)
    $aio_noindex = get_post_meta($post_id, '_aioseo_noindex', true);
    if (myaio_sac_meta_contains_noindex($aio_noindex)) return false;

    return true;
}

function myaio_sac_is_indexable_term($term_id, $taxonomy) {
    if ((int) get_option('blog_public') === 0) return false;

    // Yoast term meta
    $yoast = get_term_meta((int)$term_id, 'wpseo_noindex', true);
    if (myaio_sac_meta_contains_noindex($yoast)) return false;

    // Rank Math term meta (varies by version)
    $rm = get_term_meta((int)$term_id, 'rank_math_robots', true);
    if (myaio_sac_meta_contains_noindex($rm)) return false;
    $rm2 = get_term_meta((int)$term_id, '_rank_math_robots', true);
    if (myaio_sac_meta_contains_noindex($rm2)) return false;

    // AIOSEO term meta (common)
    $aio = get_term_meta((int)$term_id, '_aioseo_noindex', true);
    if (myaio_sac_meta_contains_noindex($aio)) return false;

    return true;
}


/** ========= Sitemap classification + refinements ========= **/

/**
 * Cached URL status check. Excludes redirects and errors per requirements.
 *
 * @param string $url URL to check.
 * @return bool True if HTTP 200, false otherwise.
 */
function myaio_sac_sitemap_url_is_http_200( $url ) {
    $url = trim( (string) $url );
    if ( '' === $url ) {
        return false;
    }

    $cache_key = 'myaio_sac_http200_' . md5( $url );
    $cached    = get_transient( $cache_key );
    if ( false !== $cached ) {
        return (bool) $cached;
    }

    $args = array(
        'timeout'     => 6,
        'redirection' => 0, // Important: we must exclude 3xx.
        'sslverify'   => true,
        'headers'     => array(
            'User-Agent' => 'Mozilla/5.0 (MYAIO Sitemap Generator)',
            'Accept'     => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        ),
    );

    $code = 0;
    $res  = wp_remote_head( $url, $args );
    if ( ! is_wp_error( $res ) ) {
        $code = (int) wp_remote_retrieve_response_code( $res );
    } else {
        // Some servers block HEAD. Try GET without following redirects.
        $res = wp_remote_get( $url, $args );
        if ( ! is_wp_error( $res ) ) {
            $code = (int) wp_remote_retrieve_response_code( $res );
        }
    }

    $ok = ( 200 === $code );

    // Cache for 12 hours to avoid expensive checks in large sites.
    set_transient( $cache_key, $ok ? 1 : 0, 12 * HOUR_IN_SECONDS );

    return $ok;
}

/**
 * Determine if a page URL should be excluded entirely based on path keywords.
 *
 * @param string $url URL.
 * @return bool
 */
function myaio_sac_sitemap_is_hard_excluded_url( $url ) {
    $url  = trim( (string) $url );
    $path = (string) wp_parse_url( $url, PHP_URL_PATH );
    $path = strtolower( untrailingslashit( $path ) );

    if ( '' === $path ) {
        return false;
    }

    // Exclude login/admin/search/thank-you/checkout-style endpoints.
    $hard_excludes = array(
        '/wp-login.php',
        '/wp-admin',
        '/login',
        '/signin',
        '/sign-in',
        '/thank-you',
        '/thanks',
        '/confirmation',
        '/confirm',
        '/checkout',
        '/cart',
        '/my-account',
        '/account',
    );

    foreach ( $hard_excludes as $ex ) {
        if ( $path === $ex || 0 === strpos( $path, $ex . '/' ) ) {
            return true;
        }
    }

    // Exclude search results URLs.
    // Common patterns: /?s=, /search/, /search
    $query = (string) wp_parse_url( $url, PHP_URL_QUERY );
    if ( '' !== $query ) {
        parse_str( $query, $qs );
        if ( isset( $qs['s'] ) && '' !== trim( (string) $qs['s'] ) ) {
            return true;
        }
    }
    if ( '/search' === $path || 0 === strpos( $path, '/search/' ) ) {
        return true;
    }

    return false;
}

/**
 * Convert a human requirement "six-month" into a valid sitemap changefreq.
 * The sitemap protocol does not support biannual values; we map to 'yearly' (closest).
 *
 * @param string $freq Raw frequency.
 * @return string|null
 */
function myaio_sac_sitemap_normalize_changefreq( $freq ) {
    $freq = strtolower( trim( (string) $freq ) );
    if ( '' === $freq ) {
        return null;
    }
    if ( 'six-month' === $freq || 'six-monthly' === $freq || '6-month' === $freq || 'biannual' === $freq ) {
        return 'yearly';
    }
    return $freq;
}

/**
 * Determine priority + changefreq for a post/page/url.
 *
 * @param array $ctx Context: type, post_id, term_id, url, is_home, is_posts_page, is_archive, archive_kind.
 * @return array{priority:float,freq:string|null}
 */
function myaio_sac_sitemap_get_tags( array $ctx ) {
    $priority = 0.5;
    $freq     = 'monthly';

    $type = isset( $ctx['type'] ) ? (string) $ctx['type'] : '';

    if ( 'home' === $type ) {
        $priority = 1.0;
        $freq     = 'weekly';
        return array( 'priority' => $priority, 'freq' => $freq );
    }

    if ( 'category' === $type ) {
        $priority = 0.8;
        $freq     = 'weekly';
        return array( 'priority' => $priority, 'freq' => $freq );
    }

    if ( 'tag_archive' === $type || 'author_archive' === $type ) {
        $priority = 0.2;
        $freq     = 'monthly';
        return array( 'priority' => $priority, 'freq' => $freq );
    }

    if ( 'pagination' === $type ) {
        $priority = 0.1;
        $freq     = 'monthly';
        return array( 'priority' => $priority, 'freq' => $freq );
    }

    if ( 'post' === $type ) {
        // Blog posts: priority 0.7, changefreq depends on age.
        $priority = 0.7;
        $post_id  = isset( $ctx['post_id'] ) ? (int) $ctx['post_id'] : 0;
        if ( $post_id > 0 ) {
            $published = (int) get_post_time( 'U', true, $post_id );
            $age_days  = $published > 0 ? floor( ( time() - $published ) / DAY_IN_SECONDS ) : 999999;

            if ( $age_days < 14 ) {
                $freq = 'daily';
            } elseif ( $age_days < 60 ) {
                $freq = 'weekly';
            } elseif ( $age_days < 183 ) { // ~2-6 months.
                $freq = 'monthly';
            } else {
                // Protocol does not support "with traffic" condition here; default monthly.
                $freq = 'monthly';
            }
        } else {
            $freq = 'weekly';
        }

        return array( 'priority' => $priority, 'freq' => $freq );
    }

    if ( 'page' === $type ) {
        $post_id = isset( $ctx['post_id'] ) ? (int) $ctx['post_id'] : 0;
        $url     = isset( $ctx['url'] ) ? (string) $ctx['url'] : '';
        $slug    = '';
        if ( $post_id > 0 ) {
            $post = get_post( $post_id );
            if ( $post && ! is_wp_error( $post ) ) {
                $slug = (string) $post->post_name;
            }
        }

        $path = strtolower( (string) wp_parse_url( $url, PHP_URL_PATH ) );

        // Blog listing page.
        if ( ! empty( $ctx['is_posts_page'] ) ) {
            $priority = 0.6;
            $freq     = 'weekly';
            return array( 'priority' => $priority, 'freq' => $freq );
        }

        // FAQ.
        if ( false !== strpos( $slug, 'faq' ) || false !== strpos( $path, '/faq' ) ) {
            $priority = 0.5;
            $freq     = 'monthly';
            return array( 'priority' => $priority, 'freq' => $freq );
        }

        // Testimonials / Reviews.
        if ( false !== strpos( $slug, 'testimonial' ) || false !== strpos( $slug, 'review' ) || false !== strpos( $path, '/testimonials' ) || false !== strpos( $path, '/reviews' ) ) {
            $priority = 0.5;
            $freq     = 'monthly';
            return array( 'priority' => $priority, 'freq' => $freq );
        }

        // About, Contact, Team.
        if ( in_array( $slug, array( 'about', 'contact', 'team' ), true ) || false !== strpos( $path, '/about' ) || false !== strpos( $path, '/contact' ) || false !== strpos( $path, '/team' ) ) {
            $priority = 0.4;
            $freq     = 'monthly';
            return array( 'priority' => $priority, 'freq' => $freq );
        }

        // Legal / policies.
        $legal_markers = array( 'privacy', 'terms', 'policy', 'cookies', 'cookie', 'legal', 'gdpr', 'disclaimer' );
        foreach ( $legal_markers as $mk ) {
            if ( false !== strpos( $slug, $mk ) || false !== strpos( $path, '/' . $mk ) ) {
                $priority = 0.1;
                $freq     = myaio_sac_sitemap_normalize_changefreq( 'six-month' ); // becomes yearly.
                return array( 'priority' => $priority, 'freq' => $freq );
            }
        }

        // Service/product pages: heuristic.
        $money_markers = array( 'service', 'services', 'product', 'products', 'pricing', 'plans', 'solutions' );
        foreach ( $money_markers as $mk ) {
            if ( false !== strpos( $slug, $mk ) || false !== strpos( $path, '/' . $mk ) ) {
                $priority = 0.9;
                $freq     = 'monthly';
                return array( 'priority' => $priority, 'freq' => $freq );
            }
        }

        // Location landing pages: heuristic.
        if ( false !== strpos( $path, '/location' ) || false !== strpos( $path, '/locations' ) ) {
            $priority = 0.9;
            $freq     = 'monthly';
            return array( 'priority' => $priority, 'freq' => $freq );
        }

        // Default other pages.
        $priority = 0.4;
        $freq     = 'monthly';
        return array( 'priority' => $priority, 'freq' => $freq );
    }

    // Fallback.
    return array( 'priority' => $priority, 'freq' => $freq );
}

/**
 * Sort sitemap items by priority (desc) then URL (asc).
 *
 * @param array $items Items.
 * @return array
 */
function myaio_sac_sitemap_sort_items( array $items ) {
    usort(
        $items,
        function ( $a, $b ) {
            $pa = isset( $a['priority'] ) ? (float) $a['priority'] : 0.0;
            $pb = isset( $b['priority'] ) ? (float) $b['priority'] : 0.0;

            if ( $pa === $pb ) {
                $la = isset( $a['loc'] ) ? (string) $a['loc'] : '';
                $lb = isset( $b['loc'] ) ? (string) $b['loc'] : '';
                return strnatcasecmp( $la, $lb );
            }
            return ( $pa > $pb ) ? -1 : 1;
        }
    );

    return $items;
}

/**
 * Build a standard sitemap item array.
 *
 * @param string      $loc URL.
 * @param string|null $lastmod ISO 8601.
 * @param array       $ctx Classification context for tags.
 * @return array|null Item or null if excluded.
 */
function myaio_sac_sitemap_build_item( $loc, $lastmod, array $ctx ) {
    $loc = trim( (string) $loc );
    if ( '' === $loc ) {
        return null;
    }

    if ( myaio_sac_sitemap_is_hard_excluded_url( $loc ) ) {
        return null;
    }

    if ( ! myaio_sac_sitemap_url_is_http_200( $loc ) ) {
        return null;
    }

    $ctx['url'] = $loc;
    $tags       = myaio_sac_sitemap_get_tags( $ctx );

    $freq = isset( $tags['freq'] ) ? myaio_sac_sitemap_normalize_changefreq( $tags['freq'] ) : null;

    return array(
        'loc'      => $loc,
        'lastmod'  => $lastmod,
        'freq'     => $freq,
        'priority' => isset( $tags['priority'] ) ? (string) $tags['priority'] : '',
    );
}

/**
 * Try to enrich an existing sitemap URL (string) into an item with tags.
 * This helps when we keep previously-present URLs during merge.
 *
 * @param string $url URL.
 * @return array|null
 */
function myaio_sac_sitemap_enrich_from_url( $url ) {
    $url = trim( (string) $url );
    if ( '' === $url ) {
        return null;
    }

    if ( myaio_sac_sitemap_is_hard_excluded_url( $url ) ) {
        return null;
    }

    // Only include if it resolves cleanly.
    if ( ! myaio_sac_sitemap_url_is_http_200( $url ) ) {
        return null;
    }

    $post_id = url_to_postid( $url );
    if ( $post_id > 0 && myaio_sac_is_indexable_post( $post_id ) ) {
        $type = ( 'post' === get_post_type( $post_id ) ) ? 'post' : 'page';
        return myaio_sac_sitemap_build_item(
            get_permalink( $post_id ),
            get_post_modified_time( 'c', true, $post_id ),
            array(
                'type'    => $type,
                'post_id' => (int) $post_id,
                'is_posts_page' => ( (int) get_option( 'page_for_posts' ) === (int) $post_id ),
            )
        );
    }

    // Try category/tag archives.
    $term = null;
    $term_id = 0;
    $tax = '';
    $maybe = wp_parse_url( $url, PHP_URL_PATH );
    if ( $maybe ) {
        $term_id = 0;
        // No reliable reverse mapping without rewrite rules; skip.
    }

    // If it matches homepage or posts page by URL.
    if ( myaio_sac_norm_url( $url ) === myaio_sac_norm_url( home_url( '/' ) ) ) {
        return myaio_sac_sitemap_build_item( home_url( '/' ), myaio_sac_get_last_modified_for_url( home_url( '/' ) ), array( 'type' => 'home' ) );
    }

    $posts_page_id = (int) get_option( 'page_for_posts' );
    if ( $posts_page_id > 0 ) {
        $posts_url = get_permalink( $posts_page_id );
        if ( $posts_url && myaio_sac_norm_url( $url ) === myaio_sac_norm_url( $posts_url ) ) {
            return myaio_sac_sitemap_build_item( $posts_url, get_post_modified_time( 'c', true, $posts_page_id ), array( 'type' => 'page', 'post_id' => $posts_page_id, 'is_posts_page' => true ) );
        }
    }

    // Unknown URL; keep loc only (no tags) if it's valid.
    return array(
        'loc'     => $url,
        'lastmod' => null,
    );
}




function myaio_sac_collect_posts() {
    $items = array();
    $paged = 1;

    do {
        $q = new WP_Query(
            array(
                'post_type'           => 'post',
                'post_status'         => 'publish',
                'posts_per_page'      => 500,
                'paged'               => $paged,
                'no_found_rows'       => true,
                'ignore_sticky_posts' => true,
                'fields'              => 'ids',
                'orderby'             => 'ID',
                'order'               => 'ASC',
            )
        );

        foreach ( (array) $q->posts as $id ) {
            $id = (int) $id;
            if ( ! myaio_sac_is_indexable_post( $id ) ) {
                continue;
            }

            $loc = get_permalink( $id );
            $it  = myaio_sac_sitemap_build_item(
                $loc,
                get_post_modified_time( 'c', true, $id ),
                array(
                    'type'    => 'post',
                    'post_id' => $id,
                )
            );

            if ( null !== $it ) {
                $items[] = $it;
            }
        }

        $paged++;
    } while ( $q->post_count === 500 );

    wp_reset_postdata();

    // De-dupe by normalized URL.
    $by_loc = array();
    foreach ( $items as $it ) {
        $by_loc[ myaio_sac_norm_url( $it['loc'] ) ] = $it;
    }

    return myaio_sac_sitemap_sort_items( array_values( $by_loc ) );
}



function myaio_sac_collect_pages() {
    $items = array();
    $paged = 1;

    do {
        $q = new WP_Query(
            array(
                'post_type'           => 'page',
                'post_status'         => 'publish',
                'posts_per_page'      => 500,
                'paged'               => $paged,
                'no_found_rows'       => true,
                'fields'              => 'ids',
                'orderby'             => 'ID',
                'order'               => 'ASC',
            )
        );

        foreach ( (array) $q->posts as $id ) {
            $id = (int) $id;

            if ( ! myaio_sac_is_indexable_post( $id ) ) {
                continue;
            }

            $loc = get_permalink( $id );
            $it  = myaio_sac_sitemap_build_item(
                $loc,
                get_post_modified_time( 'c', true, $id ),
                array(
                    'type'          => 'page',
                    'post_id'       => $id,
                    'is_posts_page' => ( (int) get_option( 'page_for_posts' ) === (int) $id ),
                )
            );

            if ( null !== $it ) {
                $items[] = $it;
            }
        }

        $paged++;
    } while ( $q->post_count === 500 );

    wp_reset_postdata();

    // Blog posts listing page should be included even if not in normal page query for some themes.
    $posts_page_id = (int) get_option( 'page_for_posts' );
    if ( $posts_page_id > 0 && myaio_sac_is_indexable_post( $posts_page_id ) ) {
        $posts_url = get_permalink( $posts_page_id );
        if ( $posts_url ) {
            $it = myaio_sac_sitemap_build_item(
                $posts_url,
                get_post_modified_time( 'c', true, $posts_page_id ),
                array(
                    'type'          => 'page',
                    'post_id'       => $posts_page_id,
                    'is_posts_page' => true,
                )
            );
            if ( null !== $it ) {
                $items[] = $it;
            }
        }
    }

    // De-dupe.
    $by_loc = array();
    foreach ( $items as $it ) {
        $by_loc[ myaio_sac_norm_url( $it['loc'] ) ] = $it;
    }

    return myaio_sac_sitemap_sort_items( array_values( $by_loc ) );
}



function myaio_sac_collect_categories() {
    $items = array();

    $terms = get_terms(
        array(
            'taxonomy'   => 'category',
            'hide_empty' => true,
            'fields'     => 'ids',
        )
    );
    if ( is_wp_error( $terms ) ) {
        return $items;
    }

    foreach ( $terms as $term_id ) {
        $term_id = (int) $term_id;

        if ( ! myaio_sac_is_indexable_term( $term_id, 'category' ) ) {
            continue;
        }

        $loc = get_term_link( $term_id, 'category' );
        if ( is_wp_error( $loc ) || empty( $loc ) ) {
            continue;
        }

        // Get lastmod as newest modified post in category.
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
        $latest = new WP_Query(
            array(
                'post_type'      => 'post',
                'post_status'    => 'publish',
                'posts_per_page' => 1,
                'no_found_rows'  => true,
                'orderby'        => 'modified',
                'order'          => 'DESC',
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
                'tax_query'      => array(
                    array(
                        'taxonomy' => 'category',
                        'field'    => 'term_id',
                        'terms'    => $term_id,
                    ),
                ),
                'fields'         => 'ids',
            )
        );

        $lastmod = null;
        if ( $latest->have_posts() ) {
            $pid     = (int) $latest->posts[0];
            $lastmod = get_post_modified_time( 'c', true, $pid );
        }
        wp_reset_postdata();

        $it = myaio_sac_sitemap_build_item(
            $loc,
            $lastmod,
            array(
                'type'    => 'category',
                'term_id' => $term_id,
            )
        );

        if ( null !== $it ) {
            $items[] = $it;
        }
    }

    // De-dupe.
    $by_loc = array();
    foreach ( $items as $it ) {
        $by_loc[ myaio_sac_norm_url( $it['loc'] ) ] = $it;
    }

    return myaio_sac_sitemap_sort_items( array_values( $by_loc ) );
}

/**
 * Collect additional sitemap URLs required by the refinement spec:
 * - Homepage
 * - Blog listing (page_for_posts)
 * - Tag archives
 * - Author archives
 * - Pagination (limited)
 *
 * @return array
 */
function myaio_sac_collect_misc() {
    $items = array();

    // Homepage.
    $home_url = home_url( '/' );
    $home_it  = myaio_sac_sitemap_build_item(
        $home_url,
        myaio_sac_get_last_modified_for_url( $home_url ),
        array(
            'type' => 'home',
        )
    );
    if ( null !== $home_it ) {
        $items[] = $home_it;
    }

    // Tags (tag archives).
    $tag_ids = get_terms(
        array(
            'taxonomy'   => 'post_tag',
            'hide_empty' => true,
            'fields'     => 'ids',
        )
    );
    if ( ! is_wp_error( $tag_ids ) ) {
        foreach ( (array) $tag_ids as $tid ) {
            $tid = (int) $tid;
            $loc = get_term_link( $tid, 'post_tag' );
            if ( is_wp_error( $loc ) || empty( $loc ) ) {
                continue;
            }

            $it = myaio_sac_sitemap_build_item(
                $loc,
                null,
                array(
                    'type'    => 'tag_archive',
                    'term_id' => $tid,
                )
            );
            if ( null !== $it ) {
                $items[] = $it;
            }

            // Pagination for tags (limited).
            $items = array_merge( $items, myaio_sac_collect_archive_pagination_urls( $loc, 'pagination' ) );
        }
    }

    // Authors.
    $authors = get_users(
        array(
            'fields'  => array( 'ID' ),
            'who'     => 'authors',
            'orderby' => 'ID',
            'order'   => 'ASC',
        )
    );

    foreach ( (array) $authors as $u ) {
        $uid = isset( $u->ID ) ? (int) $u->ID : 0;
        if ( $uid <= 0 ) {
            continue;
        }

        // Only include authors with published posts.
        $count = (int) count_user_posts( $uid, 'post', true );
        if ( $count <= 0 ) {
            continue;
        }

        $loc = get_author_posts_url( $uid );
        if ( empty( $loc ) ) {
            continue;
        }

        $it = myaio_sac_sitemap_build_item(
            $loc,
            null,
            array(
                'type' => 'author_archive',
            )
        );
        if ( null !== $it ) {
            $items[] = $it;
        }

        $items = array_merge( $items, myaio_sac_collect_archive_pagination_urls( $loc, 'pagination' ) );
    }

    // Blog listing pagination (page_for_posts).
    $posts_page_id = (int) get_option( 'page_for_posts' );
    if ( $posts_page_id > 0 ) {
        $posts_url = get_permalink( $posts_page_id );
        if ( $posts_url ) {
            $items = array_merge( $items, myaio_sac_collect_archive_pagination_urls( $posts_url, 'pagination', true ) );
        }
    }

    // Category pagination (limited).
    $cats = get_terms(
        array(
            'taxonomy'   => 'category',
            'hide_empty' => true,
            'fields'     => 'ids',
        )
    );
    if ( ! is_wp_error( $cats ) ) {
        foreach ( (array) $cats as $cid ) {
            $cid = (int) $cid;
            $loc = get_term_link( $cid, 'category' );
            if ( is_wp_error( $loc ) || empty( $loc ) ) {
                continue;
            }
            $items = array_merge( $items, myaio_sac_collect_archive_pagination_urls( $loc, 'pagination' ) );
        }
    }

    // De-dupe + sort.
    $by_loc = array();
    foreach ( $items as $it ) {
        if ( empty( $it['loc'] ) ) {
            continue;
        }
        $by_loc[ myaio_sac_norm_url( $it['loc'] ) ] = $it;
    }

    return myaio_sac_sitemap_sort_items( array_values( $by_loc ) );
}

/**
 * Generate pagination URLs for an archive base URL (limited to reduce bloat).
 *
 * @param string $base_url Base archive URL.
 * @param string $type Type label for tags.
 * @param bool   $is_blog_listing Whether this is the blog listing (controls page count source).
 * @return array
 */
function myaio_sac_collect_archive_pagination_urls( $base_url, $type = 'pagination', $is_blog_listing = false ) {
    $items = array();

    $base_url = trim( (string) $base_url );
    if ( '' === $base_url ) {
        return $items;
    }

    $ppp = (int) get_option( 'posts_per_page' );
    if ( $ppp <= 0 ) {
        $ppp = 10;
    }

    $max_pages = 0;

    if ( $is_blog_listing ) {
        $counts = wp_count_posts( 'post' );
        $total  = isset( $counts->publish ) ? (int) $counts->publish : 0;
        $max_pages = ( $ppp > 0 ) ? (int) ceil( $total / $ppp ) : 0;
    } else {
        // We don't have reliable term counts for every archive URL here; keep small.
        $max_pages = 0;
    }

    // For non-blog archives, still provide a small, safe number of pages to satisfy requirement.
    if ( $max_pages <= 0 ) {
        $max_pages = 3;
    }

    // Cap to avoid huge sitemaps.
    $max_pages = min( 5, $max_pages );

    // Page 1 is the base URL; start at 2.
    for ( $p = 2; $p <= $max_pages; $p++ ) {
        $page_url = trailingslashit( $base_url ) . 'page/' . $p . '/';
        $it       = myaio_sac_sitemap_build_item(
            $page_url,
            null,
            array(
                'type' => $type,
            )
        );
        if ( null !== $it ) {
            $items[] = $it;
        }
    }

    return $items;
}



/** ========= Extra: guess folder children for /sitemap.xml/ ========= **/
function myaio_sac_guess_folder_children($folder_url) {
    // Support both "sitemap-posts.xml" style and "post-sitemap.xml" style + chunk variants
    $bases = [
        'sitemap-posts', 'post-sitemap',
        'sitemap-pages', 'page-sitemap',
        'sitemap-categories', 'category-sitemap',
        'sitemap-tags', 'tag-sitemap',
        'sitemap-authors', 'author-sitemap',
    ];
    $found = [];

    foreach ($bases as $base) {
        // base.xml
        $u = rtrim($folder_url, '/') . '/' . $base . '.xml';
        if (myaio_sac_url_exists($u)) $found[] = $u;

        // chunk styles: base-2.xml (common) and base2.xml (Yoast-style)
        for ($i = 2; $i <= 25; $i++) {
            $u1 = rtrim($folder_url, '/') . '/' . $base . '-' . $i . '.xml';
            $u2 = rtrim($folder_url, '/') . '/' . $base . $i . '.xml';
            $hit = false;
            if (myaio_sac_url_exists($u1)) { $found[] = $u1; $hit = true; }
            if (myaio_sac_url_exists($u2)) { $found[] = $u2; $hit = true; }
            if (!$hit) break; // stop scanning if next chunk not found
        }
    }
    return array_values(array_unique($found));
}

/** ========= VIRTUAL SERVE: always serve /sitemaps/sitemap.xml ========= **/

// 1) Add rewrite rule: /sitemaps/sitemap.xml -> index.php?myaio_sac_sitemap=1
function myaio_sac_register_virtual_sitemap() {
    add_rewrite_rule('^sitemaps/sitemap\.xml$', 'index.php?myaio_sac_sitemap=1', 'top');
}
add_action('init', 'myaio_sac_register_virtual_sitemap');

// 2) Make the query var recognized
function myaio_sac_query_vars($vars) {
    $vars[] = 'myaio_sac_sitemap';
    return $vars;
}
add_filter('query_vars', 'myaio_sac_query_vars');

// 3) One-time flush to activate the new rule (safe & idempotent)
add_action('init', function () {
    if (!get_option('myaio_sac_rewrite_flushed')) {
        flush_rewrite_rules(false);
        update_option('myaio_sac_rewrite_flushed', 1, false);
    }
}, 20);

// 4) Locate the physical sitemap file (webroot /sitemaps/ preferred; fallback to uploads)
function myaio_sac_locate_sitemap_file() {
    $where   = myaio_sac_resolve_sitemaps_dir();
    $root    = $where['dir'] . 'sitemap.xml'; // this will be ABSPATH/sitemaps/ if writable
    $uploads = wp_get_upload_dir();
    $u_path  = trailingslashit($uploads['basedir']) . 'sitemaps/sitemap.xml';

    $path = null;
    if (is_readable($root)) {
        $path = $root;
    } elseif (is_readable($u_path)) {
        $path = $u_path;
    }

    return [
        'path'  => $path,
        'mtime' => ($path && file_exists($path)) ? filemtime($path) : 0,
        'url'   => home_url('/sitemaps/sitemap.xml') // canonical public URL
    ];
}

// 5) Stream the file when /sitemaps/sitemap.xml is requested
function myaio_sac_serve_virtual_sitemap() {
    if ( get_query_var( 'myaio_sac_sitemap' ) !== '1' ) {
        return;
    }

    $info = myaio_sac_locate_sitemap_file();

    global $wp_filesystem;
    if ( ! function_exists( 'WP_Filesystem' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    WP_Filesystem();

    if ( empty( $info['path'] ) || ! $wp_filesystem->is_readable( $info['path'] ) ) {
        status_header( 404 );
        header( 'Content-Type: application/xml; charset=UTF-8' );
        echo '<?xml version="1.0" encoding="UTF-8"?><error>Not Found</error>';
        exit;
    }

    // Output headers
    $mtime = (int) $info['mtime'];
    if ( $mtime > 0 ) {
        header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $mtime ) . ' GMT' );
    }

    header( 'Content-Type: application/xml; charset=UTF-8' );
    //header( 'Cache-Control: public, max-age=600' ); // 10 minutes
    header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
    // Stream contents using WP_Filesystem
    $content = $wp_filesystem->get_contents( $info['path'] );
    
    // Remove xml-stylesheet processing instruction if present (from RankMath or other SEO plugins)
    // This removes the entire xml-stylesheet tag regardless of the href URL
    $content = preg_replace('/<\?xml-stylesheet\s+[^?]*\?>\s*/i', '', $content);
    
    // Replace RankMath branding with MYAIO branding
    $content = str_ireplace('rankmath.com', 'myaio.com', $content);
    $content = str_replace('Rank Math', 'MYAIO', $content);
    
    echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML is generated/escaped in renderer.
    exit;
}

add_action('template_redirect', 'myaio_sac_serve_virtual_sitemap');

/** ========= CHECK ENDPOINT (discover + count all sitemaps) ========= **/
function myaio_sac_check_sitemap(WP_REST_Request $request) {
    // Cache for client sites to avoid repeated heavy crawling
    $cache_key = 'myaio_sac_check_sitemap_cache_v1';
    // $cached = get_transient($cache_key);
    // if (is_array($cached)) return rest_ensure_response($cached);

    $canonical_url = home_url('/sitemaps/sitemap.xml');

    // Prefer the last URL we wrote during generation (but we REPORT the canonical)
    $saved = get_option('myaio_sac_last_sitemap_url');

    $where   = myaio_sac_resolve_sitemaps_dir();
    $uploads = wp_get_upload_dir();

    // Discover sitemaps (robots.txt first, then fallback URLs)
    $checked = [];
    $exists  = [];

    // 1) robots.txt sitemap discovery (MUST be first)
    $robots_url  = home_url('/robots.txt');
    $robots_info = myaio_sac_get_sitemaps_from_robots($robots_url);

    if (!empty($robots_info['sitemaps'])) {
        foreach ($robots_info['sitemaps'] as $u) {
            $checked[] = $u;
            if (myaio_sac_url_exists($u)) {
                $exists[] = $u;
            }
        }
    }

    // 2) Fallback to common sitemap URLs (in order) ONLY if robots had none (or none were reachable)
    if (empty($exists)) {
        $fallbacks = [
            home_url('/sitemap.xml'),
            home_url('/sitemap_index.xml'),
            home_url('/wp-sitemap.xml'), // WordPress core sitemap
        ];

        foreach ($fallbacks as $u) {
            $checked[] = $u;
            if (myaio_sac_url_exists($u)) {
                $exists[] = $u;
                break; // sequential check: stop at first hit
            }
        }
    }


    // 2b) Check the plugin's canonical sitemap URL (keeps generator compatible)
    //     Still minimal: at most +1 request beyond the required fallbacks.
    if (empty($exists)) {
        $checked[] = $canonical_url;
        if (myaio_sac_url_exists($canonical_url)) {
            $exists[] = $canonical_url;
        }
    }

    // 3) Do NOT probe additional locations to avoid timeouts.
    //    Keep a compat value for the response payload (historically a cap).
    $max_candidate_checks = count($checked);
    //    Keep folder fields for backward-compatible response shape.
    $guessed_children = [];

    // Crawl sitemaps if anything responds
    if (empty($exists)) {
        $resp = [
            'available'               => false,
            'message'                 => 'No sitemap found at known locations (including robots.txt).',
            'sitemap_url'             => $canonical_url,
            'previous_sitemap_url'    => get_option('myaio_sac_prev_sitemap_url') ?: ($saved ?: null),
            'last_modified'           => null,
            'previous_last_modified'  => myaio_sac_iso8601((int) (get_option('myaio_sac_prev_sitemap_mtime') ?: get_option('myaio_sac_last_sitemap_mtime'))),
            'checked'                 => $checked,
            'sitemap_files_count'     => 0,
            'sitemaps_found'          => [],
            'total_url_count'         => 0,
            'unique_url_count'        => 0,
            'folder_children_count'   => count($guessed_children),
            'folder_children_samples' => array_slice($guessed_children, 0, 20),
            'robots'                  => [
                'url'      => $robots_url,
                'ok'       => (bool)($robots_info['ok'] ?? false),
                'status'   => (int)($robots_info['status'] ?? 0),
                'sitemaps' => array_values(array_unique($robots_info['sitemaps'] ?? [])),
                'error'    => $robots_info['error'] ?? null,
            ],
        ];
        set_transient($cache_key, $resp, 10 * MINUTE_IN_SECONDS);
        return rest_ensure_response($resp);
    }

    // Crawl sitemaps: support index (recursive) and urlset
    $visited    = [];
    $queue      = $exists;     // start from seed(s)
    $sitemaps   = [];
    $all_urls   = [];
    $safe_loops = 0;

    // Safety caps for client sites
    $max_nodes = 60;     // max sitemap documents fetched
    $max_urls  = 20000;  // max unique URLs tracked (memory/time safety)
    $partial   = false;

    while (!empty($queue) && $safe_loops < $max_nodes) {
        $next = array_shift($queue);
        $norm = myaio_sac_norm_url($next);
        if (isset($visited[$norm])) { $safe_loops++; continue; }
        $visited[$norm] = true;

        $xml  = myaio_sac_fetch_xml($next); // returns SimpleXMLElement or null
        if (!$xml) {
            $sitemaps[] = ['url' => $next, 'type' => 'fetch-failed', 'url_count' => null];
            $safe_loops++;
            continue;
        }

        $type = myaio_sac_parse_sitemap_type($xml);

        if ($type === 'index') {
            $children = myaio_sac_extract_index_children($xml);
            $sitemaps[] = ['url' => $next, 'type' => 'index', 'url_count' => null];

            // IMPORTANT: don't call myaio_sac_url_exists() for each child (saves tons of requests)
            foreach ($children as $child) {
                $cn = myaio_sac_norm_url($child);
                if (!isset($visited[$cn])) $queue[] = $child;
            }
        } elseif ($type === 'urlset') {
            $urls = myaio_sac_extract_urlset_urls($xml);
            $sitemaps[] = ['url' => $next, 'type' => 'urlset', 'url_count' => count($urls)];

            foreach ($urls as $u) {
                $all_urls[myaio_sac_norm_url($u)] = true;
                if (count($all_urls) >= $max_urls) { $partial = true; break; }
            }
            if ($partial) break;
        } else {
            $sitemaps[] = ['url' => $next, 'type' => 'unknown', 'url_count' => null];
        }

        $safe_loops++;
    }

    if ($safe_loops >= $max_nodes) $partial = true;

    // Totals
    $sum_counts   = 0;
    $file_count   = count($sitemaps);
    $unique_total = count($all_urls);
    foreach ($sitemaps as $s) {
        if ($s['type'] === 'urlset' && isset($s['url_count'])) {
            $sum_counts += (int)$s['url_count'];
        }
    }

    // Last modified (from physical file if present)
    $loc = myaio_sac_locate_sitemap_file();
    $curr_mtime = (int) (get_option('myaio_sac_last_sitemap_mtime') ?: 0);
    if (!empty($loc['mtime']) && (int)$loc['mtime'] > 0) $curr_mtime = (int)$loc['mtime'];

    $prev_url   = get_option('myaio_sac_prev_sitemap_url') ?: null;
    $prev_mtime = (int) (get_option('myaio_sac_prev_sitemap_mtime') ?: 0);

    $resp = [
        'available'               => true,
        'partial'                 => $partial,
        'message'                 => $partial
            ? sprintf('Found %d sitemap file(s) (partial crawl due to safety limits); total URLs (non-deduped across files): %d; unique URLs: %d.', $file_count, $sum_counts, $unique_total)
            : sprintf('Found %d sitemap file(s); total URLs (non-deduped across files): %d; unique URLs: %d.', $file_count, $sum_counts, $unique_total),
        'sitemap_url'             => $canonical_url, // always the canonical path
        'previous_sitemap_url'    => $prev_url ?: ($saved ?: null),
        'last_modified'           => myaio_sac_iso8601($curr_mtime),
        'previous_last_modified'  => myaio_sac_iso8601($prev_mtime ?: (int) get_option('myaio_sac_last_sitemap_mtime')),
        'sitemap_files_count'     => $file_count,
        'sitemaps_found'          => $sitemaps,
        'total_url_count'         => $sum_counts,
        'unique_url_count'        => $unique_total,
        'folder_children_count'   => count($guessed_children),
        'folder_children_samples' => array_slice($guessed_children, 0, 20),
        'checked'                 => $checked,
        'robots'                  => [
            'url'      => $robots_url,
            'ok'       => (bool)($robots_info['ok'] ?? false),
            'status'   => (int)($robots_info['status'] ?? 0),
            'sitemaps' => array_values(array_unique($robots_info['sitemaps'] ?? [])),
            'error'    => $robots_info['error'] ?? null,
        ],
        'limits'                  => [
            'max_candidate_checks' => $max_candidate_checks,
            'max_nodes'            => $max_nodes,
            'max_urls'             => $max_urls,
        ],
    ];

    set_transient($cache_key, $resp, 10 * MINUTE_IN_SECONDS);
    return rest_ensure_response($resp);
}


/**
 * NEW: Parse robots.txt and extract Sitemap: lines
 */
function myaio_sac_get_sitemaps_from_robots($robots_url) {
    $out = [
        'ok'       => false,
        'status'   => 0,
        'sitemaps' => [],
        'error'    => null,
    ];

    $res = wp_remote_get($robots_url, [
        'timeout'     => 5,
        'redirection' => 2,
        'sslverify'   => true,
        'headers'     => [
            'User-Agent' => 'Mozilla/5.0 (WordPress REST Sitemap Checker)',
            'Accept'     => 'text/plain',
        ],
    ]);

    if (is_wp_error($res)) {
        $out['error'] = $res->get_error_message();
        return $out;
    }

    $out['ok']     = true;
    $out['status'] = (int) wp_remote_retrieve_response_code($res);

    $body = (string) wp_remote_retrieve_body($res);
    if ($body === '') return $out;

    // Extract lines like: Sitemap: https://example.com/sitemap.xml
    // Be tolerant: casing + extra spaces
    if (preg_match_all('/^\s*sitemap\s*:\s*(\S+)\s*$/im', $body, $m)) {
        foreach (($m[1] ?? []) as $u) {
            $u = trim($u);
            if ($u === '') continue;

            // If robots contains relative (rare), convert to absolute
            if (strpos($u, 'http://') !== 0 && strpos($u, 'https://') !== 0) {
                $u = home_url('/' . ltrim($u, '/'));
            }

            $out['sitemaps'][] = esc_url_raw($u);
        }
    }

    $out['sitemaps'] = array_values(array_unique(array_filter($out['sitemaps'])));
    return $out;
}



/**
 * Convert a site-local URL to a filesystem path under ABSPATH (webroot).
 * Returns null if the URL is not on this host or path cannot be resolved safely.
 */
function myaio_sac_url_to_local_path($url) {
    $url = trim((string)$url);
    if ($url === '') return null;

    $u = wp_parse_url($url);
    $h = wp_parse_url(home_url('/'));
    if (!$u || !$h) return null;

    $u_host = strtolower($u['host'] ?? '');
    $h_host = strtolower($h['host'] ?? '');
    if ($u_host === '' || $h_host === '' || $u_host !== $h_host) return null;

    $path = $u['path'] ?? '';
    if ($path === '' || $path[0] !== '/') return null;

    // Disallow parent traversal
    if (strpos($path, '..') !== false) return null;

    $local = ABSPATH . ltrim($path, '/');
    return $local;
}

/**
 * Ensure robots.txt references the sitemap URL.
 * Tries to write a physical robots.txt in ABSPATH; if not possible, stores a value to be appended via robots_txt filter.
 */
function myaio_sac_ensure_robots_has_sitemap($sitemap_url) {
    $sitemap_url = esc_url_raw($sitemap_url);
    if (!$sitemap_url) return ['ok' => false, 'mode' => null, 'error' => 'Invalid sitemap URL'];

    $line = "Sitemap: " . $sitemap_url;

    $robots_path = ABSPATH . 'robots.txt';
    $can_write = (file_exists($robots_path) && wp_is_writable($robots_path))
        || (!file_exists($robots_path) && wp_is_writable(ABSPATH));

    if ($can_write) {
        $body = file_exists($robots_path) ? (string) @file_get_contents($robots_path) : "";
        // If already present (case-insensitive), nothing to do.
        if (preg_match('/^\s*sitemap\s*:\s*' . preg_quote($sitemap_url, '/') . '\s*$/im', $body)) {
            // Also store option for virtual robots (harmless)
            update_option('myaio_sac_robots_sitemap_url', $sitemap_url, false);
            return ['ok' => true, 'mode' => 'file', 'updated' => false];
        }

        // Append on a new line (keep existing sitemaps intact)
        $sep = ($body !== '' && substr($body, -1) !== "\n") ? "\n" : "";
        $body .= $sep . $line . "\n";
        $ok = (bool) @file_put_contents($robots_path, $body);

        // Store option too, so WP virtual robots also includes it if used.
        update_option('myaio_sac_robots_sitemap_url', $sitemap_url, false);

        return ['ok' => $ok, 'mode' => 'file', 'updated' => $ok, 'error' => $ok ? null : 'Failed writing robots.txt'];
    }

    // Fallback: add via WP virtual robots output
    update_option('myaio_sac_robots_sitemap_url', $sitemap_url, false);
    return ['ok' => true, 'mode' => 'filter', 'updated' => true, 'error' => null];
}

// Append sitemap line to WordPress virtual robots.txt output when a physical robots.txt cannot be written or isn't used.
add_filter('robots_txt', function ($output, $public) {
    $sitemap_url = get_option('myaio_sac_robots_sitemap_url');
    if (!$sitemap_url) return $output;

    if (preg_match('/^\s*sitemap\s*:\s*' . preg_quote($sitemap_url, '/') . '\s*$/im', (string)$output)) {
        return $output;
    }
    $output = rtrim((string)$output) . "\n" . "Sitemap: " . $sitemap_url . "\n";
    return $output;
}, 9999, 2);

/**
 * Extract URL list from an existing urlset XML string.
 */
function myaio_sac_extract_urlset_urls_from_string($xml_string) {
    $out = [];
    if (!$xml_string) return $out;
    libxml_use_internal_errors(true);
    $xml = simplexml_load_string($xml_string);
    if (!$xml) return $out;
    $type = myaio_sac_parse_sitemap_type($xml);
    if ($type !== 'urlset') return $out;
    return myaio_sac_extract_urlset_urls($xml);
}


/**
 * Read a local XML file safely.
 *
 * @param string $path File path.
 * @return SimpleXMLElement|null
 */
function myaio_sac_read_local_xml_file( $path ) {
    $path = (string) $path;

    if ( '' === $path || ! file_exists( $path ) || ! is_readable( $path ) ) {
        return null;
    }

    $contents = (string) @file_get_contents( $path );
    if ( '' === $contents ) {
        return null;
    }

    libxml_use_internal_errors( true );
    $xml = simplexml_load_string( $contents );
    libxml_clear_errors();

    return $xml ?: null;
}

/**
 * Extract full urlset items from XML string, including image:image entries.
 *
 * @param string $xml_string XML contents.
 * @return array
 */
function myaio_sac_extract_urlset_items_from_string( $xml_string ) {
    $items = [];

    if ( empty( $xml_string ) ) {
        return $items;
    }

    libxml_use_internal_errors( true );
    $xml = simplexml_load_string( $xml_string );
    libxml_clear_errors();

    if ( ! $xml || 'urlset' !== myaio_sac_parse_sitemap_type( $xml ) ) {
        return $items;
    }

    $nodes = $xml->xpath( '/*[local-name()="urlset"]/*[local-name()="url"]' );
    if ( ! is_array( $nodes ) ) {
        return $items;
    }

    foreach ( $nodes as $node ) {
        $loc_nodes = $node->xpath( './*[local-name()="loc"]' );
        $loc       = ( is_array( $loc_nodes ) && ! empty( $loc_nodes ) ) ? trim( (string) $loc_nodes[0] ) : '';

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

        $lastmod_nodes  = $node->xpath( './*[local-name()="lastmod"]' );
        $freq_nodes     = $node->xpath( './*[local-name()="changefreq"]' );
        $priority_nodes = $node->xpath( './*[local-name()="priority"]' );
        $image_nodes    = $node->xpath( './*[local-name()="image"]/*[local-name()="loc"]' );

        $images = [];
        if ( is_array( $image_nodes ) ) {
            foreach ( $image_nodes as $image_node ) {
                $image_url = trim( (string) $image_node );
                if ( '' !== $image_url ) {
                    $images[] = $image_url;
                }
            }
        }

        $items[] = [
            'loc'      => $loc,
            'lastmod'  => ( is_array( $lastmod_nodes ) && ! empty( $lastmod_nodes ) ) ? trim( (string) $lastmod_nodes[0] ) : null,
            'freq'     => ( is_array( $freq_nodes ) && ! empty( $freq_nodes ) ) ? trim( (string) $freq_nodes[0] ) : null,
            'priority' => ( is_array( $priority_nodes ) && ! empty( $priority_nodes ) ) ? trim( (string) $priority_nodes[0] ) : '',
            'images'   => array_values( array_unique( $images ) ),
        ];
    }

    return $items;
}

/**
 * Read urlset items from a local file.
 *
 * @param string $path File path.
 * @return array
 */
function myaio_sac_read_urlset_items_from_path( $path ) {
    $path = (string) $path;

    if ( '' === $path || ! file_exists( $path ) || ! is_readable( $path ) ) {
        return [];
    }

    $contents = (string) @file_get_contents( $path );
    if ( '' === $contents ) {
        return [];
    }

    return myaio_sac_extract_urlset_items_from_string( $contents );
}

/**
 * Normalize incoming sitemap type.
 *
 * @param string $type Raw type.
 * @return string
 */
function myaio_sac_normalize_payload_type( $type ) {
    $type = strtolower( trim( (string) $type ) );

    if ( in_array( $type, [ 'post', 'posts' ], true ) ) {
        return 'post';
    }

    if ( in_array( $type, [ 'page', 'pages' ], true ) ) {
        return 'page';
    }

    if ( in_array( $type, [ 'category', 'categories' ], true ) ) {
        return 'category';
    }

    return '';
}

/**
 * Normalize incoming index/noindex flag.
 *
 * Supported shapes:
 * - flag: index|noindex
 * - status: index|noindex
 * - is_noindex: true|false
 * - noindex: true|false
 * - is_index: true|false
 *
 * @param array $row Payload row.
 * @return string
 */
function myaio_sac_normalize_payload_flag( array $row ) {
    if ( isset( $row['flag'] ) ) {
        $flag = strtolower( trim( (string) $row['flag'] ) );
        if ( in_array( $flag, [ 'index', 'noindex' ], true ) ) {
            return $flag;
        }
    }

    if ( isset( $row['status'] ) ) {
        $flag = strtolower( trim( (string) $row['status'] ) );
        if ( in_array( $flag, [ 'index', 'noindex' ], true ) ) {
            return $flag;
        }
    }

    if ( isset( $row['is_noindex'] ) ) {
        return filter_var( $row['is_noindex'], FILTER_VALIDATE_BOOLEAN ) ? 'noindex' : 'index';
    }

    if ( isset( $row['noindex'] ) ) {
        return filter_var( $row['noindex'], FILTER_VALIDATE_BOOLEAN ) ? 'noindex' : 'index';
    }

    if ( isset( $row['is_index'] ) ) {
        return filter_var( $row['is_index'], FILTER_VALIDATE_BOOLEAN ) ? 'index' : 'noindex';
    }

    return '';
}

/**
 * Remove a URL from a sitemap item list.
 *
 * @param array  $items Items.
 * @param string $url   URL to remove.
 * @return array
 */
function myaio_sac_remove_url_from_items( array $items, $url ) {
    $norm = myaio_sac_norm_url( $url );
    $out  = [];

    foreach ( $items as $item ) {
        $loc = isset( $item['loc'] ) ? (string) $item['loc'] : '';
        if ( '' === $loc ) {
            continue;
        }

        if ( myaio_sac_norm_url( $loc ) === $norm ) {
            continue;
        }

        $out[] = $item;
    }

    return $out;
}

/**
 * Dedupe sitemap items by loc.
 *
 * @param array $items Items.
 * @return array
 */
function myaio_sac_dedupe_items_by_loc( array $items ) {
    $out = [];

    foreach ( $items as $item ) {
        if ( empty( $item['loc'] ) ) {
            continue;
        }

        $out[ myaio_sac_norm_url( $item['loc'] ) ] = $item;
    }

    return array_values( $out );
}

/**
 * Map type to child sitemap filename.
 *
 * @param string $type Type.
 * @return string
 */
function myaio_sac_child_basename_for_type( $type ) {
    if ( 'post' === $type ) {
        return 'post-sitemap.xml';
    }

    if ( 'page' === $type ) {
        return 'page-sitemap.xml';
    }

    if ( 'category' === $type ) {
        return 'category-sitemap.xml';
    }

    return '';
}

/**
 * Detect sitemap type from filename.
 *
 * @param string $basename Filename.
 * @return string
 */
function myaio_sac_detect_child_type_from_basename( $basename ) {
    $basename = strtolower( trim( (string) $basename ) );

    if ( false !== strpos( $basename, 'post-sitemap' ) ) {
        return 'post';
    }

    if ( false !== strpos( $basename, 'page-sitemap' ) ) {
        return 'page';
    }

    if ( false !== strpos( $basename, 'category-sitemap' ) ) {
        return 'category';
    }

    return '';
}

/**
 * Detect whether ABSPATH is using a single sitemap or indexed sitemaps.
 *
 * Priority:
 * 1) ABSPATH/sitemap_index.xml
 * 2) ABSPATH/sitemap.xml as urlset
 * 3) ABSPATH/sitemap.xml as sitemap index
 *
 * @return array|WP_Error
 */
function myaio_sac_detect_editable_sitemap_structure() {
    $single_path = trailingslashit( ABSPATH ) . 'sitemap.xml';
    $index_path  = trailingslashit( ABSPATH ) . 'sitemap_index.xml';

    if ( file_exists( $index_path ) && is_readable( $index_path ) ) {
        $xml = myaio_sac_read_local_xml_file( $index_path );

        if ( $xml && 'index' === myaio_sac_parse_sitemap_type( $xml ) ) {
            return [
                'mode'       => 'indexed',
                'root_path'  => $index_path,
                'root_url'   => home_url( '/sitemap_index.xml' ),
                'child_urls' => myaio_sac_extract_index_children( $xml ),
            ];
        }
    }

    if ( file_exists( $single_path ) && is_readable( $single_path ) ) {
        $xml = myaio_sac_read_local_xml_file( $single_path );

        if ( $xml ) {
            $type = myaio_sac_parse_sitemap_type( $xml );

            if ( 'urlset' === $type ) {
                return [
                    'mode'        => 'single',
                    'single_path' => $single_path,
                    'single_url'  => home_url( '/sitemap.xml' ),
                ];
            }

            if ( 'index' === $type ) {
                return [
                    'mode'       => 'indexed',
                    'root_path'  => $single_path,
                    'root_url'   => home_url( '/sitemap.xml' ),
                    'child_urls' => myaio_sac_extract_index_children( $xml ),
                ];
            }
        }
    }

    return new WP_Error(
        'myaio_sitemap_not_editable',
        'No editable physical sitemap.xml or sitemap_index.xml file found in ABSPATH.'
    );
}

/**
 * Build an item from incoming payload URL + type.
 *
 * IMPORTANT:
 * - For sync-sitemap-urls, lastmod must be fresh when flag=index.
 * - Existing image entries can be passed back in so re-index does not delete images.
 *
 * @param string      $url     URL.
 * @param string      $type    Type.
 * @param string|null $lastmod Optional forced ISO-8601 lastmod.
 * @param array       $images  Existing image URLs to preserve.
 * @return array|null
 */
function myaio_sac_build_item_for_payload( $url, $type, $lastmod = null, array $images = [] ) {
    $url  = esc_url_raw( trim( (string) $url ) );
    $type = myaio_sac_normalize_payload_type( $type );

    if ( '' === $url || '' === $type ) {
        return null;
    }

    $forced_lastmod = ! empty( $lastmod ) ? (string) $lastmod : gmdate( 'c' );

    if ( in_array( $type, [ 'post', 'page' ], true ) ) {
        $post_id = url_to_postid( $url );

        if ( $post_id > 0 ) {
            $post_url = get_permalink( $post_id );

            $item = myaio_sac_sitemap_build_item(
                $post_url,
                $forced_lastmod,
                [
                    'type'          => $type,
                    'post_id'       => (int) $post_id,
                    'is_posts_page' => ( (int) get_option( 'page_for_posts' ) === (int) $post_id ),
                ]
            );

            if ( is_array( $item ) ) {
                $item['lastmod'] = $forced_lastmod;
                $item['images']  = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $images ) ) ) );
            }

            return $item;
        }
    }

    $item = myaio_sac_sitemap_build_item(
        $url,
        $forced_lastmod,
        [ 'type' => $type ]
    );

    if ( is_array( $item ) ) {
        $item['lastmod'] = $forced_lastmod;
        $item['images']  = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $images ) ) ) );
    }

    return $item;
}

/**
 * Load child sitemap files for indexed sitemap mode.
 *
 * @param array $structure Structure from detector.
 * @return array
 */
function myaio_sac_load_indexed_children( array $structure ) {
    $children = [];

    foreach ( (array) $structure['child_urls'] as $child_url ) {
        $child_url = esc_url_raw( trim( (string) $child_url ) );
        if ( '' === $child_url ) {
            continue;
        }

        $path = myaio_sac_url_to_local_path( $child_url );
        if ( empty( $path ) ) {
            $path = trailingslashit( ABSPATH ) . basename( wp_parse_url( $child_url, PHP_URL_PATH ) );
        }

        $basename = basename( $path );
        $type     = myaio_sac_detect_child_type_from_basename( $basename );

        $children[ $basename ] = [
            'type'  => $type,
            'path'  => $path,
            'url'   => $child_url,
            'items' => myaio_sac_read_urlset_items_from_path( $path ),
        ];
    }

    return $children;
}

/**
 * Sync payload URLs into single or indexed sitemap files.
 *
 * Payload:
 * {
 *   "items": [
 *     { "url": "...", "type": "page", "flag": "index" },
 *     { "url": "...", "type": "post", "flag": "noindex" }
 *   ]
 * }
 *
 * Rules:
 * - noindex => remove only the targeted URL node
 * - index   => add/update only the targeted URL node
 * - index   => force a fresh <lastmod> so crawlers see the update
 * - images  => preserve image:image entries on unrelated URLs and on the same URL when re-indexed
 *
 * @param WP_REST_Request $request Request.
 * @return WP_REST_Response|WP_HTTP_Response|WP_Error
 */
function myaio_sac_sync_sitemap_urls( WP_REST_Request $request ) {
    $lock_key = 'myaio_sac_sync_sitemap_urls_lock';

    if ( get_transient( $lock_key ) ) {
        return new WP_REST_Response(
            [
                'success' => false,
                'message' => 'A sitemap sync is already running.',
            ],
            429
        );
    }

    set_transient( $lock_key, 1, 60 );

    try {
        $rows = $request->get_param( 'items' );

        if ( ! is_array( $rows ) || empty( $rows ) ) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'message' => 'Payload must contain a non-empty "items" array.',
                ],
                400
            );
        }

        $structure = myaio_sac_detect_editable_sitemap_structure();

        if ( is_wp_error( $structure ) ) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'message' => $structure->get_error_message(),
                ],
                400
            );
        }

        $fresh_lastmod = gmdate( 'c' );
        $fresh_epoch   = time();

        $added   = [];
        $updated = [];
        $removed = [];
        $skipped = [];
        $written = [];

        // Single sitemap mode: ABSPATH/sitemap.xml contains <urlset>.
        if ( 'single' === $structure['mode'] ) {
            $path  = $structure['single_path'];
            $items = myaio_sac_read_urlset_items_from_path( $path );

            foreach ( $rows as $index => $row ) {
                if ( ! is_array( $row ) ) {
                    $skipped[] = [
                        'row'    => $index,
                        'reason' => 'Invalid row format.',
                    ];
                    continue;
                }

                $url  = isset( $row['url'] ) ? esc_url_raw( trim( (string) $row['url'] ) ) : '';
                $type = isset( $row['type'] ) ? myaio_sac_normalize_payload_type( $row['type'] ) : '';
                $flag = myaio_sac_normalize_payload_flag( $row );

                if ( '' === $url || '' === $flag ) {
                    $skipped[] = [
                        'row'    => $index,
                        'reason' => 'Missing or invalid url/flag.',
                    ];
                    continue;
                }

                $target_norm     = myaio_sac_norm_url( $url );
                $existing_images = [];
                $already_exists  = false;

                foreach ( $items as $existing_item ) {
                    if ( ! empty( $existing_item['loc'] ) && myaio_sac_norm_url( $existing_item['loc'] ) === $target_norm ) {
                        $already_exists = true;
                        if ( ! empty( $existing_item['images'] ) && is_array( $existing_item['images'] ) ) {
                            $existing_images = $existing_item['images'];
                        }
                        break;
                    }
                }

                // Remove only this target URL node before optional re-add/update.
                $before = count( $items );
                $items  = myaio_sac_remove_url_from_items( $items, $url );

                if ( count( $items ) < $before && 'noindex' === $flag ) {
                    $removed[] = $url;
                }

                if ( 'noindex' === $flag ) {
                    continue;
                }

                if ( '' === $type ) {
                    $skipped[] = [
                        'row'    => $index,
                        'url'    => $url,
                        'reason' => 'Type is required when flag=index.',
                    ];
                    continue;
                }

                $item = myaio_sac_build_item_for_payload( $url, $type, $fresh_lastmod, $existing_images );

                if ( empty( $item ) || empty( $item['loc'] ) ) {
                    $skipped[] = [
                        'row'    => $index,
                        'url'    => $url,
                        'reason' => 'Could not build sitemap item for this URL.',
                    ];
                    continue;
                }

                // Force fresh lastmod after re-index/update.
                $item['lastmod'] = $fresh_lastmod;
                if ( ! isset( $item['images'] ) ) {
                    $item['images'] = $existing_images;
                }

                $items[] = $item;

                if ( $already_exists ) {
                    $updated[] = $item['loc'];
                } else {
                    $added[] = $item['loc'];
                }
            }

            $items = myaio_sac_dedupe_items_by_loc( $items );
            $items = myaio_sac_sitemap_sort_items( $items );

            $ok = myaio_sac_write_file( $path, myaio_sac_render_urlset( $items ) );

            if ( ! $ok ) {
                return new WP_REST_Response(
                    [
                        'success' => false,
                        'message' => 'Failed to write single sitemap file.',
                        'path'    => $path,
                    ],
                    500
                );
            }

            $written[] = basename( $path );

            update_option( 'myaio_sac_last_sitemap_url', home_url( '/sitemap.xml' ), false );
            update_option( 'myaio_sac_last_sitemap_mtime', $fresh_epoch, false );
            delete_transient( 'myaio_sac_check_sitemap_cache_v1' );

            return rest_ensure_response(
                [
                    'success'       => true,
                    'mode'          => 'single',
                    'message'       => 'Single sitemap updated successfully.',
                    'last_modified' => myaio_sac_iso8601( $fresh_epoch ),
                    'written_files' => $written,
                    'added'         => array_values( array_unique( $added ) ),
                    'updated'       => array_values( array_unique( $updated ) ),
                    'removed'       => array_values( array_unique( $removed ) ),
                    'skipped'       => $skipped,
                    'sitemap_url'   => home_url( '/sitemap.xml' ),
                ]
            );
        }

        // Indexed sitemap mode: remove only target URL from all child XMLs, add/update target child by type.
        $children = myaio_sac_load_indexed_children( $structure );

        foreach ( $rows as $index => $row ) {
            if ( ! is_array( $row ) ) {
                $skipped[] = [
                    'row'    => $index,
                    'reason' => 'Invalid row format.',
                ];
                continue;
            }

            $url  = isset( $row['url'] ) ? esc_url_raw( trim( (string) $row['url'] ) ) : '';
            $type = isset( $row['type'] ) ? myaio_sac_normalize_payload_type( $row['type'] ) : '';
            $flag = myaio_sac_normalize_payload_flag( $row );

            if ( '' === $url || '' === $flag ) {
                $skipped[] = [
                    'row'    => $index,
                    'reason' => 'Missing or invalid url/flag.',
                ];
                continue;
            }

            $target_norm     = myaio_sac_norm_url( $url );
            $existing_images = [];
            $already_exists  = false;

            // Capture images from the targeted URL only. Do not touch unrelated URL image nodes.
            foreach ( $children as $child ) {
                foreach ( (array) $child['items'] as $existing_item ) {
                    if ( ! empty( $existing_item['loc'] ) && myaio_sac_norm_url( $existing_item['loc'] ) === $target_norm ) {
                        $already_exists = true;
                        if ( ! empty( $existing_item['images'] ) && is_array( $existing_item['images'] ) ) {
                            $existing_images = $existing_item['images'];
                        }
                        break 2;
                    }
                }
            }

            // Remove only this URL node from all children. This is safe for URL moves/type changes.
            $was_removed = false;
            foreach ( $children as $key => $child ) {
                $before = count( $children[ $key ]['items'] );
                $children[ $key ]['items'] = myaio_sac_remove_url_from_items( $children[ $key ]['items'], $url );

                if ( count( $children[ $key ]['items'] ) < $before ) {
                    $was_removed = true;
                }
            }

            if ( 'noindex' === $flag ) {
                if ( $was_removed ) {
                    $removed[] = $url;
                }
                continue;
            }

            if ( '' === $type ) {
                $skipped[] = [
                    'row'    => $index,
                    'url'    => $url,
                    'reason' => 'Type is required when flag=index.',
                ];
                continue;
            }

            $basename = myaio_sac_child_basename_for_type( $type );
            if ( '' === $basename ) {
                $skipped[] = [
                    'row'    => $index,
                    'url'    => $url,
                    'reason' => 'Unsupported sitemap type.',
                ];
                continue;
            }

            if ( ! isset( $children[ $basename ] ) ) {
                $children[ $basename ] = [
                    'type'  => $type,
                    'path'  => trailingslashit( ABSPATH ) . $basename,
                    'url'   => home_url( '/' . $basename ),
                    'items' => [],
                ];
            }

            $item = myaio_sac_build_item_for_payload( $url, $type, $fresh_lastmod, $existing_images );

            if ( empty( $item ) || empty( $item['loc'] ) ) {
                $skipped[] = [
                    'row'    => $index,
                    'url'    => $url,
                    'reason' => 'Could not build sitemap item for this URL.',
                ];
                continue;
            }

            // Force fresh lastmod after re-index/update.
            $item['lastmod'] = $fresh_lastmod;
            if ( ! isset( $item['images'] ) ) {
                $item['images'] = $existing_images;
            }

            $children[ $basename ]['items'][] = $item;

            if ( $already_exists ) {
                $updated[] = $item['loc'];
            } else {
                $added[] = $item['loc'];
            }
        }

        // Write child sitemap files, preserving image:image entries for all unrelated URLs.
        foreach ( $children as $child ) {
            $child_items = myaio_sac_dedupe_items_by_loc( $child['items'] );
            $child_items = myaio_sac_sitemap_sort_items( $child_items );

            $ok = myaio_sac_write_file(
                $child['path'],
                myaio_sac_render_urlset( $child_items )
            );

            if ( ! $ok ) {
                return new WP_REST_Response(
                    [
                        'success' => false,
                        'message' => 'Failed writing child sitemap file.',
                        'file'    => $child['path'],
                    ],
                    500
                );
            }

            $written[] = basename( $child['path'] );
        }

        // Rebuild root sitemap index file with fresh child lastmod.
        $child_urls = [];

        foreach ( $children as $child ) {
            if ( ! empty( $child['url'] ) ) {
                $child_urls[] = $child['url'];
            }
        }

        $child_urls = array_values( array_unique( $child_urls ) );

        $ok = myaio_sac_write_file(
            $structure['root_path'],
            myaio_sac_render_sitemap_index( $child_urls )
        );

        if ( ! $ok ) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'message' => 'Failed writing root sitemap index file.',
                    'file'    => $structure['root_path'],
                ],
                500
            );
        }

        $written[] = basename( $structure['root_path'] );

        update_option( 'myaio_sac_last_sitemap_url', $structure['root_url'], false );
        update_option( 'myaio_sac_last_sitemap_mtime', $fresh_epoch, false );
        delete_transient( 'myaio_sac_check_sitemap_cache_v1' );

        return rest_ensure_response(
            [
                'success'       => true,
                'mode'          => 'indexed',
                'message'       => 'Indexed sitemap files updated successfully.',
                'last_modified' => myaio_sac_iso8601( $fresh_epoch ),
                'written_files' => array_values( array_unique( $written ) ),
                'added'         => array_values( array_unique( $added ) ),
                'updated'       => array_values( array_unique( $updated ) ),
                'removed'       => array_values( array_unique( $removed ) ),
                'skipped'       => $skipped,
                'sitemap_url'   => $structure['root_url'],
            ]
        );
    } finally {
        delete_transient( $lock_key );
    }
}

/** ========= Generate: SINGLE FILE (always served at /sitemaps/sitemap.xml) ========= **/
function myaio_sac_generate_sitemap(WP_REST_Request $request) {
    // simple build lock (60s)
    $lock_key = 'myaio_sac_sitemap_lock';
    if (get_transient($lock_key)) {
        return new WP_REST_Response([
            'generated'               => false,
            'error'                   => 'A sitemap build is already running.',
            'message'                 => 'Please retry after the current sitemap build finishes.',
            'sitemap_url'             => home_url('/sitemap.xml'),
            'last_modified'           => null,
            'previous_sitemap_url'    => get_option('myaio_sac_last_sitemap_url') ?: get_option('myaio_sac_prev_sitemap_url'),
            'previous_last_modified'  => myaio_sac_iso8601((int) (get_option('myaio_sac_last_sitemap_mtime') ?: get_option('myaio_sac_prev_sitemap_mtime'))),
        ], 429);
    }
    set_transient($lock_key, 1, 60);

    try {
        // Gather URLs (posts + pages + categories)
        $posts = myaio_sac_collect_posts();
        $pages = myaio_sac_collect_pages();
        $cats  = myaio_sac_collect_categories();
        $misc  = myaio_sac_collect_misc();

        $all_items = array_merge($posts, $pages, $cats, $misc);

        // Previous snapshot (last successful deploy)
        $previous_url   = get_option('myaio_sac_last_sitemap_url') ?: null;
        $previous_mtime = (int) (get_option('myaio_sac_last_sitemap_mtime') ?: 0);

        if (empty($all_items)) {
            return rest_ensure_response([
                'generated'               => false,
                'message'                 => 'No URLs found to include in sitemap.',
                'sitemap_url'             => home_url('/sitemap.xml'),
                'last_modified'           => null,
                'previous_sitemap_url'    => $previous_url ?: get_option('myaio_sac_prev_sitemap_url'),
                'previous_last_modified'  => myaio_sac_iso8601($previous_mtime ?: (int) get_option('myaio_sac_prev_sitemap_mtime')),
            ]);
        }

        // Protocol hard limit for a single file
        if (count($all_items) > 50000) {
            return new WP_REST_Response([
                'generated'               => false,
                'error'                   => 'Too many URLs for a single sitemap file.',
                'message'                 => 'This site has more than 50,000 URLs. Please switch to an indexed sitemap format.',
                'sitemap_url'             => home_url('/sitemap.xml'),
                'last_modified'           => null,
                'previous_sitemap_url'    => $previous_url ?: get_option('myaio_sac_prev_sitemap_url'),
                'previous_last_modified'  => myaio_sac_iso8601($previous_mtime ?: (int) get_option('myaio_sac_prev_sitemap_mtime')),
            ], 400);
        }

        // 1) Discover existing sitemap URL (robots.txt first, then common fallbacks)
        $robots_url  = home_url('/robots.txt');
        $robots_info = myaio_sac_get_sitemaps_from_robots($robots_url);

        $candidates = [];
        foreach (($robots_info['sitemaps'] ?? []) as $u) $candidates[] = $u;

        // Common fallbacks (ordered)
        $candidates[] = home_url('/sitemap.xml');
        $candidates[] = home_url('/sitemap_index.xml');
        $candidates[] = home_url('/wp-sitemap.xml');

        // Keep only unique + reachable
        $existing_url = null;
        foreach (array_values(array_unique(array_filter($candidates))) as $u) {
            if (myaio_sac_url_exists($u)) { $existing_url = $u; break; }
        }

        // 2) Decide whether we can update the existing sitemap in-place
        $mode = 'create-single-root';
        $final_sitemap_url = home_url('/sitemap.xml');
        $now_epoch = time();

        $added_urls = 0;
        $updated_files = [];

        if ($existing_url) {
            $xml = myaio_sac_fetch_xml($existing_url);
            $type = $xml ? myaio_sac_parse_sitemap_type($xml) : null;

            // Try layered (index) first (Rank Math / Yoast style)
            if ($type === 'index') {
                $children = myaio_sac_extract_index_children($xml);

                // Map types to an existing child URL if it looks like it matches; else we'll create our own child files.
                $child_map = [
                    'post'     => null,
                    'page'     => null,
                    'category' => null,
                    'misc'     => null,
                ];

                foreach ($children as $cu) {
                    $low = strtolower($cu);
                    if (!$child_map['post'] && (strpos($low, 'post-sitemap') !== false || strpos($low, 'sitemap-post') !== false)) {
                        $child_map['post'] = $cu;
                    }
                    if (!$child_map['page'] && (strpos($low, 'page-sitemap') !== false || strpos($low, 'sitemap-page') !== false)) {
                        $child_map['page'] = $cu;
                    }
                    if (!$child_map['category'] && (strpos($low, 'category-sitemap') !== false || strpos($low, 'sitemap-categor') !== false)) {
                        $child_map['category'] = $cu;
                    }
                }

                // If index/children are not writable (e.g., SEO plugin generates them dynamically),
                // fall back to creating our own root sitemap.xml and referencing it in robots.txt.
                $index_path = myaio_sac_url_to_local_path($existing_url);
                $can_write_index = $index_path && wp_is_writable(dirname($index_path)) && (!file_exists($index_path) || wp_is_writable($index_path));

                // Check if at least one mapped child is writable; if not, we won't attempt in-place updates.
                $any_child_writable = false;
                foreach ($child_map as $t => $cu) {
                    if (!$cu) continue;
                    $p = myaio_sac_url_to_local_path($cu);
                    if ($p && wp_is_writable(dirname($p)) && (!file_exists($p) || wp_is_writable($p))) { $any_child_writable = true; break; }
                }

                if ($can_write_index && $any_child_writable) {
                    $mode = 'update-layered-index';

                    // Update each child urlset we can
                    $new_children = $children;

                    $updates = [
                        'post'     => $posts,
                        'page'     => $pages,
                        'category' => $cats,
                        'misc'     => $misc,
                    ];

                    foreach ($updates as $t => $items_for_type) {
                        if (empty($items_for_type)) continue;

                        // Determine child URL to update/create
                        $child_url = $child_map[$t];
                        if (!$child_url) {
                            // Create a new child sitemap under root
                            $child_url = home_url('/' . $t . '-sitemap.xml');
                            $new_children[] = $child_url;
                        }

                        $child_path = myaio_sac_url_to_local_path($child_url);
                        if (!$child_path) continue;

                        // Read existing child URLs (if any)
                        $existing_child_urls = [];
                        if (file_exists($child_path) && is_readable($child_path)) {
                            $existing_child_urls = myaio_sac_extract_urlset_urls_from_string((string) @file_get_contents($child_path));
                        }

                        $existing_norm = [];
                        foreach ($existing_child_urls as $u) $existing_norm[myaio_sac_norm_url($u)] = true;

                        $merged = [];
                        foreach ($items_for_type as $it) {
                            $merged[myaio_sac_norm_url($it['loc'])] = $it; // de-dupe by normalized URL
                        }
                        // Keep already-present URLs (even if not in current WP query)
                        foreach ($existing_child_urls as $u) {
                            $n = myaio_sac_norm_url($u);
                            if (!isset($merged[$n])) {
                                $enriched = myaio_sac_sitemap_enrich_from_url( $u );
                                if ( null !== $enriched ) {
                                    $merged[$n] = $enriched;
                                }
                            }
                        }

                        $merged_items = myaio_sac_sitemap_sort_items( array_values( $merged ) );

                        // Count additions (new in WP but not already in sitemap)
                        foreach ($items_for_type as $it) {
                            $n = myaio_sac_norm_url($it['loc']);
                            if (!isset($existing_norm[$n])) $added_urls++;
                        }

                        // Write child
                        @wp_mkdir_p(dirname($child_path));
                        $ok = myaio_sac_write_file($child_path, myaio_sac_render_urlset($merged_items));
                        if ($ok) $updated_files[] = $child_url;
                    }

                    // Re-write index with (possibly) new children
                    $new_children = array_values(array_unique(array_filter($new_children)));
                    $ok = myaio_sac_write_file($index_path, myaio_sac_render_sitemap_index($new_children));
                    if ($ok) {
                        $final_sitemap_url = $existing_url; // keep index as the main sitemap
                        $updated_files[] = $existing_url;
                    } else {
                        return new WP_REST_Response([
                            'generated'           => false,
                            'error'               => 'Existing layered sitemap index is not writable.',
                            'message'             => 'A layered sitemap index exists but could not be updated (write failed). No new URLs were added.',
                            'existing_sitemap_url' => $existing_url,
                            'mode'                => 'error-non-writable-existing-sitemap',
                        ], 409);
                    }
                }
                else {
                    // Existing layered sitemap (SEO plugin) is not writable, so we cannot append new URLs.
                    $reason = [];
                    if (!$can_write_index) $reason[] = 'index sitemap is not writable';
                    if (!$any_child_writable) $reason[] = 'no child sitemap file is writable';
                    return new WP_REST_Response([
                        'generated'            => false,
                        'error'                => 'Existing sitemap is not writable.',
                        'message'              => 'A sitemap already exists (likely generated by Rank Math/Yoast or similar) but it is not writable, so new URLs cannot be added. ' . (!empty($reason) ? ('Reason: ' . implode('; ', $reason) . '.') : ''),
                        'existing_sitemap_url'  => $existing_url,
                        'mode'                 => 'error-non-writable-existing-sitemap',
                    ], 409);
                }
            }

            // Single file sitemap we can overwrite (urlset)
            if ($mode === 'create-single-root' && $type === 'urlset') {
                $path = myaio_sac_url_to_local_path($existing_url);

                $can_write = $path && wp_is_writable(dirname($path)) && (!file_exists($path) || wp_is_writable($path));
                if ($can_write) {
                    $mode = 'update-single-existing';

                    $existing_urls = myaio_sac_extract_urlset_urls($xml);
                    $existing_norm = [];
                    foreach ($existing_urls as $u) $existing_norm[myaio_sac_norm_url($u)] = true;

                    $merged = [];
                    foreach ($all_items as $it) {
                        $merged[myaio_sac_norm_url($it['loc'])] = $it;
                    }
                    // Keep already-present URLs (even if not currently returned by WP queries)
                    foreach ($existing_urls as $u) {
                        $n = myaio_sac_norm_url($u);
                        if (!isset($merged[$n])) {
                            $enriched = myaio_sac_sitemap_enrich_from_url( $u );
                            if ( null !== $enriched ) {
                                $merged[$n] = $enriched;
                            }
                        }
                    }

                    // additions
                    foreach ($all_items as $it) {
                        $n = myaio_sac_norm_url($it['loc']);
                        if (!isset($existing_norm[$n])) $added_urls++;
                    }

                    $merged_items = myaio_sac_sitemap_sort_items( array_values( $merged ) );
                    $ok = myaio_sac_write_file($path, myaio_sac_render_urlset($merged_items));

                    if ($ok) {
                        $final_sitemap_url = $existing_url;
                        $updated_files[] = $existing_url;
                    } else {
                        return new WP_REST_Response([
                            'generated'           => false,
                            'error'               => 'Existing sitemap is not writable.',
                            'message'             => 'A sitemap already exists but it is not writable, so new URLs cannot be added.',
                            'existing_sitemap_url' => $existing_url,
                            'mode'                => 'error-non-writable-existing-sitemap',
                        ], 409);
                    }
                }
            }
        }

        // 3) Create (or overwrite) our own single root sitemap.xml if needed
        if ($mode === 'create-single-root') {
            // De-dupe + sort before writing.
            $by_loc = array();
            foreach ( (array) $all_items as $it ) {
                if ( empty( $it['loc'] ) ) { continue; }
                $by_loc[ myaio_sac_norm_url( $it['loc'] ) ] = $it;
            }
            $final_items = myaio_sac_sitemap_sort_items( array_values( $by_loc ) );

            $root_path = ABSPATH . 'sitemap.xml';
            $ok = myaio_sac_write_file($root_path, myaio_sac_render_urlset($final_items));
            if (!$ok || !is_readable($root_path)) {
                return new WP_REST_Response([
                    'generated'   => false,
                    'error'       => 'Sitemap could not be written to the site root.',
                    'message'     => 'Tried writing ABSPATH/sitemap.xml. Check file permissions and ownership.',
                    'location'    => $root_path,
                    'sitemap_url' => $final_sitemap_url,
                ], 500);
            }
            $added_urls = count($final_items);
            $updated_files[] = $final_sitemap_url;
        }

        // 4) Ensure robots.txt references the sitemap we updated/created
        $robots_update = myaio_sac_ensure_robots_has_sitemap($final_sitemap_url);

        // Record previous -> prev_* snapshot
        if ($previous_url) {
            update_option('myaio_sac_prev_sitemap_url',   $previous_url,   false);
            update_option('myaio_sac_prev_sitemap_mtime', $previous_mtime, false);
        }

        // Record current deploy time (UTC epoch) and set canonical URL
        update_option('myaio_sac_last_sitemap_url',   $final_sitemap_url, false);
        update_option('myaio_sac_last_sitemap_mtime', $now_epoch, false);

        return rest_ensure_response([
            'generated'               => true,
            'mode'                    => $mode,
            'message'                 => $mode === 'create-single-root'
                ? sprintf('Sitemap created at root with %d URL(s) and referenced in robots.txt.', count($all_items))
                : sprintf('Sitemap updated. Added %d new URL(s).', (int)$added_urls),
            'sitemap_url'             => $final_sitemap_url,
            'last_modified'           => myaio_sac_iso8601($now_epoch),
            'previous_sitemap_url'    => $previous_url ?: get_option('myaio_sac_prev_sitemap_url'),
            'previous_last_modified'  => myaio_sac_iso8601($previous_mtime ?: (int) get_option('myaio_sac_prev_sitemap_mtime')),
            'added_url_count'         => (int) $added_urls,
            'updated_files'           => array_values(array_unique(array_filter($updated_files))),
            'robots'                  => [
                'url'      => $robots_url,
                'detected' => array_values(array_unique($robots_info['sitemaps'] ?? [])),
                'update'   => $robots_update,
            ],
        ]);
    } finally {
        delete_transient($lock_key);
    }
}
