<?php
/**
 * DBA GeoBot Inbound Linker — Site-wide scanner for inbound links TO city articles
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  PROBLEM: GeoBot city posts are an isolated silo. No PageRank flows
 *  in from the rest of the blog (core EN/FR articles, PE news, LuxStrat,
 *  service pages, Italian-language pages, etc.)
 *
 *  SOLUTION: Scans the ENTIRE site — every post, every page, every
 *  language — and auto-links city name mentions to GeoBot city articles.
 *
 *  SCAN STRATEGY (3-tier):
 *    Tier 1 — WPML language match:
 *      Posts/pages in the country's WPML language (e.g. 'it' for Italy)
 *    Tier 2 — URL pattern match:
 *      Posts/pages whose slug contains country-related terms
 *      (e.g. "italy", "italia", "italian", "italiano", "investire")
 *    Tier 3 — Full site sweep:
 *      Every remaining published post/page on the entire site
 *
 *  All 3 tiers run in a single scan pass. The tiering controls
 *  PRIORITY only (Tier 1 posts are processed first), not inclusion.
 *
 *  LINK TARGETS (what we link TO):
 *    - Published GeoBot city posts for the target country
 *    - The hub/pillar post for the target country
 *    - Commune names from the registry (even if post not yet published)
 *
 *  EXCLUSIONS (never scanned):
 *    - The target country's own GeoBot city posts (no self-linking)
 *    - The target country's own hub post
 *    Note: Other countries' GeoBot posts ARE scanned (France post
 *    mentioning Milan → link to Italy Milan article)
 *
 *  LINK RULES:
 *    - Only first safe mention of each city per post
 *    - Max 3 GeoBot links per post (avoid over-optimization)
 *    - Never link inside <a>, <h1>–<h4>, <script>, <style>, comments
 *    - City names < 4 chars skipped (no false positives)
 *    - Case-insensitive match, original case preserved
 *    - All links tagged with class="dba-geo-inbound" (fully reversible)
 *
 *  ZERO CONFLICT: Own meta key (_dba_geo_inbound_linked), own CSS class.
 * ═══════════════════════════════════════════════════════════════════════
 *
 * @since 2.12.0
 */

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

class DBA_GeoBot_Inbound {

    private static $instance = null;

    /** Max GeoBot inbound links per post */
    const MAX_LINKS_PER_POST = 3;

    /** Min city name length (skip "Pau", "Eu", "Aci", "Nus") */
    const MIN_CITY_NAME_LENGTH = 4;

    /** Batch size for DB queries */
    const SCAN_BATCH_SIZE = 50;

    /** Max posts per scan run (safety valve) */
    const MAX_SCAN_POSTS = 2000;

    /**
     * Country-related URL slug patterns.
     * If a post/page slug matches any of these, it's a Tier 2 candidate.
     */
    private static $url_patterns = array(
        'france'   => array( 'france', 'french', 'francais', 'francaise', 'investir-en-france', 'investissement' ),
        'italy'    => array( 'italy', 'italia', 'italian', 'italiano', 'italiana', 'investire', 'investire-in-italia', 'investimenti' ),
        'spain'    => array( 'spain', 'espana', 'spanish', 'espanol', 'espanola', 'invertir', 'invertir-en-espana', 'inversiones' ),
        'portugal' => array( 'portugal', 'portuguese', 'portugues', 'portuguesa', 'investir-em-portugal', 'investimento' ),
    );

    /**
     * Get URL patterns for a country (public accessor for admin display).
     */
    public static function get_url_patterns( string $country_key ) {
        return self::$url_patterns[ $country_key ] ?? array();
    }

    public static function get_instance() {
        if ( null === self::$instance ) self::$instance = new self();
        return self::$instance;
    }

    private function __construct() {
        add_action( 'wp_ajax_dba_geo_inbound_scan',       array( $this, 'ajax_run_scan' ) );
        add_action( 'wp_ajax_dba_geo_inbound_scan_stats', array( $this, 'ajax_get_stats' ) );
        add_action( 'wp_ajax_dba_geo_inbound_strip',      array( $this, 'ajax_strip_links' ) );
    }

    /* =================================================================
     *  LINK TARGET INDEX — What we link TO
     * ================================================================= */

    /**
     * Build a lookup table of link targets for a country:
     *   city name (lowercase) → { permalink, post_id, commune, country, type }
     *
     * Includes:
     *   - Published GeoBot city posts
     *   - Hub/pillar post (linked via country name keywords)
     *
     * @param string $country_key
     * @return array Keyed by lowercase city name, sorted longest-first.
     */
    public function build_link_targets( string $country_key ) {
        $config = DBA_GeoBot_Registry::get_country( $country_key );
        if ( ! $config ) return array();

        $targets = array();

        /* ── City posts ── */
        $meta_key = $config['meta_prefix'] . 'auto_created';
        $posts = get_posts( array(
            'post_type'      => 'post',
            'post_status'    => 'publish',
            'posts_per_page' => 500,
            'meta_query'     => array(
                array( 'key' => $meta_key, 'compare' => 'EXISTS' ),
            ),
            'fields' => 'ids',
        ) );

        foreach ( $posts as $pid ) {
            $commune = get_post_meta( $pid, $config['meta_prefix'] . 'commune', true );
            if ( ! $commune ) continue;
            if ( mb_strlen( $commune, 'UTF-8' ) < self::MIN_CITY_NAME_LENGTH ) continue;

            $key = mb_strtolower( trim( $commune ), 'UTF-8' );
            if ( ! isset( $targets[ $key ] ) ) {
                $targets[ $key ] = array(
                    'post_id'   => $pid,
                    'commune'   => $commune,
                    'permalink' => get_permalink( $pid ),
                    'title'     => get_the_title( $pid ),
                    'country'   => $country_key,
                    'type'      => 'city',
                );
            }
        }

        /* ── Hub post (link "invest in Italy" type phrases) ── */
        $hub_id = (int) get_option( 'dba_geo_' . $country_key . '_hub_page_id', 0 );
        if ( $hub_id && get_post_status( $hub_id ) === 'publish' ) {
            $hub_permalink = get_permalink( $hub_id );
            $hub_title     = get_the_title( $hub_id );

            // Register hub under the H1 format keywords
            // e.g. "Investire in Italia" → hub
            $hub_phrases = $this->get_hub_anchor_phrases( $country_key );
            foreach ( $hub_phrases as $phrase ) {
                if ( mb_strlen( $phrase, 'UTF-8' ) >= self::MIN_CITY_NAME_LENGTH ) {
                    $pkey = mb_strtolower( $phrase, 'UTF-8' );
                    if ( ! isset( $targets[ $pkey ] ) ) {
                        $targets[ $pkey ] = array(
                            'post_id'   => $hub_id,
                            'commune'   => $phrase,
                            'permalink' => $hub_permalink,
                            'title'     => $hub_title,
                            'country'   => $country_key,
                            'type'      => 'hub',
                        );
                    }
                }
            }
        }

        // Sort by name length descending (match "Saint-Étienne" before "Étienne")
        uasort( $targets, function( $a, $b ) {
            return mb_strlen( $b['commune'] ) - mb_strlen( $a['commune'] );
        } );

        return $targets;
    }

    /**
     * Get anchor text phrases that should link to the hub post.
     * These are country-specific investment keyword phrases.
     *
     * @param string $country_key
     * @return array
     */
    private function get_hub_anchor_phrases( string $country_key ) {
        $phrases = array(
            'france' => array(
                'Investir en France',
                'investissements en France',
                'investissement en France',
                'investing in France',
                'investment in France',
            ),
            'italy' => array(
                'Investire in Italia',
                'investimenti in Italia',
                'investimento in Italia',
                'investing in Italy',
                'investment in Italy',
                'invest in Italy',
            ),
            'spain' => array(
                'Invertir en España',
                'inversiones en España',
                'inversión en España',
                'investing in Spain',
                'investment in Spain',
                'invest in Spain',
            ),
            'portugal' => array(
                'Investir em Portugal',
                'investimentos em Portugal',
                'investimento em Portugal',
                'investing in Portugal',
                'investment in Portugal',
                'invest in Portugal',
            ),
        );

        return $phrases[ $country_key ] ?? array();
    }

    /**
     * Build combined targets for multiple countries.
     *
     * @param array $country_keys  Empty = all registered countries.
     * @return array Combined targets.
     */
    public function build_multi_country_targets( array $country_keys = array() ) {
        if ( empty( $country_keys ) ) {
            $country_keys = DBA_GeoBot_Registry::get_country_keys();
        }

        $combined = array();
        foreach ( $country_keys as $key ) {
            $country_targets = $this->build_link_targets( $key );
            foreach ( $country_targets as $city_key => $data ) {
                // Prefix with country to avoid cross-country collisions
                $combined_key = $data['country'] . ':' . $city_key;
                $combined[ $combined_key ] = $data;
            }
        }

        return $combined;
    }

    /* =================================================================
     *  BUILD EXCLUDE LIST — Posts we never scan
     * ================================================================= */

    /**
     * Get post IDs to exclude from scanning for a given country.
     * Only excludes the TARGET country's own GeoBot posts + hub.
     * Other countries' GeoBot posts ARE scanned.
     *
     * @param string $country_key  Single country or 'all'.
     * @return array Post IDs.
     */
    private function build_exclude_ids( string $country_key ) {
        $exclude = array();
        $countries = ( $country_key === 'all' )
            ? DBA_GeoBot_Registry::get_country_keys()
            : array( $country_key );

        foreach ( $countries as $ck ) {
            $config = DBA_GeoBot_Registry::get_country( $ck );
            if ( ! $config ) continue;

            // This country's GeoBot city posts
            $meta_key = $config['meta_prefix'] . 'auto_created';
            $geo_posts = get_posts( array(
                'post_type'      => 'post',
                'post_status'    => 'any',
                'posts_per_page' => 1000,
                'meta_query'     => array(
                    array( 'key' => $meta_key, 'compare' => 'EXISTS' ),
                ),
                'fields' => 'ids',
            ) );
            $exclude = array_merge( $exclude, $geo_posts );

            // This country's hub post
            $hub_id = (int) get_option( 'dba_geo_' . $ck . '_hub_page_id', 0 );
            if ( $hub_id ) $exclude[] = $hub_id;
        }

        return array_unique( array_filter( $exclude ) );
    }

    /* =================================================================
     *  WPML HELPERS
     * ================================================================= */

    /**
     * Get the WPML language of a post.
     *
     * @param int $post_id
     * @return string|null Language code or null.
     */
    private function get_post_language( int $post_id ) {
        if ( ! function_exists( 'wpml_get_language_information' ) ) return null;

        $lang_info = wpml_get_language_information( null, $post_id );
        if ( is_wp_error( $lang_info ) ) return null;

        return $lang_info['language_code'] ?? null;
    }

    /**
     * Get all post IDs in a specific WPML language.
     *
     * @param string $lang_code  e.g. 'it', 'fr', 'es'
     * @param array  $exclude_ids
     * @return array Post IDs.
     */
    private function get_posts_by_language( string $lang_code, array $exclude_ids = array() ) {
        global $wpdb;

        // WPML stores language info in icl_translations table
        $icl_table = $wpdb->prefix . 'icl_translations';

        if ( $wpdb->get_var( "SHOW TABLES LIKE '{$icl_table}'" ) !== $icl_table ) {
            return array(); // WPML not installed
        }

        $exclude_str = '';
        if ( ! empty( $exclude_ids ) ) {
            $exclude_str = ' AND p.ID NOT IN (' . implode( ',', array_map( 'intval', $exclude_ids ) ) . ')';
        }

        $results = $wpdb->get_col( $wpdb->prepare(
            "SELECT p.ID FROM {$wpdb->posts} p
             INNER JOIN {$icl_table} t ON t.element_id = p.ID
             WHERE t.language_code = %s
             AND t.element_type IN ('post_post', 'post_page')
             AND p.post_status = 'publish'
             {$exclude_str}
             ORDER BY p.post_date DESC
             LIMIT 500",
            $lang_code
        ) );

        return array_map( 'intval', $results ?: array() );
    }

    /**
     * Get posts whose slug matches country-related URL patterns.
     *
     * @param string $country_key
     * @param array  $exclude_ids
     * @return array Post IDs.
     */
    private function get_posts_by_url_pattern( string $country_key, array $exclude_ids = array() ) {
        global $wpdb;

        $patterns = self::$url_patterns[ $country_key ] ?? array();
        if ( empty( $patterns ) ) return array();

        $like_clauses = array();
        foreach ( $patterns as $pat ) {
            $like_clauses[] = $wpdb->prepare( "p.post_name LIKE %s", '%' . $wpdb->esc_like( $pat ) . '%' );
        }

        $where_likes = '(' . implode( ' OR ', $like_clauses ) . ')';

        $exclude_str = '';
        if ( ! empty( $exclude_ids ) ) {
            $exclude_str = ' AND p.ID NOT IN (' . implode( ',', array_map( 'intval', $exclude_ids ) ) . ')';
        }

        $results = $wpdb->get_col(
            "SELECT p.ID FROM {$wpdb->posts} p
             WHERE {$where_likes}
             AND p.post_type IN ('post', 'page')
             AND p.post_status = 'publish'
             {$exclude_str}
             ORDER BY p.post_date DESC
             LIMIT 500"
        );

        return array_map( 'intval', $results ?: array() );
    }

    /* =================================================================
     *  MAIN SCANNER
     * ================================================================= */

    /**
     * Run the full site-wide inbound link scan.
     *
     * @param string $country_key  Single country or 'all'.
     * @param bool   $force_rescan Strip existing links and re-scan.
     * @return array Results.
     */
    public function run_scan( string $country_key = 'all', bool $force_rescan = false ) {

        @set_time_limit( 600 );
        @ini_set( 'memory_limit', '512M' );

        // Build link targets
        if ( $country_key === 'all' ) {
            $targets = $this->build_multi_country_targets();
        } else {
            $targets = $this->build_multi_country_targets( array( $country_key ) );
        }

        if ( empty( $targets ) ) {
            return array(
                'error'          => 'No link targets found for ' . $country_key . '. Need at least one published GeoBot city post or hub post.',
                'scanned'        => 0,
                'linked'         => 0,
                'links_injected' => 0,
                'targets_count'  => 0,
            );
        }

        $exclude_ids = $this->build_exclude_ids( $country_key );

        // ── Build prioritized scan queue ──
        // Tier 1: WPML language match
        // Tier 2: URL pattern match
        // Tier 3: Everything else
        $scan_queue = array();
        $queued_ids = array();

        $scan_countries = ( $country_key === 'all' )
            ? DBA_GeoBot_Registry::get_country_keys()
            : array( $country_key );

        // Tier 1: WPML language posts
        foreach ( $scan_countries as $ck ) {
            $config = DBA_GeoBot_Registry::get_country( $ck );
            if ( ! $config ) continue;

            $lang_posts = $this->get_posts_by_language( $config['wpml_code'], $exclude_ids );
            foreach ( $lang_posts as $pid ) {
                if ( ! isset( $queued_ids[ $pid ] ) ) {
                    $scan_queue[] = array( 'id' => $pid, 'tier' => 1 );
                    $queued_ids[ $pid ] = true;
                }
            }
        }

        // Tier 2: URL pattern match
        foreach ( $scan_countries as $ck ) {
            $url_posts = $this->get_posts_by_url_pattern( $ck, $exclude_ids );
            foreach ( $url_posts as $pid ) {
                if ( ! isset( $queued_ids[ $pid ] ) ) {
                    $scan_queue[] = array( 'id' => $pid, 'tier' => 2 );
                    $queued_ids[ $pid ] = true;
                }
            }
        }

        // Tier 3: Full site sweep (all remaining published posts/pages)
        $page = 1;
        $tier3_count = 0;

        while ( $tier3_count < self::MAX_SCAN_POSTS ) {
            $meta_query = array();
            if ( ! $force_rescan ) {
                $meta_query[] = array(
                    'key'     => '_dba_geo_inbound_linked',
                    'compare' => 'NOT EXISTS',
                );
            }

            $all_ids_to_exclude = array_merge( $exclude_ids, array_keys( $queued_ids ) );

            $remaining = get_posts( array(
                'post_type'      => array( 'post', 'page' ),
                'post_status'    => 'publish',
                'posts_per_page' => self::SCAN_BATCH_SIZE,
                'paged'          => $page,
                'post__not_in'   => $all_ids_to_exclude,
                'meta_query'     => $meta_query,
                'orderby'        => 'date',
                'order'          => 'DESC',
                'fields'         => 'ids',
            ) );

            if ( empty( $remaining ) ) break;

            foreach ( $remaining as $pid ) {
                if ( ! isset( $queued_ids[ $pid ] ) ) {
                    $scan_queue[] = array( 'id' => $pid, 'tier' => 3 );
                    $queued_ids[ $pid ] = true;
                    $tier3_count++;
                }
            }

            $page++;
            if ( $page > 100 ) break; // Safety
        }

        // ── Process queue ──
        $results = array(
            'scanned'        => 0,
            'linked'         => 0,
            'links_injected' => 0,
            'skipped'        => 0,
            'targets_count'  => count( $targets ),
            'tier1_scanned'  => 0,
            'tier2_scanned'  => 0,
            'tier3_scanned'  => 0,
            'details'        => array(),
        );

        foreach ( $scan_queue as $item ) {
            $pid  = $item['id'];
            $tier = $item['tier'];

            $results['scanned']++;
            $results[ 'tier' . $tier . '_scanned' ]++;

            $scan_result = $this->scan_single_post( $pid, $targets, $force_rescan );

            if ( $scan_result['links_added'] > 0 ) {
                $results['linked']++;
                $results['links_injected'] += $scan_result['links_added'];
                $results['details'][] = array(
                    'post_id' => $pid,
                    'title'   => get_the_title( $pid ),
                    'tier'    => $tier,
                    'links'   => $scan_result['cities_linked'],
                );
            } else {
                $results['skipped']++;
            }

            if ( $results['scanned'] >= self::MAX_SCAN_POSTS ) break;
        }

        error_log( "DBA GeoBot Inbound [{$country_key}]: Scan complete — "
            . "Targets: {$results['targets_count']}, "
            . "Scanned: {$results['scanned']} (T1:{$results['tier1_scanned']} T2:{$results['tier2_scanned']} T3:{$results['tier3_scanned']}), "
            . "Linked: {$results['linked']}, Links: {$results['links_injected']}" );

        return $results;
    }

    /**
     * Scan a single post and inject city/hub links.
     *
     * @param int   $post_id
     * @param array $targets     Link target index.
     * @param bool  $force_rescan
     * @return array
     */
    private function scan_single_post( int $post_id, array $targets, bool $force_rescan = false ) {

        $result = array( 'links_added' => 0, 'cities_linked' => array() );

        $content = get_post_field( 'post_content', $post_id );
        if ( empty( trim( $content ) ) ) return $result;

        // Strip existing inbound links if force rescan
        if ( $force_rescan ) {
            $content = $this->strip_inbound_links( $content );
        }

        $original_content = $content;
        $links_added = 0;

        foreach ( $targets as $data ) {
            if ( $links_added >= self::MAX_LINKS_PER_POST ) break;

            $phrase    = $data['commune'];
            $permalink = $data['permalink'];

            // Quick check: does this phrase even appear? (case-insensitive)
            if ( mb_stripos( $content, $phrase ) === false ) continue;

            // Already linked to this target?
            if ( strpos( $content, $permalink ) !== false ) continue;

            // Inject link at first safe occurrence
            $new_content = $this->link_first_safe_mention( $content, $phrase, $permalink );

            if ( $new_content !== $content ) {
                $content = $new_content;
                $links_added++;
                $result['cities_linked'][] = $phrase;
            }
        }

        if ( $content !== $original_content ) {
            $update = wp_update_post( array(
                'ID'           => $post_id,
                'post_content' => $content,
            ), true );

            if ( is_wp_error( $update ) ) {
                error_log( "DBA GeoBot Inbound: Failed to update post #{$post_id}: " . $update->get_error_message() );
                return $result;
            }

            update_post_meta( $post_id, '_dba_geo_inbound_linked', wp_json_encode( array(
                'date'   => current_time( 'Y-m-d H:i:s' ),
                'cities' => $result['cities_linked'],
                'count'  => $links_added,
            ) ) );

            $result['links_added'] = $links_added;
        }

        return $result;
    }

    /* =================================================================
     *  SAFE HTML LINK INJECTION
     * ================================================================= */

    /**
     * Link the first SAFE mention of a phrase in HTML content.
     *
     * "Safe" = NOT inside: <a>, <h1>–<h4>, <script>, <style>, comments, HTML attributes.
     * Whole-word match (unicode word boundaries).
     *
     * @param string $content    HTML content.
     * @param string $phrase     Text to find and link.
     * @param string $permalink  URL to link to.
     * @return string            Modified content (or unchanged if no safe match).
     */
    private function link_first_safe_mention( string $content, string $phrase, string $permalink ) {

        $escaped = preg_quote( $phrase, '/' );

        // Unicode-aware word boundary
        $pattern = '/(?<![\\w\\p{L}\\p{M}])(' . $escaped . ')(?![\\w\\p{L}\\p{M}])/iu';

        // Split into safe text vs unsafe zones (HTML tags, anchors, headings, scripts)
        $unsafe_pattern = '/'
            . '(<a\s[^>]*>.*?<\/a>'
            . '|<h[1-4][^>]*>.*?<\/h[1-4]>'
            . '|<script[\s>].*?<\/script>'
            . '|<style[\s>].*?<\/style>'
            . '|<!--.*?-->'
            . '|<[^>]+>'
            . ')/is';

        $parts = preg_split( $unsafe_pattern, $content, -1, PREG_SPLIT_DELIM_CAPTURE );

        $linked  = false;
        $rebuilt = '';

        foreach ( $parts as $part ) {
            if ( $linked ) {
                $rebuilt .= $part;
                continue;
            }

            // Unsafe zone or standalone HTML tag — skip
            if ( preg_match( '/^<(?:a\s|\/a>|h[1-4]|\/h[1-4]|script|\/script|style|\/style|!--)/i', $part )
                 || preg_match( '/^<[^>]+>$/s', $part ) ) {
                $rebuilt .= $part;
            } else {
                // Safe text — try to link
                $link_html = '<a href="' . esc_url( $permalink ) . '" title="${1}" class="dba-geo-inbound">${1}</a>';
                $new_part  = preg_replace( $pattern, $link_html, $part, 1, $count );

                if ( $count > 0 ) {
                    $rebuilt .= $new_part;
                    $linked = true;
                } else {
                    $rebuilt .= $part;
                }
            }
        }

        return $rebuilt;
    }

    /**
     * Strip all GeoBot inbound links (identified by class="dba-geo-inbound").
     * Preserves anchor text.
     */
    public function strip_inbound_links( string $content ) {
        return preg_replace(
            '/<a\s[^>]*class="[^"]*dba-geo-inbound[^"]*"[^>]*>(.*?)<\/a>/is',
            '$1',
            $content
        );
    }

    /* =================================================================
     *  STATS — Admin display
     * ================================================================= */

    public function get_scan_stats( string $country_key = 'all' ) {

        // Posts with inbound links
        $linked_posts = get_posts( array(
            'post_type'      => array( 'post', 'page' ),
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'meta_query'     => array(
                array( 'key' => '_dba_geo_inbound_linked', 'compare' => 'EXISTS' ),
            ),
            'fields' => 'ids',
        ) );

        $total_links = 0;
        $city_counts = array();

        foreach ( $linked_posts as $pid ) {
            $meta = json_decode( get_post_meta( $pid, '_dba_geo_inbound_linked', true ), true );
            if ( $meta ) {
                $total_links += ( $meta['count'] ?? 0 );
                foreach ( ( $meta['cities'] ?? array() ) as $city ) {
                    $city_counts[ $city ] = ( $city_counts[ $city ] ?? 0 ) + 1;
                }
            }
        }

        // Link targets count
        if ( $country_key === 'all' ) {
            $targets = $this->build_multi_country_targets();
        } else {
            $targets = $this->build_multi_country_targets( array( $country_key ) );
        }

        arsort( $city_counts );

        return array(
            'posts_with_inbound_links' => count( $linked_posts ),
            'total_inbound_links'      => $total_links,
            'link_targets'             => count( $targets ),
            'cities_with_inbound'      => count( $city_counts ),
            'top_linked_cities'        => array_slice( $city_counts, 0, 10, true ),
        );
    }

    /* =================================================================
     *  AJAX HANDLERS
     * ================================================================= */

    public function ajax_run_scan() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Unauthorized' );

        $country_key  = sanitize_key( $_POST['country_key'] ?? 'all' );
        $force_rescan = ! empty( $_POST['force_rescan'] );

        if ( $country_key !== 'all' && ! DBA_GeoBot_Registry::get_country( $country_key ) ) {
            wp_send_json_error( 'Invalid country key: ' . $country_key );
        }

        $results = $this->run_scan( $country_key, $force_rescan );
        wp_send_json_success( $results );
    }

    public function ajax_get_stats() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        $country_key = sanitize_key( $_POST['country_key'] ?? 'all' );
        wp_send_json_success( $this->get_scan_stats( $country_key ) );
    }

    public function ajax_strip_links() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Unauthorized' );

        @set_time_limit( 300 );

        $linked_posts = get_posts( array(
            'post_type'      => array( 'post', 'page' ),
            'post_status'    => 'any',
            'posts_per_page' => -1,
            'meta_query'     => array(
                array( 'key' => '_dba_geo_inbound_linked', 'compare' => 'EXISTS' ),
            ),
            'fields' => 'ids',
        ) );

        $stripped = 0;
        foreach ( $linked_posts as $pid ) {
            $content     = get_post_field( 'post_content', $pid );
            $new_content = $this->strip_inbound_links( $content );
            if ( $new_content !== $content ) {
                wp_update_post( array(
                    'ID'           => $pid,
                    'post_content' => $new_content,
                ) );
                $stripped++;
            }
            delete_post_meta( $pid, '_dba_geo_inbound_linked' );
        }

        wp_send_json_success( array(
            'posts_cleaned' => $stripped,
            'meta_removed'  => count( $linked_posts ),
        ) );
    }
}
