# metasync/2.6.10/otto/otto_pixel.php

Search Atlas SEO – OTTO AI SEO Automation for WordPress, version 2.6.10. 2,092 lines.

- Page: https://pluginprobe.com/plugins/metasync/2.6.10/code/otto/otto_pixel.php
- Raw: https://pluginprobe.com/plugins/metasync/2.6.10/raw/otto/otto_pixel.php
- Modified: 2026-06-03T00:09:30+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/metasync/2.6.10/code/otto/otto_pixel.php#L10-L20`.

```php
<?php 
/**
 * This handles the OTTO SSR
 */
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

if (!defined('METASYNC_OTTO_EXCLUDED_TRANSIENT_KEY')) {
    define('METASYNC_OTTO_EXCLUDED_TRANSIENT_KEY', 'metasync_otto_excluded_urls_manual');
}
if (!defined('METASYNC_OTTO_EXCLUDED_TRANSIENT_TTL')) {
    define('METASYNC_OTTO_EXCLUDED_TRANSIENT_TTL', 60);
}

# uses the simple html dom library
use simplehtmldom\HtmlDocument;

# include the otto class file
require_once plugin_dir_path( __FILE__ ) . '/vendor/autoload.php';
require_once plugin_dir_path( __FILE__ ) . '/Otto_html_class.php';
require_once plugin_dir_path( __FILE__ ) . '/Otto_pixel_class.php';
require_once plugin_dir_path( __FILE__ ) . '/metasync-otto-seo-functions.php';
require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-transient-cache.php';
require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-render-strategy.php';
require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-config.php';
require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-bot-detector.php';
require_once plugin_dir_path( __FILE__ ) . '/class-metasync-otto-bot-statistics-database.php';

# OPTIMIZED: get the metasync options (cached in static class)
$metasync_options = Metasync_Otto_Config::get_options();

# OTTO SSR is always enabled by default
$otto_enabled = true;

# add tag to wp head
add_action('wp_head', function(){
    # load globals
    global $metasync_options, $otto_enabled;

    # OTTO SSR is always enabled
    $string_enabled = 'true';

    # OPTIMIZED: check uuid set using cached config
    if(!Metasync_Otto_Config::is_otto_enabled()){
        return;
    }

    # Performance optimization: Add DNS prefetch and preconnect for OTTO API
    # This improves connection speed by resolving DNS and establishing connections early
    # Use endpoint manager to get the correct domain
    $otto_domain = 'sa.searchatlas.com'; # default
    if (class_exists('Metasync_Endpoint_Manager')) {
        $otto_api_domain = Metasync_Endpoint_Manager::get_endpoint('OTTO_API_DOMAIN');
        $parsed = parse_url($otto_api_domain);
        if (!empty($parsed['host'])) {
            $otto_domain = $parsed['host'];
        }
    }
    echo '<link rel="dns-prefetch" href="//' . esc_attr($otto_domain) . '">' . "\n";
    echo '<link rel="preconnect" href="https://' . esc_attr($otto_domain) . '" crossorigin>' . "\n";

    # adding the otto tag to pages
    $plugin_version = defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown';
    # OPTIMIZED: use cached uuid
    $otto_tag = '<meta name="otto" content="uuid='.esc_attr(Metasync_Otto_Config::get_otto_uuid()).'; type=wordpress; enabled='.esc_attr($string_enabled).'; version='.esc_attr($plugin_version).'">';

    # out the otto tag
    echo $otto_tag;
}, 1); # Priority 1 to output early in head

/**
 * Start end point to handle requests on page updates
 * The Otto Crawler will call this end point once a page is updated
 **/

# function to register the route
function metasync_otto_crawl_notify($request){

    # get request data params
    $data = $request->get_json_params();

    # fields
    $fields = ['domain', 'urls'];

    # validate the json request
    foreach ($fields as $key => $value) {

        # if the value is empty stop tehre
        if(empty($data[$value])){

            # Handle the POST request
            return new WP_REST_Response(array(
                'success' => false,
                'message' => 'Invalid Field : '. $value,
            ), 400);
        }

    }

    # load otto pixel
    $otto_pixel = new Metasync_otto_pixel(false);

    # save the otto data first (cheap local DB write — kept synchronous)
    $otto_pixel->save_crawl_data($data);

    # Defer the expensive per-URL work (OTTO API fetch, post meta sync,
    # per-URL cache purge) to background jobs. The webhook caller enforces a
    # strict response-time budget; doing this work inline previously blocked
    # the response for up to 30s × N URLs and timed the caller out.
    # Each URL gets its own scheduled event so failures are isolated.
    #
    # WP-299: Cap the number of per-URL cron jobs scheduled per webhook batch.
    # Large crawl batches (100+ URLs) previously created hundreds of cron events
    # that overwhelmed WP-Cron on shared/managed hosts like WP Engine. Excess
    # URLs beyond the cap are silently skipped — OTTO will re-crawl them on the
    # next webhook cycle.
    $max_jobs_per_batch = defined('METASYNC_MAX_JOBS_PER_BATCH') ? METASYNC_MAX_JOBS_PER_BATCH : 25;
    $now = time();
    $routes_to_process = array();
    $scheduled_count = 0;

    foreach($data['urls'] AS $key => $url){
        # Enforce per-batch cap to prevent cron overload
        if ($scheduled_count >= $max_jobs_per_batch) {
            break;
        }

        # prepare the route
        $route = $data['domain'] . $url;

        # validate the route
        $route = rtrim($route, '/');

        # Resolve redirect table: use final destination URL before excluded/404 checks and OTTO processing
        $route = metasync_otto_resolve_redirect_to_final_url($route);

        # OTTO has confirmed this URL is crawlable. Remove any auto-exclusion that was
        # previously set (e.g. because url_to_postid() returned 0 for a custom post type
        # and metasync_otto_is_url_available() incorrectly treated it as a 404).
        # Manual exclusions (auto_excluded = 0) are left untouched.
        metasync_otto_remove_auto_exclusion($route);

        # Skip manually excluded URLs - don't queue OTTO processing for them
        if (metasync_is_otto_url_excluded($route)) {
            continue;
        }

        # Queue the per-URL processing for background execution. The handler
        # (metasync_handle_otto_crawl_url_job) performs transient warming,
        # SEO meta sync, and per-URL host cache purge.
        # Offset each event by $key seconds to avoid wp_schedule_single_event()
        # silently dropping duplicates when timestamp + hook + args collide.
        wp_schedule_single_event($now + $key, 'metasync_process_otto_crawl_url_job', array($route));

        $routes_to_process[] = $route;
        $scheduled_count++;
    }

    # Schedule a single batch job for cache warming and edge CDN purge.
    # These operations benefit from batching: warm_urls() uses concurrent
    # fire-and-forget requests, and edge providers (Cloudflare, Fastly)
    # support multi-URL purge in a single API call.
    if (!empty($routes_to_process)) {
        $batch_time = $now + count($data['urls']) + 5; // run after all per-URL jobs
        wp_schedule_single_event($batch_time, 'metasync_process_otto_batch_cache_job', array($routes_to_process));
    }

    # Track OTTO optimization event in GA4 (local, non-blocking)
    try {
        Metasync_GA4::get_instance()->track_otto_optimization($data);
    } catch (Exception $e) {
        // Analytics tracking failed, continue
    }

    # Return 200 immediately so the webhook caller does not time out.
    return new WP_REST_Response(array(
        'success' => true,
        'message' => 'OTTO crawl notification received',
    ), 200);
}

/**
 * Background handler for a single OTTO crawl-notify URL.
 *
 * Runs the per-URL sequence that was previously inline in metasync_otto_crawl_notify():
 * warm the OTTO transient cache, sync SEO post meta, and clear the per-URL host cache.
 *
 * Cache warming and edge CDN purge are handled separately by
 * metasync_handle_otto_batch_cache_job() in a single batched event, because
 * those operations benefit from multi-URL batching (fewer API calls).
 *
 * On failure, retries up to METASYNC_OTTO_JOB_MAX_RETRIES times with 60s backoff.
 * After all retries are exhausted the failure is recorded via
 * metasync_record_failed_action() for surfacing in Site Health.
 *
 * @param string $route       Fully-qualified URL to process.
 * @param int    $retry_count Current retry attempt (0 = first run).
 */
function metasync_handle_otto_crawl_url_job($route, $retry_count = 0) {
    try {
        # Step 1: Warm OTTO transient cache (fetch fresh suggestions from OTTO API into WP transient)
        $otto_uuid = Metasync_Otto_Config::get_otto_uuid();
        if (!empty($otto_uuid)) {
            $transient_cache = new Metasync_Otto_Transient_Cache($otto_uuid);
            $transient_cache->warm_cache($route);
        }

        # Step 2: Write SEO post meta synchronously BEFORE the cache purge.
        # This ensures the DB is fully up-to-date when Kinsta (or any host) re-populates
        # the cache on the very next request — eliminating the race condition where the
        # 1-second scheduled job hadn't run yet and stale meta got cached.
        # allow_defer=false: do NOT reschedule a new metasync_process_seo_job cron
        # event from this synchronous path — the crawl_url_job retry mechanism already
        # handles failures, and rescheduling here caused an unbounded cron pile-up (WP-299).
        metasync_process_otto_seo_data($route, false);

        # Step 3: Clear the per-URL cache entry
        $otto_pixel = new Metasync_otto_pixel(false);
        $otto_pixel->refresh_cache($route);
        Metasync_Cache_Purge::purge_single_url($route);

    } catch (Exception $e) {
        $max_retries = defined('METASYNC_OTTO_JOB_MAX_RETRIES') ? METASYNC_OTTO_JOB_MAX_RETRIES : 3;

        if ($retry_count < $max_retries) {
            # Schedule a retry with exponential backoff: 60s, 120s, 240s
            $delay = 60 * pow(2, $retry_count);
            wp_schedule_single_event(time() + $delay, 'metasync_process_otto_crawl_url_job', array($route, $retry_count + 1));
            error_log('MetaSync OTTO: retrying crawl-url job for ' . $route . ' (attempt ' . ($retry_count + 1) . '/' . $max_retries . ') in ' . $delay . 's: ' . $e->getMessage());
        } else {
            metasync_record_failed_action('metasync_process_otto_crawl_url_job');
            error_log('MetaSync OTTO: background crawl-url job permanently failed for ' . $route . ' after ' . $max_retries . ' retries: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
        }
    }
}

/**
 * Batch handler for cache warming and edge CDN purge.
 *
 * Runs after all per-URL jobs have had time to complete. Batching these
 * operations avoids N separate API calls to edge providers (Cloudflare
 * supports up to 30 tags per request) and lets warm_urls() issue concurrent
 * fire-and-forget requests.
 *
 * @param array $routes List of fully-qualified URLs to warm and purge.
 */
function metasync_handle_otto_batch_cache_job($routes) {
    try {
        # Re-populate the cache so OTTO-modified output is what gets stored.
        # Per-URL host cache was already cleared by individual jobs.
        # warm_urls() hits each URL with a non-blocking request so our code —
        # with fresh transients and post meta — is first to populate the host cache.
        Metasync_Cache_Purge::warm_urls($routes);

        # Purge edge CDN caches (Cloudflare, Fastly, Akamai, Sucuri, Sevalla, etc.)
        # Tag-based providers purge only the affected posts; full-flush providers fire once per batch.
        Metasync_Edge_Cache_Purge::purge($routes);
    } catch (Exception $e) {
        metasync_record_failed_action('metasync_process_otto_batch_cache_job');
        error_log('MetaSync OTTO: batch cache job failed for ' . count($routes) . ' URLs: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
    }
}

# NOTE: Cache system removed - these functions are no longer needed
# Kept for backward compatibility in case old cache directories need cleanup
function metasync_deleteDir($dir) {
    if (!is_dir($dir)) {
        return false;
    }
    $files = array_diff(scandir($dir), array('.', '..'));
    foreach ($files as $file) {
        $filePath = $dir . DIRECTORY_SEPARATOR . $file;
        if (is_dir($filePath)) {
            metasync_deleteDir($filePath);
        } else {
            unlink($filePath);
        }
    }
    return rmdir($dir);
}

# Cleanup function for removing old cache directories (if they exist)
function metasync_invalidate_all_caches($folder = ''){
    # Cache system removed - this function only exists to clean up old cache directories
    if(!defined('WP_CONTENT_DIR')){
        return false;
    }
    $wp_content_dir = WP_CONTENT_DIR;
    $cache_dir = $wp_content_dir . '/metasync_caches';
    if(in_array($folder, ['posts', 'pages'])){
        $cache_dir = $cache_dir . '/' . $folder;
    }
    if(is_dir($cache_dir)){
        metasync_deleteDir($cache_dir);
    }
}

function metasync_start_otto(){

    # PERFORMANCE FIX: Cache is now enabled for speed
    # Skip initial cache cleanup to preserve existing cache
    # Cache files are valuable for performance - only clear on OTTO updates
    # Periodic cache clearing can be configured in plugin settings if needed

    # exclude AJAX requests and WooCommerce transactional pages from OTTO SSR
    # SSR is now ENABLED for: single products, product categories, product tags
    # SSR is SKIPPED for: cart, checkout
    # Note: title/description filters (pre_get_document_title, wp_head meta desc)
    # run on ALL pages regardless — they are hooked unconditionally in seo-functions.php

    # ── WooCommerce-independent cart/checkout protection (WP-374) ──────────
    # The is_cart()/is_checkout() guards below only recognize WooCommerce. Carts
    # from other systems — e.g. the Point of Rental "Catalog" plugin on
    # venturarental.com — are invisible to them, so OTTO would process those
    # pages and let caching layers store them. A cached cart page corrupts the
    # live cart (added items vanish because a stale page is served). The checks
    # here do not depend on WooCommerce being active.

    # Never run OTTO on non-GET requests. SSR exists for crawlers, which only
    # issue GET; POST/PUT/etc. are form or cart submissions that must pass
    # through untouched. (OTTO's own internal fetches use GET.)
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'GET') {
        return;
    }

    # Skip OTTO on cart/checkout-type paths regardless of the cart plugin, and
    # signal caching plugins (WP Rocket/Kinsta/etc.) to never cache them — a
    # cached cart/checkout page is precisely what corrupts cart state.
    # Matched against the FIRST path segment (relative to the WP home path) so
    # subdirectory installs (/shop/cart) still work, while unrelated content pages
    # like /blog/cart or /features/checkout do NOT false-match.
    # Drop the query string with explode() (no shared internal pointer like strtok),
    # then lower-case and strip surrounding slashes.
    $otto_req_path = explode('?', (string) wp_unslash($_SERVER['REQUEST_URI'] ?? ''), 2)[0];
    $otto_req_path = strtolower(trim($otto_req_path, '/'));
    # Strip the site's base path so first-segment matching works on subdir installs.
    $otto_home_path = trim((string) parse_url(home_url(), PHP_URL_PATH), '/');
    if ($otto_home_path !== '' && strpos($otto_req_path, $otto_home_path . '/') === 0) {
        $otto_req_path = substr($otto_req_path, strlen($otto_home_path) + 1);
    }
    $otto_first_segment = explode('/', $otto_req_path, 2)[0];
    # Filterable so sites can add/remove transactional slugs without code changes.
    $otto_cart_segments = apply_filters('metasync_otto_cart_paths', array(
        'cart', 'checkout', 'basket', 'request-a-quote', 'quote-request',
    ));
    $otto_cart_segments = array_map('strtolower', (array) $otto_cart_segments);
    if ($otto_first_segment !== '' && in_array($otto_first_segment, $otto_cart_segments, true)) {
        if (!defined('DONOTCACHEPAGE')) {
            define('DONOTCACHEPAGE', true);
        }
        return;
    }
    # ──────────────────────────────────────────────────────────────────────

    if (
        # disable ajax calls
        isset($_GET['ucfrontajaxaction']) ||
        # OTTO Preview mode - skip OTTO when previewing original content
        (isset($_GET['otto_preview']) && $_GET['otto_preview'] === '1') ||
        # WooCommerce shop archive page only (products and categories now use SSR)
        //(function_exists('is_shop') && is_shop()) ||
        # Cart page
        (function_exists('is_cart') && is_cart()) ||
        # Checkout page
        (function_exists('is_checkout') && is_checkout()) ||
        # My Account page
        //(function_exists('is_account_page') && is_account_page()) ||
        # Standard WordPress AJAX
        (function_exists('wp_doing_ajax') && wp_doing_ajax()) ||
        # check by constant
        (defined('DOING_AJAX') && DOING_AJAX) ||
        # WooCommerce AJAX endpoint (e.g., ?wc-ajax=update_cart)
        (isset($_REQUEST['wc-ajax']) && !empty($_REQUEST['wc-ajax'])) ||
        # AJAX requests via X-Requested-With header
        (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') ||
        # Gravity Forms submission detection - skip OTTO to allow form processing
        (isset($_POST['gform_submit']) && (
            is_array($_POST['gform_submit']) ||
            (is_string($_POST['gform_submit']) && isset($_POST['is_submit_' . $_POST['gform_submit']]) && !empty($_POST['gform_submit']))
        )) ||
        # Gravity Forms AJAX submission
        (isset($_POST['gform_ajax']) && isset($_POST['gform_submit']) && (
            is_array($_POST['gform_submit']) || !empty($_POST['gform_submit'])
        )) ||
        # Gravity Forms file upload
        (isset($_POST['gform_uploaded_files'])) ||
        # Any Gravity Forms POST parameter
        (isset($_POST['gform_submit']) || isset($_POST['gform_unique_id']) || isset($_POST['gform_field_values'])) ||
        # Formidable Forms AJAX submission detection - skip OTTO to allow form processing
        (isset($_POST['action']) && $_POST['action'] === 'frm_entries_create') ||
        # Formidable Forms POST parameters
        (isset($_POST['form_id']) && !empty($_POST['form_id'])) ||
        # Formidable Forms action parameter
        (isset($_POST['frm_action']) && !empty($_POST['frm_action'])) ||
        # Formidable Forms item_key (used in form submissions)
        (isset($_POST['item_key']) && !empty($_POST['item_key']))
    ) {
        return;
    }

    # fetch globals
    global $metasync_options, $otto_enabled;

    # OPTIMIZED: check for the disable otto for logged in users option using cached config
    if(Metasync_Otto_Config::is_disabled_for_loggedin()){

        # get user
        $current_user = wp_get_current_user();

        # check if user is logged in
        if( !empty($current_user->ID)){

            return;
        }
    }

    # check if current URL is manually excluded from OTTO
    # NOTE: auto-exclusions (false-positive 404 detections) are intentionally NOT checked
    # here — they must not block OTTO rendering. Use metasync_is_otto_url_excluded() only
    # in the webhook handler where we gate SEO meta writes.
    $request_uri = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
    $current_url = home_url(strtok($request_uri, '?') ?: $request_uri);
    if (metasync_is_otto_url_manually_excluded($current_url)) {
        return;
    }

    # Skip OTTO for XML endpoints (sitemaps, RSS-as-xml, etc.). OTTO has no
    # suggestions for machine-readable XML, and the upstream API returns
    # API_ERROR for these URLs — which then stamps misleading
    # X-MetaSync-OTTO-Cache: API_ERROR / X-MetaSync-OTTO-Method: NONE headers
    # onto otherwise-healthy sitemap responses and causes false-alarm bug
    # reports from customers. See WP-353.
    $request_path = strtok($request_uri, '?');
    if ($request_path && preg_match('#\.xml$#i', $request_path)) {
        return;
    }

    # BOT DETECTION: Always detect bots so crawl data reaches the SA backend.
    # Real bots don't execute JS, so otto-tracker.js never fires for them.
    # push_crawl_log_to_sa() fires a non-blocking wp_remote_post here instead.
    $bot_detector = Metasync_Otto_Bot_Detector::get_instance();
    $detection    = $bot_detector->detect();
    if ( $detection['is_bot'] ) {
        $bot_detector->push_crawl_log_to_sa( $detection, $current_url );
    }

    # Throttle OTTO rendering for non-search-engine bots: at most one render
    # per URL+bot every 5 minutes. Humans and verified search engines
    # (Googlebot, Bingbot, etc.) are never throttled — their hits always get
    # full OTTO content so indexing remains current. SEO tools, AI scrapers,
    # uptime monitors, and unverified crawlers are de-duplicated so repeat
    # hits don't re-trigger the expensive DOM-rewriting path.
    if ( $detection['is_bot'] && $detection['bot_type'] !== 'search_engine' ) {
        $normalized_url = strtok( $current_url, '?' );
        if ( $normalized_url === false ) {
            $normalized_url = $current_url;
        }
        // Lowercase + length cap so attacker-controlled bot_name variations
        // (EvilBota, EvilBotB, EvilBotc, ...) collapse to a bounded key space
        // and cannot flood wp_options with unique transient rows.
        $bot_name            = substr( strtolower( $detection['bot_name'] ?? 'unknown' ), 0, 32 );
        $render_throttle_key = 'metasync_otto_rendered_' . md5( $normalized_url . '|' . $bot_name );
        if ( get_transient( $render_throttle_key ) ) {
            // Prevent page caches (WP Rocket, W3TC, etc.) from saving the
            // un-OTTO'd response and serving it to subsequent human visitors.
            if ( ! defined( 'DONOTCACHEPAGE' ) ) {
                define( 'DONOTCACHEPAGE', true );
            }
            return;
        }
        set_transient( $render_throttle_key, 1, 5 * MINUTE_IN_SECONDS );
    }

    # Optionally skip OTTO processing for bot traffic (when the setting is enabled)
    if ($bot_detector->should_skip_otto()) {
        // Log the bot locally and count the saved API call
        $bot_detector->log_detection($detection);
        $bot_stats_db = Metasync_Otto_Bot_Statistics_Database::get_instance();
        $bot_stats_db->increment_api_calls_saved();

        // Skip OTTO processing for this bot
        return;
    }

    # OPTIMIZED: Check if Otto should be disabled for WP Rocket compatibility
    if (class_exists('WP_Rocket')) {
        $wp_rocket_compat_mode = Metasync_Otto_Config::get_wp_rocket_compat_mode();

        if ($wp_rocket_compat_mode === 'disable_otto') {
            return; # Exit early, Otto is disabled when WP Rocket is active
        }
    }

    # Skip OTTO for Divi AJAX pagination and paginated archive requests (WP-315).
    # ?et_blog = Divi AJAX pagination callback
    # /page/N/ = paginated blog/archive pages — OTTO's buffer/HTTP render causes
    # module numbering mismatch between page 1 (with TB template) and page N
    # (without TB template), breaking Divi's JS pagination selector matching.
    if (isset($_GET['et_blog']) || (is_paged() && !is_singular())) {
        return;
    }

    # Handle cache plugin compatibility early - before any caching happens
    metasync_otto_handle_cache_compatibility();

    # check if OTTO is disabled for this specific page/post
    $post_id = get_the_ID();
    if ($post_id && class_exists('Metasync_Otto_Frontend_Toolbar')) {
        if (Metasync_Otto_Frontend_Toolbar::is_otto_disabled($post_id)) {
            return;
        }
    }

    # check if we are having an otto request
    if(!empty($_GET['is_otto_page_fetch'])){

        # Block SEO plugins NOW for this internal fetch request
       # metasync_otto_block_seo_plugins();
       # $_SERVER['REQUEST_URI'] = remove_query_arg('is_otto_page_fetch', $_SERVER['REQUEST_URI']);
       $block_title = !empty($_GET['otto_block_title']) && $_GET['otto_block_title'] === '1';
        $block_description = !empty($_GET['otto_block_desc']) && $_GET['otto_block_desc'] === '1';
        
        # Block SEO plugins conditionally based on what Otto has
        if ($block_title || $block_description) {
            metasync_otto_block_seo_plugins($block_title, $block_description);
        }
        
        # Remove ALL Otto parameters from REQUEST_URI to prevent them from appearing in pagination, etc.
        $request_uri_raw = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
        $_SERVER['REQUEST_URI'] = remove_query_arg(
            ['is_otto_page_fetch', 'otto_block_title', 'otto_block_desc'],
            $request_uri_raw
        );
        
        # Also remove from $_GET to prevent WordPress from using them
        unset($_GET['is_otto_page_fetch']);
        unset($_GET['otto_block_title']);
        unset($_GET['otto_block_desc']);
        return;
    }

    # to avoid unnecessary processese
    # OPTIMIZED: check that otto is configured
    # And the UUID is properly set before running OTT

    # check that we have the option
    if(!Metasync_Otto_Config::is_otto_enabled()){
        return;
    }

    # check that otto is enabled
    if(!$otto_enabled){
        return;
    }

    # get the otto uuid
    $otto_uuid = Metasync_Otto_Config::get_otto_uuid();

    # start the class
    $otto = new Metasync_otto_pixel($otto_uuid);

    # call render
    $otto->render_route_html();
}

/**
 * Handle cache plugin compatibility with Otto
 * Controls DONOTCACHEPAGE constant based on active plugins and configuration
 * This function is called early in the WordPress lifecycle
 */
function metasync_otto_handle_cache_compatibility() {
    # Detect active plugins
    $brizy_active = class_exists('Brizy_Editor') || defined('BRIZY_VERSION');
    $wp_rocket_active = class_exists('WP_Rocket');
    $sg_optimizer_active = is_plugin_active('sg-cachepress/sg-cachepress.php');

    # OPTIMIZED: Get configuration option using cached config
    $wp_rocket_compat_mode = Metasync_Otto_Config::get_wp_rocket_compat_mode();

    # Check for Brizy posts in database
    global $wpdb;
    $has_brizy_posts = false;

    if ($brizy_active) {
        # OPTIMIZED: Check cache first (1-hour TTL) to avoid querying on every page load
        $cached = get_transient('metasync_has_brizy_posts');
        if ($cached !== false) {
            $has_brizy_posts = ($cached === 'yes');
        } else {
            # Query database only if cache missed
            $has_brizy_posts = $wpdb->get_var(
                "SELECT COUNT(*) FROM {$wpdb->postmeta}
                 WHERE meta_key = 'brizy_post_uid'
                 AND meta_value != ''
                 LIMIT 1"
            );

            # Cache result for 1 hour
            $result = !empty($has_brizy_posts) ? 'yes' : 'no';
            set_transient('metasync_has_brizy_posts', $result, HOUR_IN_SECONDS);
        }
    }

    # Determine if DONOTCACHEPAGE should be set
    $should_set_donotcachepage = false;

    # Case 1: Brizy is active with posts - always needed
    if ($brizy_active && !empty($has_brizy_posts)) {
        $should_set_donotcachepage = true;
    }

    # Case 2: SG Optimizer without WP Rocket - prevent conflicts
    elseif ($sg_optimizer_active && !$wp_rocket_active) {
        $should_set_donotcachepage = true;
    }

    # Case 3: User explicitly disabled Otto for WP Rocket compatibility
    elseif ($wp_rocket_active && $wp_rocket_compat_mode === 'disable_otto') {
        $should_set_donotcachepage = true;
        return; # Exit early, Otto won't run
    }

    # Case 4: WP Rocket active with auto/buffer mode - DON'T set DONOTCACHEPAGE
    # This allows WP Rocket optimizations to continue working

    # Only set DONOTCACHEPAGE if needed
    if ($should_set_donotcachepage && !defined('DONOTCACHEPAGE')) {
        define('DONOTCACHEPAGE', true);
    }

    # Apply SG Optimizer specific filters only if SG is active and no WP Rocket
    if ($sg_optimizer_active && !$wp_rocket_active && $should_set_donotcachepage) {
        if (!defined('SG_CachePress_SUPERCACHER')) {
            define('SG_CachePress_SUPERCACHER', false);
        }

        add_filter('sgo_html_cache_disable', '__return_true', 999);
        add_filter('sgo_css_combine_exclude', '__return_true', 999);
        add_filter('sgo_js_combine_exclude', '__return_true', 999);
        add_filter('sgo_cache_this_page', '__return_false', 999);

        if (!headers_sent()) {
            header('Cache-Control: no-cache, must-revalidate, max-age=0');
            header('X-Accel-Expires: 0');
        }
    }
}

/**
 * Block SEO plugins conditionally based on what Otto is providing
 * Only blocks title if Otto has title, only blocks description if Otto has description
 * This prevents duplicate SEO tags while allowing fallback to SEO plugins when Otto has no data.
 * Supports Yoast SEO, Rank Math, and AIOSEO (free + pro).
 *
 * @param bool  $block_title       Whether to block title tags.
 * @param bool  $block_description Whether to block description tags.
 * @param array $description_tags  Optional. Granular list of OTTO-provided description tags
 *                                 (e.g. ['meta[name=description]', 'meta[property=og:description]']).
 *                                 When provided, only matching AIOSEO tags are suppressed.
 *                                 When empty, all AIOSEO description tags are suppressed (legacy behavior).
 */
function metasync_otto_block_seo_plugins($block_title = false, $block_description = false, $description_tags = []) {
    # Disable Yoast SEO (free and premium)
    if (is_plugin_active('wordpress-seo/wp-seo.php') ||
        is_plugin_active('wordpress-seo-premium/wp-seo-premium.php')) {

        # TITLE: Never block Yoast's title output during SSR fetch.
        # Yoast removes WordPress's native _wp_render_title_tag action and is the sole
        # renderer of the <title> tag. Returning false/empty from wpseo_title or removing
        # Title_Presenter leaves the page with NO <title> tag at all — OTTO's buffer
        # post-processing then has nothing to replace, producing a missing title.
        # Instead, let Yoast render its own title; OTTO's replace_title() will overwrite
        # it in the final HTML buffer. deduplicate_title_tags() cleans up any duplicates.

        # Block description only if Otto has description
        if ($block_description) {
            add_filter('wpseo_metadesc', '__return_false', 999);
            add_filter('wpseo_meta_description', '__return_false', 999);
            add_filter('wpseo_metakeywords', '__return_false', 999);
        }

        # Block Yoast's modern presenters — description only, never title
        add_filter('wpseo_frontend_presenters', function($presenters) use ($block_description) {
            if (!is_array($presenters)) return $presenters;

            $presenters_to_remove = [];


            # Remove description presenters only when OTTO has a description
            if ($block_description) {
                $presenters_to_remove[] = 'Yoast\WP\SEO\Presenters\Meta_Description_Presenter';
                $presenters_to_remove[] = 'Yoast\WP\SEO\Presenters\Open_Graph\Description_Presenter';
                $presenters_to_remove[] = 'Yoast\WP\SEO\Presenters\Twitter\Description_Presenter';
            }

            foreach ($presenters as $key => $presenter) {
                // Safely get class name, suppressing autoload errors
                // This prevents warnings when Composer autoloader tries to load deprecated Yoast files
                $class_name = is_object($presenter) ? @get_class($presenter) : '';

                if (!empty($class_name) && in_array($class_name, $presenters_to_remove)) {
                    unset($presenters[$key]);
                }
            }
            return $presenters;
        }, 999);
    }
    
    # Disable Rank Math
    if (is_plugin_active('seo-by-rank-math/rank-math.php') ||
        is_plugin_active('seo-by-rankmath/rank-math.php')) {

        if ($block_title) {
            add_filter('rank_math/frontend/title', '__return_empty_string', 999);
        }

        if ($block_description) {
            add_filter('rank_math/frontend/description', '__return_false', 999);
            add_filter('rank_math/frontend/show_keywords', '__return_false', 999);
        }
    }

    # Disable AIOSEO (free and pro)
    if (is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php') ||
        is_plugin_active('all-in-one-seo-pack-pro/all_in_one_seo_pack.php')) {

        if ($block_title) {
            add_filter('aioseo_title', '__return_empty_string', 999);
            add_filter('aioseo_facebook_tags', function($meta) {
                if (is_array($meta)) { unset($meta['og:title']); }
                return $meta;
            }, 999);
            add_filter('aioseo_twitter_tags', function($meta) {
                if (is_array($meta)) { unset($meta['twitter:title']); }
                return $meta;
            }, 999);
        }

        if ($block_description) {
            # Use granular tag list when available to only block what OTTO provides
            $tags = !empty($description_tags) ? $description_tags : [];
            $block_standard  = empty($tags) || in_array('meta[name=description]', $tags);
            $block_og_desc   = empty($tags) || in_array('meta[property=og:description]', $tags);
            $block_tw_desc   = empty($tags) || in_array('meta[name=twitter:description]', $tags);

            if ($block_standard) {
                add_filter('aioseo_description', '__return_empty_string', 999);
            }
            if ($block_og_desc) {
                add_filter('aioseo_facebook_tags', function($meta) {
                    if (is_array($meta)) { unset($meta['og:description']); }
                    return $meta;
                }, 999);
            }
            if ($block_tw_desc) {
                add_filter('aioseo_twitter_tags', function($meta) {
                    if (is_array($meta)) { unset($meta['twitter:description']); }
                    return $meta;
                }, 999);
            }
        }
    }
}
# check that otto is not added via js to the site
function metasync_check_otto_js(){

    # Cache result to avoid a blocking HTTP self-request (~2.4s)
    # on every single admin page load.
    $cache_key = 'metasync_otto_js_detected';
    $cached = get_transient($cache_key);

    if ($cached !== false) {
        return $cached === 'yes';
    }

    # get the site url
    $site_url = site_url() . '?is_otto_page_fetch=1';

    # get the html — short timeout so admin doesn't hang
    $page_data = wp_remote_get($site_url, array('timeout' => 5, 'sslverify' => false));

    if (is_wp_error($page_data)) {
        set_transient($cache_key, 'no', HOUR_IN_SECONDS);
        return false;
    }

    # now get the html body
    $body = wp_remote_retrieve_body($page_data);

    # now load the body into html
    $dom = new HtmlDocument($body);

    # now check the dom for a meta tag with
    $script = $dom->find('script#sa-dynamic-optimization', 0);

    # check script
    if($script AND !empty($script->getAttribute('data-uuid'))){
        set_transient($cache_key, 'yes', 12 * HOUR_IN_SECONDS);
        return true;
    }

    set_transient($cache_key, 'no', 12 * HOUR_IN_SECONDS);
    return false;
};

# Handle AJAX Clear Cache request
# NOTE: Cache system removed - this is now a no-op
function metasync_clear_otto_cache_handler() {
    if (!empty($_GET['clear_otto_cache'])) {
        delete_transient('metasync_otto_js_detected');
        # Cache system has been removed - no cache to clear
        wp_send_json_success(['message' => 'Cache system removed - all pages processed in real-time']);
    }
    else {
        wp_send_json_error(['message' => 'Missing parameter']);
    }
}

# Clear cache hook
add_action('wp_ajax_metasync_clear_otto_cache', 'metasync_clear_otto_cache_handler');

# add admin action to check script
function metasync_show_otto_ssr_notice() {
    if (!Metasync::current_user_has_plugin_access()) {
        return; // Only show to admins
    }

    # Get the plugin name using centralized method
    $plugin_name = Metasync::get_effective_plugin_name();
    $whitelabel_otto_name = Metasync::get_whitelabel_otto_name();
    if (metasync_check_otto_js()) {

        # Show admin notice with plugin name included in the message
        echo '<div class="notice notice-error">
                 <p><b>Warning from ' . esc_html($plugin_name) . '</b>
                    <br>
                    ' . esc_html($whitelabel_otto_name) . ' JavaScript has been detected on your site. Please remove it and configure ' . esc_html($whitelabel_otto_name) . ' for Wordpress. Contact support for help
                </p>
         </div>';
    }
}

add_action('admin_notices', 'metasync_show_otto_ssr_notice');


# staging dummy change
# load otto in the wp hook
add_action('wp', 'metasync_start_otto');

  # ENHANCED OTTO SEO INTEGRATION
  # Register async SEO processing hook
  add_action('metasync_process_seo_job', 'metasync_process_otto_seo_data', 10, 3);
  add_action('metasync_process_otto_crawl_url_job', 'metasync_handle_otto_crawl_url_job', 10, 2);
  add_action('metasync_process_otto_batch_cache_job', 'metasync_handle_otto_batch_cache_job');

  # Process OTTO SEO data and update WordPress meta fields for SEO plugins
  # This function now runs asynchronously via WordPress cron system
  #
  # @param string $route         Fully-qualified URL to process.
  # @param bool   $allow_defer   When false, skip CPU-deferral rescheduling (used when
  #                               called synchronously from crawl_url_job whose own retry
  #                               mechanism already handles failures).
  # @param int    $deferral_count How many times this job has already been deferred for
  #                               CPU load. Prevents infinite reschedule loops.

function metasync_process_otto_seo_data($route, $allow_defer = true, $deferral_count = 0) {
    # Maximum number of times a job can be deferred before it is dropped.
    $max_deferrals = defined('METASYNC_SEO_JOB_MAX_DEFERRALS') ? METASYNC_SEO_JOB_MAX_DEFERRALS : 5;
    $lock_key      = null;
    $lock_acquired = false;

    try {
        # Validate input
        if (empty($route) || !is_string($route)) {
            return false;
        }

        # CPU load check — defer if server is under load.
        # Only reschedule when called from the cron hook (allow_defer=true) and
        # we haven't exceeded the maximum deferral count.
        # Checked BEFORE acquiring the lock so that deferred jobs don't
        # acquire-then-immediately-release it (which defeats concurrency protection).
        if (!Metasync_CPU_Monitor::is_load_safe()) {
            if ($allow_defer && $deferral_count < $max_deferrals) {
                wp_schedule_single_event(
                    time() + 60,
                    'metasync_process_seo_job',
                    array($route, true, $deferral_count + 1)
                );
                return false;
            }
            # Max deferrals reached or not allowed to defer — drop the job to
            # prevent unbounded cron accumulation that causes server overload.
            if ($deferral_count >= $max_deferrals) {
                metasync_record_failed_action('metasync_process_seo_job');
                return false;
            }
            # allow_defer=false (sync path): just return false, parent handles retry.
            return false;
        }

        # Concurrency lock: prevent multiple SEO jobs from running simultaneously.
        # Uses a per-URL transient lock with a 120s TTL as a safety net (the lock is
        # explicitly deleted on completion). If the lock exists another run is already
        # processing this URL — reschedule once with a short delay instead of stacking.
        $lock_key = 'metasync_seo_lock_' . md5($route);
        if (get_transient($lock_key) !== false) {
            if ($allow_defer && $deferral_count < $max_deferrals) {
                wp_schedule_single_event(
                    time() + 30,
                    'metasync_process_seo_job',
                    array($route, true, $deferral_count + 1)
                );
            }
            return false;
        }
        set_transient($lock_key, true, 120);
        $lock_acquired = true;

        # Resolve redirect table: use final destination URL before 404 checks and OTTO processing
        $route = metasync_otto_resolve_redirect_to_final_url($route);

        # Skip excluded URLs - don't process SEO data for them
        if (metasync_is_otto_url_excluded($route)) {
            //error_log('MetaSync OTTO: Skipping SEO processing for excluded URL: ' . $route);
            return false;
        }

        # Pre-flight 404 check: exclude URLs that would return 404 before making API call
        if (!metasync_otto_is_url_available($route)) {
            error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404: {$route}");
            metasync_otto_auto_exclude_404_url($route);
            return false;
        }

        # Get OTTO UUID from settings
        # OPTIMIZED: Use cached options
        $otto_uuid = Metasync_Otto_Config::get_otto_uuid();

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

        # Meta descriptions are always enabled by default - no check needed

        # Fetch SEO data from OTTO API
        $seo_data = metasync_fetch_otto_seo_data($route, $otto_uuid);

        if (!$seo_data) {
            metasync_record_failed_action( 'metasync_process_seo_job' );
            return false;
        }

        # Mark this URL as crawled by OTTO for SSR
        # Extract domain and path from route
        $parsed_url = parse_url($route);
        $domain_with_scheme = ($parsed_url['scheme'] ?? 'https') . '://' . ($parsed_url['host'] ?? '');
        $url_path = ($parsed_url['path'] ?? '/');

        # Create crawl data structure
        $crawl_data = array(
            'domain' => $domain_with_scheme,
            'urls' => array($url_path)
        );

        # Load Otto pixel class and save crawl data
        $otto_pixel = new Metasync_otto_pixel($otto_uuid);
        $otto_pixel->save_crawl_data($crawl_data);

        # Get WordPress post ID from URL
        $post_id = url_to_postid($route);

        # Special handling for WooCommerce shop page (url_to_postid doesn't work for it)
        if ((!$post_id || $post_id <= 0) && function_exists('wc_get_page_id')) {
            # Check if this URL is the WooCommerce shop page
            $shop_page_id = wc_get_page_id('shop');
            if ($shop_page_id > 0) {
                $shop_url = get_permalink($shop_page_id);
                $route_normalized = rtrim($route, '/');
                $shop_url_normalized = rtrim($shop_url, '/');

                if ($route_normalized === $shop_url_normalized) {
                    $post_id = $shop_page_id;
                }
            }
        }

        # Try to find WooCommerce product by URL if url_to_postid failed
        if ((!$post_id || $post_id <= 0) && strpos($route, '/product/') !== false && function_exists('wc_get_products')) {
            # Extract product slug from URL
            $product_slug = basename(parse_url($route, PHP_URL_PATH));

            # Try to get product by slug
            $products = wc_get_products(array(
                'name' => $product_slug,
                'limit' => 1,
                'status' => 'publish',
            ));

            if (empty($products)) {
                # Fallback: try by slug using WP_Query
                $args = array(
                    'post_type' => 'product',
                    'name' => $product_slug,
                    'posts_per_page' => 1,
                    'post_status' => 'publish',
                );
                $query = new WP_Query($args);

                if ($query->have_posts()) {
                    $product_post = $query->posts[0];
                    $post_id = $product_post->ID;
                }
            } else {
                $product = $products[0];
                $post_id = $product->get_id();
            }
        }

        if (!$post_id || $post_id <= 0) {
            # Check if this is a category page
            if (strpos($route, '/category/') !== false) {
                # Extract category slug from URL
                $category_slug = basename(parse_url($route, PHP_URL_PATH));
                $category = get_category_by_slug($category_slug);

                if ($category) {
                    # Check if category would return 404 before applying OTTO changes
                    if (metasync_would_term_return_404($category->term_id, 'category', $route)) {
                        error_log("MetaSync OTTO: Skipping SEO processing for category that would return 404: {$route} (Category ID: {$category->term_id})");
                        metasync_otto_auto_exclude_404_url($route);
                        return false;
                    }
                    
                    # Update comprehensive category SEO meta fields
                    $update_result = metasync_update_comprehensive_category_seo_fields($category->term_id, $seo_data);

                    if ($update_result['updated']) {
                        # Prepare trimmed values to 30 characters
                        $trim = function($value) {
                            if ($value === null) { return ''; }
                            $value = (string) $value;
                            $value = trim($value);
                            if (mb_strlen($value) > 30) {
                                return mb_substr($value, 0, 30);
                            }
                            return $value;
                        };

                        # Log individual field updates for category
                        foreach ($update_result['fields_updated'] as $field_type => $field_value) {
                            $short = '';
                            $title = '';

                            switch ($field_type) {
                                case 'meta_title':
                                    $short = $trim($field_value);
                                    $title = "Category Meta Title Update ({$short}...)";
                                    break;
                                case 'meta_description':
                                    $short = $trim($field_value);
                                    $title = "Category Meta Description Update ({$short}...)";
                                    break;
                                case 'meta_keywords':
                                    $short = $trim($field_value);
                                    $title = "Category Meta Keywords Update ({$short}...)";
                                    break;
                                case 'og_title':
                                    $short = $trim($field_value);
                                    $title = "Category Open Graph Title Update ({$short}...)";
                                    break;
                                case 'og_description':
                                    $short = $trim($field_value);
                                    $title = "Category Open Graph Description Update ({$short}...)";
                                    break;
                                case 'twitter_title':
                                    $short = $trim($field_value);
                                    $title = "Category Twitter Title Update ({$short}...)";
                                    break;
                                case 'twitter_description':
                                    $short = $trim($field_value);
                                    $title = "Category Twitter Description Update ({$short}...)";
                                    break;
                                case 'image_alt_data':
                                    $image_count = count($field_value);
                                    $title = "Category Image Alt Text Update ({$image_count} images)";
                                    break;
                                case 'headings_data':
                                    $heading_count = count($field_value);
                                    $title = "Category Headings Update ({$heading_count} headings)";
                                    break;
                                case 'structured_data':
                                    $title = 'Category Structured Data Update';
                                    break;
                            }

                            if (!empty($title)) {
                                metasync_log_sync_history([
                                    'title' => $title,
                                    'source' => 'OTTO SEO',
                                    'status' => 'published',
                                    'content_type' => 'Category SEO',
                                    'url' => $route,
                                    'meta_data' => json_encode([
                                        'field' => $field_type,
                                        'field_value' => $field_value,
                                        'category_id' => $category->term_id,
                                        'category_name' => $category->name
                                    ])
                                ]);
                            }
                        }

                        return true;
                    }

                    return false;
                }
            }

            # Check if this is a WooCommerce product category
            if (strpos($route, '/product-category/') !== false) {
                # Extract product category slug from URL
                $category_slug = basename(parse_url($route, PHP_URL_PATH));
                $term = get_term_by('slug', $category_slug, 'product_cat');

                if ($term && !is_wp_error($term)) {
                    # Check if product category would return 404 before applying OTTO changes
                    if (metasync_would_term_return_404($term->term_id, 'product_cat', $route)) {
                        error_log("MetaSync OTTO: Skipping SEO processing for product category that would return 404: {$route} (Term ID: {$term->term_id})");
                        metasync_otto_auto_exclude_404_url($route);
                        return false;
                    }
                    
                    # Update comprehensive taxonomy SEO meta fields
                    $update_result = metasync_update_comprehensive_taxonomy_seo_fields($term->term_id, 'product_cat', $seo_data);
                    
                    if ($update_result['updated']) {
                        # Prepare trimmed values to 30 characters
                        $trim = function($value) {
                            if ($value === null) { return ''; }
                            $value = (string) $value;
                            $value = trim($value);
                            if (mb_strlen($value) > 30) {
                                return mb_substr($value, 0, 30);
                            }
                            return $value;
                        };

                        # Log individual field updates for product category
                        foreach ($update_result['fields_updated'] as $field_type => $field_value) {
                            $short = '';
                            $title = '';

                            switch ($field_type) {
                                case 'meta_title':
                                    $short = $trim($field_value);
                                    $title = "Product Category Meta Title Update ({$short}...)";
                                    break;
                                case 'meta_description':
                                    $short = $trim($field_value);
                                    $title = "Product Category Meta Description Update ({$short}...)";
                                    break;
                                case 'meta_keywords':
                                    $short = $trim($field_value);
                                    $title = "Product Category Meta Keywords Update ({$short}...)";
                                    break;
                                case 'og_title':
                                    $short = $trim($field_value);
                                    $title = "Product Category Open Graph Title Update ({$short}...)";
                                    break;
                                case 'og_description':
                                    $short = $trim($field_value);
                                    $title = "Product Category Open Graph Description Update ({$short}...)";
                                    break;
                                case 'twitter_title':
                                    $short = $trim($field_value);
                                    $title = "Product Category Twitter Title Update ({$short}...)";
                                    break;
                                case 'twitter_description':
                                    $short = $trim($field_value);
                                    $title = "Product Category Twitter Description Update ({$short}...)";
                                    break;
                                case 'image_alt_data':
                                    $image_count = count($field_value);
                                    $title = "Product Category Image Alt Text Update ({$image_count} images)";
                                    break;
                                case 'headings_data':
                                    $heading_count = count($field_value);
                                    $title = "Product Category Headings Update ({$heading_count} headings)";
                                    break;
                                case 'structured_data':
                                    $title = 'Product Category Structured Data Update';
                                    break;
                            }

                            if (!empty($title)) {
                                metasync_log_sync_history([
                                    'title' => $title,
                                    'source' => 'OTTO SEO',
                                    'status' => 'published',
                                    'content_type' => 'WooCommerce Product Category SEO',
                                    'url' => $route,
                                    'meta_data' => json_encode([
                                        'field' => $field_type,
                                        'field_value' => $field_value,
                                        'term_id' => $term->term_id,
                                        'term_name' => $term->name,
                                        'taxonomy' => 'product_cat'
                                    ])
                                ]);
                            }
                        }
                        
                        return true;
                    }
                    
                    return false;
                }
            }
            
            # Check if this is the home page (landing page)
            $site_url = rtrim(site_url(), '/');
            $route_clean = rtrim($route, '/');
            
            if ($route_clean === $site_url) {
                # Get the home page (front page)
                $front_page_id = get_option('page_on_front');
                $home_page = null;
                
                if ($front_page_id && $front_page_id > 0) {
                    $home_page = get_post($front_page_id);
                } else {
                    # If no static front page is set, get the latest post
                    $home_page = get_posts(['numberposts' => 1, 'post_status' => 'publish'])[0] ?? null;
                }
                
                if ($home_page) {
                    # Check if home page would return 404 before applying OTTO changes
                    if (metasync_would_page_return_404($home_page->ID, $route)) {
                        error_log("MetaSync OTTO: Skipping SEO processing for home page that would return 404: {$route} (Post ID: {$home_page->ID}, Status: {$home_page->post_status})");
                        metasync_otto_auto_exclude_404_url($route);
                        return false;
                    }
                    
                    # Update comprehensive home page SEO meta fields
                    $update_result = metasync_update_comprehensive_seo_fields($home_page->ID, $seo_data);
                    
                    if ($update_result['updated']) {
                        # Clear relevant caches
                        metasync_clear_post_seo_caches($home_page->ID);
                        
                        # Prepare trimmed values to 30 characters
                        $trim = function($value) {
                            if ($value === null) { return ''; }
                            $value = (string) $value;
                            $value = trim($value);
                            if (mb_strlen($value) > 30) {
                                return mb_substr($value, 0, 30);
                            }
                            return $value;
                        };

                        # Log individual field updates for home page
                        foreach ($update_result['fields_updated'] as $field_type => $field_value) {
                            $short = '';
                            $title = '';
                            
                            switch ($field_type) {
                                case 'meta_title':
                                    $short = $trim($field_value);
                                    $title = "Home Page Meta Title Update ({$short}...)";
                                    break;
                                case 'meta_description':
                                    $short = $trim($field_value);
                                    $title = "Home Page Meta Description Update ({$short}...)";
                                    break;
                                case 'meta_keywords':
                                    $short = $trim($field_value);
                                    $title = "Home Page Meta Keywords Update ({$short}...)";
                                    break;
                                case 'og_title':
                                    $short = $trim($field_value);
                                    $title = "Home Page Open Graph Title Update ({$short}...)";
                                    break;
                                case 'og_description':
                                    $short = $trim($field_value);
                                    $title = "Home Page Open Graph Description Update ({$short}...)";
                                    break;
                                case 'twitter_title':
                                    $short = $trim($field_value);
                                    $title = "Home Page Twitter Title Update ({$short}...)";
                                    break;
                                case 'twitter_description':
                                    $short = $trim($field_value);
                                    $title = "Home Page Twitter Description Update ({$short}...)";
                                    break;
                                case 'image_alt_data':
                                    $image_count = count($field_value);
                                    $title = "Home Page Image Alt Text Update ({$image_count} images)";
                                    break;
                                case 'headings_data':
                                    $heading_count = count($field_value);
                                    $title = "Home Page Headings Update ({$heading_count} headings)";
                                    break;
                                case 'structured_data':
                                    $title = 'Home Page Structured Data Update';
                                    break;
                            }

                            if (!empty($title)) {
                                metasync_log_sync_history([
                                    'title' => $title,
                                    'source' => 'OTTO SEO',
                                    'status' => 'published',
                                    'content_type' => 'Home Page SEO',
                                    'url' => $route,
                                    'meta_data' => json_encode([
                                        'field' => $field_type,
                                        'field_value' => $field_value,
                                        'post_id' => $home_page->ID
                                    ])
                                ]);
                            }
                        }
                        
                        return true;
                    }
                    
                    return false;
                }
            }

            # Check if this is the blog/posts page (page_for_posts)
            $posts_page_id = intval(get_option('page_for_posts'));
            if ($posts_page_id > 0) {
                $posts_page = get_post($posts_page_id);
                $posts_page_url = rtrim(get_permalink($posts_page_id), '/');

                if ($posts_page && $route_clean === $posts_page_url) {
                    # Check if blog page would return 404
                    if (metasync_would_page_return_404($posts_page->ID, $route)) {
                        error_log("MetaSync OTTO: Skipping SEO processing for blog page that would return 404: {$route} (Post ID: {$posts_page->ID}, Status: {$posts_page->post_status})");
                        metasync_otto_auto_exclude_404_url($route);
                        return false;
                    }

                    # Update comprehensive blog page SEO meta fields
                    $update_result = metasync_update_comprehensive_seo_fields($posts_page->ID, $seo_data);

                    if ($update_result['updated']) {
                        # Clear relevant caches
                        metasync_clear_post_seo_caches($posts_page->ID);

                        # Prepare trimmed values to 30 characters
                        $trim = function($value) {
                            if ($value === null) { return ''; }
                            $value = (string) $value;
                            $value = trim($value);
                            if (mb_strlen($value) > 30) {
                                return mb_substr($value, 0, 30);
                            }
                            return $value;
                        };

                        # Log individual field updates for blog page
                        foreach ($update_result['fields_updated'] as $field_type => $field_value) {
                            $short = '';
                            $title = '';

                            switch ($field_type) {
                                case 'meta_title':
                                    $short = $trim($field_value);
                                    $title = "Blog Page Meta Title Update ({$short}...)";
                                    break;
                                case 'meta_description':
                                    $short = $trim($field_value);
                                    $title = "Blog Page Meta Description Update ({$short}...)";
                                    break;
                                case 'meta_keywords':
                                    $short = $trim($field_value);
                                    $title = "Blog Page Meta Keywords Update ({$short}...)";
                                    break;
                                case 'og_title':
                                    $short = $trim($field_value);
                                    $title = "Blog Page Open Graph Title Update ({$short}...)";
                                    break;
                                case 'og_description':
                                    $short = $trim($field_value);
                                    $title = "Blog Page Open Graph Description Update ({$short}...)";
                                    break;
                                case 'twitter_title':
                                    $short = $trim($field_value);
                                    $title = "Blog Page Twitter Title Update ({$short}...)";
                                    break;
                                case 'twitter_description':
                                    $short = $trim($field_value);
                                    $title = "Blog Page Twitter Description Update ({$short}...)";
                                    break;
                                case 'image_alt_data':
                                    $image_count = count($field_value);
                                    $title = "Blog Page Image Alt Text Update ({$image_count} images)";
                                    break;
                                case 'headings_data':
                                    $heading_count = count($field_value);
                                    $title = "Blog Page Headings Update ({$heading_count} headings)";
                                    break;
                                case 'structured_data':
                                    $title = 'Blog Page Structured Data Update';
                                    break;
                            }

                            if (!empty($title)) {
                                metasync_log_sync_history([
                                    'title' => $title,
                                    'source' => 'OTTO SEO',
                                    'status' => 'published',
                                    'content_type' => 'Blog Page SEO',
                                    'url' => $route,
                                    'meta_data' => json_encode([
                                        'field' => $field_type,
                                        'field_value' => $field_value,
                                        'post_id' => $posts_page->ID
                                    ])
                                ]);
                            }
                        }

                        return true;
                    }

                    return false;
                }
            }

            # URL didn't resolve to any supported entity (post, category, home page, blog page)
            # Treat as 404 and auto-exclude (e.g. deleted post, non-existent page)
            if (!metasync_otto_is_url_available($route)) {
                error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404 (no matching entity): {$route}");
                metasync_otto_auto_exclude_404_url($route);
            }
            return false;
        }
        
        # Verify this is actually a post, page, or WooCommerce product
        $post = get_post($post_id);

        # Get supported post types dynamically
        $supported_post_types = metasync_get_supported_post_types();

        if (!$post || !in_array($post->post_type, $supported_post_types)) {
            # Skip unsupported post types
            return false;
        }

        # Check if page would return 404 before applying OTTO changes
        if (metasync_would_page_return_404($post_id, $route)) {
            error_log("MetaSync OTTO: Skipping SEO processing for URL that would return 404: {$route} (Post ID: {$post_id}, Status: {$post->post_status})");
            metasync_otto_auto_exclude_404_url($route);
            return false;
        }

        # Update comprehensive SEO meta fields
        $update_result = metasync_update_comprehensive_seo_fields($post_id, $seo_data);

        if ($update_result['updated']) {
            # Clear relevant caches
            metasync_clear_post_seo_caches($post_id);

            # Prepare trimmed values to 30 characters
            $trim = function($value) {
                if ($value === null) { return ''; }
                $value = (string) $value;
                $value = trim($value);
                if (mb_strlen($value) > 30) {
                    return mb_substr($value, 0, 30);
                }
                return $value;
            };

            # Log individual field updates
            foreach ($update_result['fields_updated'] as $field_type => $field_value) {
                $short = '';
                $title = '';
                
                switch ($field_type) {
                    case 'meta_title':
                        $short = $trim($field_value);
                        $title = 'Meta Title Update (' . $short . '...)';
                        break;
                    case 'meta_description':
                        $short = $trim($field_value);
                        $title = 'Meta Description Update (' . $short . '...)';
                        break;
                    case 'meta_keywords':
                        $short = $trim($field_value);
                        $title = 'Meta Keywords Update (' . $short . '...)';
                        break;
                    case 'og_title':
                        $short = $trim($field_value);
                        $title = 'Open Graph Title Update (' . $short . '...)';
                        break;
                    case 'og_description':
                        $short = $trim($field_value);
                        $title = 'Open Graph Description Update (' . $short . '...)';
                        break;
                    case 'twitter_title':
                        $short = $trim($field_value);
                        $title = 'Twitter Title Update (' . $short . '...)';
                        break;
                    case 'twitter_description':
                        $short = $trim($field_value);
                        $title = 'Twitter Description Update (' . $short . '...)';
                        break;
                    case 'image_alt_data':
                        $image_count = count($field_value);
                        $title = "Image Alt Text Update ({$image_count} images)";
                        break;
                    case 'headings_data':
                        $heading_count = count($field_value);
                        $title = "Headings Update ({$heading_count} headings)";
                        break;
                    case 'structured_data':
                        $title = 'Structured Data Update';
                        break;
                }

                if (!empty($title)) {
                    metasync_log_sync_history([
                        'title' => $title,
                        'source' => 'OTTO SEO',
                        'status' => 'published',
                        'content_type' => 'SEO Meta',
                        'url' => $route,
                        'meta_data' => json_encode([
                            'field' => $field_type,
                            'field_value' => $field_value,
                            'post_id' => $post_id
                        ])
                    ]);
                }
            }

            return true;
        }

        return false;

    } catch (Exception $e) {
        metasync_record_failed_action( 'metasync_process_seo_job' );
        return false;
    } finally {
        # Release the concurrency lock only if we actually acquired it.
        # Early returns (CPU deferral, lock contention) must NOT delete a lock
        # that another process may be holding.
        if ($lock_acquired && $lock_key) {
            delete_transient($lock_key);
        }
    }
}

/**
 * Log sync history entry
 * @param array $data Sync data to log
 */
function metasync_log_sync_history($data) {
    try {
        // Classes are now autoloaded, no need for manual require
        $sync_db = new Metasync_Sync_History_Database();

        // Minimal duplicate prevention within short time window
        if (!empty($data['title']) && !empty($data['source'])) {
            global $wpdb;
            $table = $wpdb->prefix . Metasync_Sync_History_Database::$table_name;
            $recent = $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM `$table` WHERE title = %s AND source = %s AND created_at >= %s",
                $data['title'],
                $data['source'],
                gmdate('Y-m-d H:i:s', time() - 60)
            ));
            if ((int)$recent > 0) {
                return; // skip duplicate log within 60 seconds
            }
        }

        $sync_db->add($data);

    } catch (Exception $e) {
        error_log("MetaSync: Failed to log sync history: " . $e->getMessage());
    }
}

/**
 * Get supported post types for OTTO SEO optimization
 * Includes WooCommerce products if WooCommerce is active
 *
 * @return array List of supported post types
 */
function metasync_get_supported_post_types() {
    # Start with default post types
    $post_types = ['post', 'page'];

    # Add WooCommerce product post type if WooCommerce is active
    if (class_exists('WooCommerce') || function_exists('is_woocommerce')) {
        $post_types[] = 'product';
    }

    # Include all public custom post types (e.g. 'location', 'service', 'team', etc.)
    # so OTTO can write post meta for them during metasync_process_otto_seo_data().
    $custom_post_types = get_post_types(['public' => true, '_builtin' => false], 'names');
    if (!empty($custom_post_types)) {
        $post_types = array_merge($post_types, array_values($custom_post_types));
    }

    # Allow developers to filter supported post types
    $post_types = apply_filters('metasync_otto_supported_post_types', $post_types);

    return $post_types;
}

/**
 * Get supported taxonomies for OTTO SEO optimization
 * Includes WooCommerce product categories and tags if WooCommerce is active
 *
 * @return array List of supported taxonomies
 */
function metasync_get_supported_taxonomies() {
    # Start with default taxonomies
    $taxonomies = ['category'];

    # Add WooCommerce taxonomies if WooCommerce is active
    if (class_exists('WooCommerce') || function_exists('is_woocommerce')) {
        $taxonomies[] = 'product_cat';  # WooCommerce product categories
        $taxonomies[] = 'product_tag';  # WooCommerce product tags
    }

    # Allow developers to filter supported taxonomies
    $taxonomies = apply_filters('metasync_otto_supported_taxonomies', $taxonomies);

    return $taxonomies;
}

/**
 * Resolve a URL through the Redirect Manager table to its final destination (follows redirect chains).
 * Used before 404 checks and OTTO processing so the final canonical URL is used, not intermediate redirects.
 *
 * @param string $url Full URL (e.g. https://example.com/old-page)
 * @return string Final destination URL, or original $url if no redirect matches
 */
function metasync_otto_resolve_redirect_to_final_url($url)
{
    if (empty($url) || !is_string($url)) {
        return $url;
    }
    try {
        $db_path = plugin_dir_path(dirname(__FILE__)) . 'redirections/class-metasync-redirection-database.php';
        $class_path = plugin_dir_path(dirname(__FILE__)) . 'redirections/class-metasync-redirection.php';
        if (!file_exists($db_path) || !file_exists($class_path)) {
            return $url;
        }
        require_once $db_path;
        require_once $class_path;
        $db = new Metasync_Redirection_Database();
        $redirect = new Metasync_Redirection($db);
        return $redirect->resolve_url_to_final_destination($url, 10);
    } catch (Exception $e) {
        error_log('MetaSync OTTO: Redirect resolution failed for ' . $url . ' - ' . $e->getMessage());
        return $url;
    }
}

/**
 * Auto-exclude a URL from OTTO with description "Auto-excluded: 404"
 * Called when a URL is detected as returning 404 so it won't be sent to OTTO again
 *
 * @param string $url Full URL to exclude (e.g. https://example.com/404-page)
 * @return bool|string True on success, false on failure, 'duplicate'/'reactivated' if already exists
 */
function metasync_otto_auto_exclude_404_url($url)
{
    if (empty($url) || !is_string($url)) {
        return false;
    }
    $url = filter_var($url, FILTER_SANITIZE_URL);
    $url = esc_url_raw($url);
    if (empty($url) || mb_strlen($url) > 2048) {
        return false;
    }
    try {
        require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php';
        $db = new Metasync_Otto_Excluded_URLs_Database();
        return $db->add([
            'url_pattern' => $url,
            'pattern_type' => 'exact',
            'description' => 'Auto-excluded: 404',
            'status' => 'active',
            'auto_excluded' => 1,
        ]);
    } catch (Exception $e) {
        error_log('MetaSync OTTO: Failed to auto-exclude 404 URL: ' . $url . ' - ' . $e->getMessage());
        return false;
    }
}

/**
 * Remove a URL from the OTTO auto-exclusion list.
 * Called when OTTO sends a webhook for a URL, confirming it is valid and crawlable.
 * Only removes records where auto_excluded = 1 (never removes manual exclusions).
 *
 * @param string $url Full URL to un-exclude (e.g. https://example.com/location/page)
 * @return bool True on success
 */
function metasync_otto_remove_auto_exclusion($url)
{
    if (empty($url) || !is_string($url)) {
        return false;
    }
    try {
        require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php';
        $db  = new Metasync_Otto_Excluded_URLs_Database();
        global $wpdb;
        $table = $wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name;
        # Normalize the same way is_url_excluded() does
        $url_normalized = rtrim(trim($url), '/');
        $records = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id FROM `{$table}` WHERE url_pattern = %s AND auto_excluded = 1 AND status = 'active'",
                $url_normalized
            )
        );
        if (!empty($records)) {
            $ids = array_map(function ($r) { return (int) $r->id; }, $records);
            $db->delete($ids);
        }
        return true;
    } catch (Exception $e) {
        return false;
    }
}

/**
 * Check if a URL is MANUALLY excluded from OTTO (auto_excluded = 0).
 * Used at render time (metasync_start_otto) — auto-exclusions must NOT block
 * rendering because they are often false positives (e.g. custom post types that
 * url_to_postid() can't resolve). Auto-exclusions are only used to gate the
 * SEO meta-writing webhook path.
 *
 * @param string $url URL to check
 * @return bool True if URL has a manual exclusion
 */
function metasync_is_otto_url_manually_excluded($url)
{
    if (empty($url) || !is_string($url)) {
        return false;
    }
    try {
        require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php';
        global $wpdb;
        $table = $wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name;
        $url_normalized = rtrim(trim($url), '/');

        $records = get_transient(METASYNC_OTTO_EXCLUDED_TRANSIENT_KEY);

        if ($records === false) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- no user input, table name from $wpdb->prefix
            $records = $wpdb->get_results(
                "SELECT url_pattern, pattern_type FROM `{$table}` WHERE status = 'active' AND (auto_excluded = 0 OR auto_excluded IS NULL) ORDER BY created_at DESC"
            );

            // Graceful recovery: auto_excluded column missing on pre-v2.7.4 installs.
            // Run ALTER TABLE to add it and treat URL as not excluded so OTTO continues rendering.
            if ($wpdb->last_error && strpos($wpdb->last_error, 'auto_excluded') !== false) {
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
                $wpdb->query("ALTER TABLE `{$table}` ADD COLUMN `auto_excluded` TINYINT(1) NOT NULL DEFAULT 0");
                return false;
            }

            set_transient(METASYNC_OTTO_EXCLUDED_TRANSIENT_KEY, $records ?: [], METASYNC_OTTO_EXCLUDED_TRANSIENT_TTL);
        }

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

        foreach ($records as $excluded) {
            $pattern      = rtrim(trim($excluded->url_pattern), '/');
            $pattern_type = $excluded->pattern_type;

            switch ($pattern_type) {
                case 'exact':
                    if ($url_normalized === $pattern) {
                        return true;
                    }
                    break;
                case 'contain':
                    if (strpos($url_normalized, $pattern) !== false) {
                        return true;
                    }
                    break;
                case 'start':
                    if (strpos($url_normalized, $pattern) === 0) {
                        return true;
                    }
                    break;
            }
        }
        return false;
    } catch (Exception $e) {
        return false;
    }
}

/**
 * Check if a URL is excluded from OTTO
 * @param string $url URL to check
 * @return bool True if URL is excluded, false otherwise
 */
function metasync_is_otto_url_excluded($url)
{
    try {
        // Load database class
        require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php';
        $db = new Metasync_Otto_Excluded_URLs_Database();

        // Check if URL is excluded
        return $db->is_url_excluded($url);

    } catch (Exception $e) {
        return false;
    }
}

/**
 * Check if a URL is now available (would NOT return 404)
 * Uses same resolution logic as metasync_process_otto_seo_data
 * Used when rechecking auto-excluded 404 URLs after 7 days
 *
 * @param string $url Full URL to check (e.g. https://example.com/page)
 * @return bool True if URL is accessible, false if it would return 404
 */
function metasync_otto_is_url_available($url)
{
    if (empty($url) || !is_string($url)) {
        return false;
    }

    $route = metasync_otto_resolve_redirect_to_final_url($url);
    $post_id = url_to_postid($route);

    # WooCommerce shop page
    if ((!$post_id || $post_id <= 0) && function_exists('wc_get_page_id')) {
        $shop_page_id = wc_get_page_id('shop');
        if ($shop_page_id > 0) {
            $shop_url = get_permalink($shop_page_id);
            if (rtrim($route, '/') === rtrim($shop_url, '/')) {
                $post_id = $shop_page_id;
            }
        }
    }

    # WooCommerce product by slug
    if ((!$post_id || $post_id <= 0) && strpos($route, '/product/') !== false && function_exists('wc_get_products')) {
        $product_slug = basename(parse_url($route, PHP_URL_PATH));
        $products = wc_get_products(array('name' => $product_slug, 'limit' => 1, 'status' => 'publish'));
        if (!empty($products)) {
            $post_id = $products[0]->get_id();
        } else {
            $query = new WP_Query(array(
                'post_type' => 'product',
                'name' => $product_slug,
                'posts_per_page' => 1,
                'post_status' => 'publish',
            ));
            if ($query->have_posts()) {
                $post_id = $query->posts[0]->ID;
            }
        }
    }

    if ($post_id && $post_id > 0) {
        $post = get_post($post_id);
        # Accept any post type — custom post types (e.g. 'location', 'service') are valid URLs.
        # The old in_array check against metasync_get_supported_post_types() caused CPT URLs
        # to be wrongly auto-excluded as "404" pages.
        if ($post) {
            return !metasync_would_page_return_404($post_id, $route);
        }
    }

    # Category
    if (strpos($route, '/category/') !== false) {
        $category_slug = basename(parse_url($route, PHP_URL_PATH));
        $category = get_category_by_slug($category_slug);
        if ($category) {
            return !metasync_would_term_return_404($category->term_id, 'category', $route);
        }
    }

    # WooCommerce product category
    if (strpos($route, '/product-category/') !== false) {
        $category_slug = basename(parse_url($route, PHP_URL_PATH));
        $term = get_term_by('slug', $category_slug, 'product_cat');
        if ($term && !is_wp_error($term)) {
            return !metasync_would_term_return_404($term->term_id, 'product_cat', $route);
        }
    }

    # Home page
    if (rtrim($route, '/') === rtrim(site_url(), '/')) {
        $front_page_id = get_option('page_on_front');
        $home_page = ($front_page_id && $front_page_id > 0)
            ? get_post($front_page_id)
            : (get_posts(['numberposts' => 1, 'post_status' => 'publish'])[0] ?? null);
        if ($home_page) {
            return !metasync_would_page_return_404($home_page->ID, $route);
        }
    }

    # Could not verify availability from local data (custom archive, paginated page, etc.).
    # Assume the URL IS available — OTTO only crawls reachable URLs, so if we can't
    # prove it's a 404, we should not auto-exclude it.
    return true;
}

/**
 * Recheck auto-excluded 404 URLs when recheck_after has passed; remove from exclusion if now available
 * Uses recheck_after timestamp (default 7 days from exclusion) to decide when to recheck
 * Mark as permanent after 30 days if still 404 (no further rechecks)
 * Called by daily cron job
 */
function metasync_otto_recheck_404_exclusions()
{
    try {
        require_once plugin_dir_path(__FILE__) . 'class-metasync-otto-excluded-urls-database.php';
        $db = new Metasync_Otto_Excluded_URLs_Database();
        $records = $db->get_auto_excluded_404_urls_due_for_recheck();

        if (empty($records)) {
            return;
        }

        $removed = 0;
        $marked_permanent = 0;
        $thirty_days_ago = strtotime('-30 days');
        $next_recheck = date('Y-m-d H:i:s', current_time('timestamp') + 7 * DAY_IN_SECONDS);

        foreach ($records as $record) {
            $url = trim($record->url_pattern);
            if (empty($url)) {
                continue;
            }
            if (metasync_otto_is_url_available($url)) {
                $db->delete([$record->id]);
                $removed++;
            } else {
                # Still 404: if excluded 30+ days ago, mark as permanent (no more rechecks)
                $created_ts = strtotime($record->created_at);
                if ($created_ts <= $thirty_days_ago) {
                    $db->update(['is_permanent' => 1], $record->id);
                    $marked_permanent++;
                } else {
                    # Schedule next recheck in 7 days
                    $db->update(['recheck_after' => $next_recheck], $record->id);
                }
            }
        }

        if ($removed > 0) {
            error_log("MetaSync OTTO: Recheck 404 exclusions - removed {$removed} URL(s) that are now available");
        }
        if ($marked_permanent > 0) {
            error_log("MetaSync OTTO: Recheck 404 exclusions - marked {$marked_permanent} URL(s) as permanent (still 404 after 30 days)");
        }
    } catch (Exception $e) {
        error_log('MetaSync OTTO: Recheck 404 exclusions failed - ' . $e->getMessage());
    }
}

add_action('metasync_otto_recheck_404_exclusions', 'metasync_otto_recheck_404_exclusions');

/**
 * Check if a post/page would return 404 without making HTTP request
 * Uses WordPress database checks for fast validation
 * 
 * @param int $post_id WordPress post ID
 * @param string $url The URL being checked (optional, for logging)
 * @return bool True if page would return 404, false if accessible
 */
function metasync_would_page_return_404($post_id, $url = '') {
    if (!$post_id || $post_id <= 0) {
        return true; // No post ID = 404
    }
    
    # Get the post object
    $post = get_post($post_id);
    if (!$post) {
        return true; // Post doesn't exist = 404
    }
    
    # 1. Check post status - must be 'publish' to be publicly accessible
    if ($post->post_status !== 'publish') {
        return true; // Draft, pending, private, etc. = 404
    }
    
    # 2. Check if post is password protected (requires password to view)
    if (!empty($post->post_password)) {
        # Password protected posts are not publicly accessible without password
        return true; // Password protected = effectively 404 for public
    }
    
    # 3. Check if post is in trash
    if ($post->post_status === 'trash') {
        return true; // Trashed = 404
    }
    
    # 4. Check if post type is publicly queryable
    $post_type_object = get_post_type_object($post->post_type);
    if ($post_type_object && !$post_type_object->publicly_queryable) {
        # Some post types might not be publicly accessible
        # But we allow if it's in our supported types
        $supported_post_types = metasync_get_supported_post_types();
        if (!in_array($post->post_type, $supported_post_types)) {
            return true; // Not publicly queryable = 404
        }
    }
    
    # 5. WordPress 5.7+ has a built-in function for this
    if (function_exists('is_post_publicly_viewable')) {
        if (!is_post_publicly_viewable($post)) {
            return true; // Not publicly viewable = 404
        }
    }
    
    # 6. Check if post is scheduled for future (not yet published)
    if ($post->post_date > current_time('mysql')) {
        return true; // Future post = 404 until publish date
    }
    
    # All checks passed - page should be accessible
    return false;
}

/**
 * Check if a taxonomy term (category, tag, etc.) would return 404
 * Uses WordPress database checks for fast validation
 * 
 * @param int $term_id Term ID
 * @param string $taxonomy Taxonomy name (e.g., 'category', 'product_cat')
 * @param string $url The URL being checked (optional, for logging)
 * @return bool True if term would return 404, false if accessible
 */
function metasync_would_term_return_404($term_id, $taxonomy, $url = '') {
    if (!$term_id || $term_id <= 0 || empty($taxonomy)) {
        return true; // Invalid term = 404
    }
    
    # Get the term object
    $term = get_term($term_id, $taxonomy);
    if (is_wp_error($term) || !$term) {
        return true; // Term doesn't exist = 404
    }
    
    # Check if taxonomy is publicly queryable
    $taxonomy_object = get_taxonomy($taxonomy);
    if (!$taxonomy_object || !$taxonomy_object->public) {
        # Check if it's in our supported taxonomies
        $supported_taxonomies = metasync_get_supported_taxonomies();
        if (!in_array($taxonomy, $supported_taxonomies)) {
            return true; // Not publicly queryable = 404
        }
    }
    
    # Terms are generally always accessible if they exist and taxonomy is public
    # WordPress doesn't have a "draft" status for terms like posts do
    # But we can check if the term has a count (has posts assigned)
    # Empty terms might not be useful, but they're still accessible
    
    # All checks passed - term should be accessible
    return false;
}

/**
 * Invalidate Brizy posts cache when posts are saved
 * OPTIMIZATION: Clears transient cache to ensure accurate detection
 */
add_action('save_post', function($post_id) {
    # Check if this post has Brizy metadata
    if (get_post_meta($post_id, 'brizy_post_uid', true)) {
        delete_transient('metasync_has_brizy_posts');
    }
}, 10, 1);
```
