<?php
/**
 * DBA_LATAM_Scheduler
 *
 * Sat/Sun 07:00 CET cron + manual "Generate Now" with live progress.
 * Progress stored directly in wp_options (bypasses cache) for real-time polling.
 * NO loopback — generation runs in the foreground AJAX call.
 *
 * @since 2.11.0
 */
if ( ! defined( 'ABSPATH' ) ) exit;

class DBA_LATAM_Scheduler {

    const CRON_HOOK    = 'dba_latam_daily_run';
    const LOCK_KEY     = 'dba_latam_running';
    const LOCK_TTL     = 900;
    const PROGRESS_KEY = 'dba_latam_progress';
    const TOTAL_STEPS  = 12;

    private static $instance = null;
    private $is_manual = false;

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

    private function __construct() {
        add_action( self::CRON_HOOK,                       array( $this, 'run' ) );
        add_action( 'wp_ajax_dba_latam_progress',          array( $this, 'ajax_progress' ) );
        add_action( 'wp_ajax_dba_latam_generate_now',      array( $this, 'ajax_generate_now' ) );
        add_action( 'wp_ajax_dba_latam_reset_rotation',    array( $this, 'ajax_reset_rotation' ) );
        add_action( 'wp_ajax_dba_latam_cleanup_counter',   array( $this, 'ajax_cleanup_counter' ) );
        add_action( 'wp_ajax_dba_latam_scan_fix_links',    array( $this, 'ajax_scan_fix_links' ) );
    }

    /* ── Progress: writes directly to DB so concurrent polls see it ── */

    private function progress( $step, $label, $detail = '', $status = 'running' ) {
        global $wpdb;
        $val = maybe_serialize( array(
            'step' => $step, 'total' => self::TOTAL_STEPS,
            'percent' => round( ( $step / self::TOTAL_STEPS ) * 100 ),
            'label' => $label, 'detail' => $detail,
            'status' => $status, 'time' => current_time( 'H:i:s' ),
        ) );
        $k = '_site_transient_' . self::PROGRESS_KEY;
        $table = is_multisite() ? $wpdb->sitemeta : $wpdb->options;
        $col   = is_multisite() ? 'meta_key'      : 'option_name';
        $vcol  = is_multisite() ? 'meta_value'    : 'option_value';

        $exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$col} = %s", $k ) );
        if ( $exists ) {
            $wpdb->update( $table, array( $vcol => $val ), array( $col => $k ) );
        } else {
            $insert_data = array( $col => $k, $vcol => $val );
            if ( ! is_multisite() ) $insert_data['autoload'] = 'no';
            $wpdb->insert( $table, $insert_data );
        }
        wp_cache_delete( $k, is_multisite() ? 'site-options' : 'options' );
    }

    private function clear_progress() {
        global $wpdb;
        $table = is_multisite() ? $wpdb->sitemeta : $wpdb->options;
        $col   = is_multisite() ? 'meta_key'      : 'option_name';
        $wpdb->delete( $table, array( $col => '_site_transient_' . self::PROGRESS_KEY ) );
    }

    /* ── AJAX: poll progress (called every 1.5s by JS) ── */

    public function ajax_progress() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        global $wpdb;
        $table = is_multisite() ? $wpdb->sitemeta : $wpdb->options;
        $col   = is_multisite() ? 'meta_key'      : 'option_name';
        $vcol  = is_multisite() ? 'meta_value'    : 'option_value';
        $raw = $wpdb->get_var( $wpdb->prepare(
            "SELECT {$vcol} FROM {$table} WHERE {$col} = %s",
            '_site_transient_' . self::PROGRESS_KEY
        ) );
        wp_send_json_success( $raw ? maybe_unserialize( $raw ) : array( 'status' => 'idle' ) );
    }

    /* ── AJAX: Generate Now — runs synchronously, progress written to DB ── */

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

        @ignore_user_abort( true );
        @set_time_limit( 180 ); // Manual runs only — cron uses default limits

        // Close the session so poll requests aren't blocked
        if ( session_status() === PHP_SESSION_ACTIVE ) session_write_close();

        $this->clear_progress();
        $this->is_manual = true;
        $this->progress( 0, '🚀 Starting...', 'Manual generation triggered' );

        $this->run( 'manual' );

        // Return final progress as the response
        global $wpdb;
        $table = is_multisite() ? $wpdb->sitemeta : $wpdb->options;
        $col   = is_multisite() ? 'meta_key'      : 'option_name';
        $vcol  = is_multisite() ? 'meta_value'    : 'option_value';
        $raw = $wpdb->get_var( $wpdb->prepare(
            "SELECT {$vcol} FROM {$table} WHERE {$col} = %s",
            '_site_transient_' . self::PROGRESS_KEY
        ) );
        $final = $raw ? maybe_unserialize( $raw ) : array( 'status' => 'unknown' );
        wp_send_json_success( $final );
    }

    /* ── AJAX: Reset rotation ── */

    public function ajax_reset_rotation() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error();
        DBA_LATAM_Registry::reset_rotation();
        wp_send_json_success( array( 'message' => 'Rotation reset. Next: Brazil.' ) );
    }

    public function ajax_cleanup_counter() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error();
        global $wpdb;
        // Delete orphaned postmeta where the post no longer exists
        $deleted = $wpdb->query( "DELETE pm FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE p.ID IS NULL AND pm.meta_key IN ('_dba_latam_auto_created', '_dba_auto_created')" );
        // Count real posts
        $real = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE pm.meta_key = '_dba_latam_auto_created' AND p.post_status IN ('publish','draft','pending','future')" );
        wp_send_json_success( array( 'message' => 'Cleaned ' . intval($deleted) . ' orphan(s). Real posts: ' . intval($real), 'count' => intval($real) ) );
    }

    /**
     * AJAX: Scan & Fix Links — Re-scan ALL existing LATAM posts
     * and inject/update SEO-optimized internal links.
     */
    public function ajax_scan_fix_links() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error();
        global $wpdb;

        // Find all published LATAM posts
        $posts = $wpdb->get_results(
            "SELECT p.ID, p.post_title, p.post_content
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_dba_latam_auto_created'
             WHERE p.post_status = 'publish'
             ORDER BY p.post_date DESC
             LIMIT 50",
            ARRAY_A
        );

        if ( empty( $posts ) ) {
            wp_send_json_success( array( 'message' => 'No LATAM posts found to scan.', 'details' => '', 'log' => array() ) );
        }

        $log = array();
        $fixed = 0;
        $links_added = 0;

        foreach ( $posts as $post ) {
            $pid = intval( $post['ID'] );
            $content = $post['post_content'];
            $title = $post['post_title'];

            // Get LATAM metadata
            $country_key = get_post_meta( $pid, '_dba_latam_country', true );
            $city_name   = get_post_meta( $pid, '_dba_latam_city', true );
            $theme_key   = get_post_meta( $pid, '_dba_latam_theme', true );

            $countries = DBA_LATAM_Registry::get_countries();
            $country_label = isset( $countries[ $country_key ]['label'] ) ? $countries[ $country_key ]['label'] : $country_key;
            $theme_label = $theme_key;
            foreach ( self::get_themes() as $t ) {
                if ( $t['key'] === $theme_key ) { $theme_label = $t['label']; break; }
            }

            // Count existing links
            $existing_links = substr_count( $content, '<a href=' ) + substr_count( $content, '<a  href=' );
            // Don't re-scan if already well-linked (6+ internal links)
            if ( $existing_links >= 6 ) {
                $log[] = "⏭️ #{$pid} \"{$title}\" — already has {$existing_links} links, skipping";
                continue;
            }

            // ── SCAN content & inject SEO links ──
            $content_phrases = $this->scan_content_keyphrases( $content );
            $scanned_links = $this->seo_scan_for_links(
                $content_phrases,
                $country_label,
                $city_name ?: '',
                $theme_label,
                $theme_key ?: '',
                10
            );

            if ( ! empty( $scanned_links ) ) {
                $content_settings = get_option( 'dba_content_settings', array() );
                $pillar_url = ! empty( $content_settings['pillar_url'] ) ? $content_settings['pillar_url'] : 'https://www.damalion.com/blog/';

                $new_content = $this->seo_inject_links( $content, $pillar_url, $scanned_links, $content_phrases );

                // Strip heading labels like "Conclusion:", "Introduction:" etc.
                $new_content = preg_replace( '/(<h[23][^>]*>)\s*(Conclusion|Introduction|Summary|Overview|Final Thoughts|In Conclusion)\s*:\s*/i', '$1', $new_content );

                // Count links added
                $new_links = substr_count( $new_content, '<a href=' ) + substr_count( $new_content, '<a  href=' );
                $added = $new_links - $existing_links;

                if ( $added > 0 || $new_content !== $content ) {
                    // Update post content directly
                    $wpdb->update(
                        $wpdb->posts,
                        array( 'post_content' => $new_content ),
                        array( 'ID' => $pid ),
                        array( '%s' ),
                        array( '%d' )
                    );
                    update_post_meta( $pid, '_et_pb_old_content', $new_content );
                    clean_post_cache( $pid );

                    $links_added += max( 0, $added );
                    $fixed++;
                    $log[] = "✅ #{$pid} \"{$title}\" — added {$added} SEO links (total: {$new_links})";
                } else {
                    $log[] = "ℹ️ #{$pid} \"{$title}\" — no new anchors found ({$existing_links} existing)";
                }
            } else {
                $log[] = "⚠️ #{$pid} \"{$title}\" — 0 matching pages found";
            }
        }

        // Also flush caches
        $this->flush_all_caches( 0 );

        $msg = "Scanned " . count( $posts ) . " posts, fixed {$fixed}, added {$links_added} SEO links";
        wp_send_json_success( array( 'message' => $msg, 'details' => '', 'log' => $log ) );
    }

    /* ── Cron activation / deactivation ── */

    /** All possible day arguments that could be registered as cron events. */
    const ALL_DAYS = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' );

    public static function activate() {
        // Ensure 'weekly' schedule exists at activation time
        if ( ! isset( wp_get_schedules()['weekly'] ) ) {
            add_filter( 'cron_schedules', function( $s ) {
                if ( ! isset( $s['weekly'] ) ) {
                    $s['weekly'] = array( 'interval' => 604800, 'display' => 'Once Weekly' );
                }
                return $s;
            } );
        }

        $settings     = get_option( 'dba_latam_settings', array() );
        $posting_days = ! empty( $settings['posting_days'] ) ? (array) $settings['posting_days'] : array( 'saturday', 'sunday' );
        $cron_hour    = isset( $settings['cron_hour'] ) ? intval( $settings['cron_hour'] ) : 7;
        $cron_time    = sprintf( '%02d:00', max( 0, min( 23, $cron_hour ) ) );
        $tz           = new DateTimeZone( 'Europe/Luxembourg' );

        // Clear crons for days that are NO LONGER selected
        foreach ( self::ALL_DAYS as $d ) {
            if ( ! in_array( $d, $posting_days, true ) ) {
                wp_clear_scheduled_hook( self::CRON_HOOK, array( $d ) );
            }
        }

        // Schedule crons for each active day
        $log = array();
        foreach ( $posting_days as $day ) {
            $day = strtolower( trim( $day ) );
            if ( ! in_array( $day, self::ALL_DAYS, true ) ) continue;
            if ( ! wp_next_scheduled( self::CRON_HOOK, array( $day ) ) ) {
                wp_schedule_event( self::next_occ( ucfirst( $day ), $cron_time, $tz ), 'weekly', self::CRON_HOOK, array( $day ) );
            }
            $ts = wp_next_scheduled( self::CRON_HOOK, array( $day ) );
            $log[] = ucfirst( $day ) . '=' . ( $ts ? gmdate( 'Y-m-d H:i:s', $ts ) : 'FAILED' );
        }
        error_log( 'DBA LATAM activate: ' . implode( ' | ', $log ) );
    }

    /**
     * Clear ALL existing cron events and re-register from current settings.
     * Called by admin clean() after saving schedule changes.
     */
    public static function reschedule() {
        // Clear every possible day arg (handles day changes cleanly)
        foreach ( self::ALL_DAYS as $day ) {
            wp_clear_scheduled_hook( self::CRON_HOOK, array( $day ) );
        }
        // Also clear no-arg events (Conflict Center reschedules)
        wp_clear_scheduled_hook( self::CRON_HOOK );

        // Re-register from settings
        self::activate();
    }

    public static function deactivate() {
        // Clear every possible day arg (not just the current ones)
        foreach ( self::ALL_DAYS as $day ) {
            wp_clear_scheduled_hook( self::CRON_HOOK, array( $day ) );
        }
        wp_clear_scheduled_hook( self::CRON_HOOK );
        delete_transient( self::LOCK_KEY );
    }

    /* ══════════════════════════════════════════════════════════════
     *  MAIN RUN
     * ══════════════════════════════════════════════════════════════ */

    public function run( $day = '' ) {
        $settings = get_option( 'dba_latam_settings', array() );
        if ( ! $this->is_manual && empty( $settings['enabled'] ) ) return;

        // Validate day — prevent double-fire if WP-Cron delays cause both weekly events to trigger same day
        if ( ! $this->is_manual && ! empty( $day ) && $day !== 'manual' ) {
            try {
                $tz  = new DateTimeZone( 'Europe/Luxembourg' );
                $now = new DateTime( 'now', $tz );
                $actual_day = strtolower( $now->format( 'l' ) );
                if ( $actual_day !== $day ) {
                    error_log( "DBA LATAM: Cron for {$day} fired on {$actual_day} — skipping." );
                    return;
                }
                // Also verify this day is still in the active posting schedule
                // (guards against leftover cron events after a schedule change)
                $active_days = ! empty( $settings['posting_days'] ) ? (array) $settings['posting_days'] : array( 'saturday', 'sunday' );
                if ( ! in_array( $day, $active_days, true ) ) {
                    error_log( "DBA LATAM: Cron for {$day} fired but {$day} is no longer a posting day — skipping & unscheduling." );
                    wp_clear_scheduled_hook( self::CRON_HOOK, array( $day ) );
                    return;
                }
            } catch ( \Exception $e ) { /* continue */ }
        }

        if ( get_transient( self::LOCK_KEY ) ) {
            $this->progress( 0, '⛔ Blocked', 'Another run in progress. Wait 15 min.', 'error' );
            return;
        }
        set_transient( self::LOCK_KEY, true, self::LOCK_TTL );

        $this->progress( 1, '🔍 Step 1 — Collision guard...', 'Checking if other bots are running' );

        // v2.15.0: Global conflict lock (replaces wait_for_other_bots — now checks ALL modules)
        if ( class_exists( 'DBA_Conflict_Center' ) && ! DBA_Conflict_Center::acquire( 'latam', self::CRON_HOOK ) ) {
            $this->progress( 0, '⏳ Deferred', 'Another bot was running. Will retry in 10 min.', 'deferred' );
            error_log( 'DBA LATAM: Another module running — rescheduled +10min via Conflict Center' );
            delete_transient( self::LOCK_KEY );
            return;
        }

        try {
            $this->do_generate_batch();
        } catch ( \Throwable $e ) {
            error_log( 'DBA LATAM Fatal: ' . $e->getMessage() );
            $this->progress( 0, '❌ Fatal error', $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(), 'error' );
        }

        // v2.15.0: Release global lock
        if ( class_exists( 'DBA_Conflict_Center' ) ) DBA_Conflict_Center::release( 'latam' );

        delete_transient( self::LOCK_KEY );
    }

    /* ══════════════════════════════════════════════════════════════
     *  GENERATE — 10 steps with progress
     * ══════════════════════════════════════════════════════════════ */

    /**
     * Batch runner.
     *
     * CRON: generates one post per enabled LATAM country (daily batch).
     * MANUAL: generates only one post (next country), as before.
     */
    private function do_generate_batch() {
        if ( $this->is_manual ) {
            $this->do_generate_one();
            return;
        }

        $enabled = DBA_LATAM_Registry::get_enabled_countries();
        if ( empty( $enabled ) ) {
            $this->progress( 1, '⚠️ No countries', 'Enable at least one country.', 'error' );
            return;
        }

        $settings      = get_option( 'dba_latam_settings', array() );
        $posts_per_run = max( 1, absint( $settings['posts_per_run'] ?? 3 ) );
        $posts_per_run = min( $posts_per_run, count( $enabled ) ); // unique countries per batch

        // Cooldown: avoid posting the same country/city if it appeared recently.
        $cooldown_days = max( 0, absint( $settings['cooldown_days'] ?? 5 ) );
        $recent        = $cooldown_days > 0 ? $this->get_recent_latam_usage( $cooldown_days ) : array( 'countries' => array(), 'cities' => array() );
        $recent_countries = $recent['countries'];
        $recent_cities    = $recent['cities'];

        $used_countries = array(); // this batch
        $used_cities    = array(); // this batch

        for ( $i = 0; $i < $posts_per_run; $i++ ) {
            $this->progress( 1, '🌎 Batch — ' . ( $i + 1 ) . '/' . $posts_per_run, 'Selecting unique country + city (cooldown ' . $cooldown_days . 'd)' );

            $country_key = $this->select_next_country_for_batch( $enabled, $recent_countries, $used_countries );
            if ( ! $country_key ) {
                // Fallback: if cooldown blocks everything, ignore recent countries but keep batch uniqueness.
                $country_key = $this->select_next_country_for_batch( $enabled, array(), $used_countries );
            }
            if ( ! $country_key ) {
                $this->progress( 1, '⚠️ Rotation exhausted', 'No eligible countries found for this batch.', 'error' );
                break;
            }

            $used_countries[ $country_key ] = true;

            // Avoid duplicate cities inside the batch AND avoid cities used within cooldown window.
            $avoid_cities = $recent_cities + $used_cities;
            $this->do_generate_one( $country_key, $avoid_cities );

            // After generation, mark the chosen city (if any) as used in this batch.
            $last_city = get_option( 'dba_latam_last_city_generated', '' );
            if ( $last_city ) {
                $used_cities[ mb_strtolower( $last_city ) ] = true;
            }
        }

        $this->progress( 12, '✅ Batch complete', 'Generated up to ' . $posts_per_run . ' post(s).', 'done' );
    }

    /**
     * Generate a single post.
     *
     * @param string|null $forced_country_key Country key to force (cron batch).
     * @param array       $avoid_cities       Map of lowercase city names to true.
     */
    private function do_generate_one( $forced_country_key = null, $avoid_cities = array() ) {
        $settings = get_option( 'dba_latam_settings', array() );

        // ── 1. Country ──
        $this->progress( 1, '🌎 Step 1 — Selecting country...', '' );

        $country_key = $forced_country_key ? sanitize_key( $forced_country_key ) : DBA_LATAM_Registry::get_next_country();

        if ( ! $country_key && $this->is_manual ) {
            $settings['enabled_countries'] = array( 'brazil' );
            update_option( 'dba_latam_settings', $settings );
            $br = DBA_LATAM_Registry::get_country( 'brazil' );
            if ( $br ) {
                $cs = get_option( $br['settings_option'], array() );
                $cs['enabled'] = true;
                update_option( $br['settings_option'], $cs );
            }
            DBA_LATAM_Registry::reset_rotation();
            $country_key = DBA_LATAM_Registry::get_next_country();
        }

        if ( ! $country_key ) {
            $this->progress( 1, '⚠️ No countries', 'Enable at least one country.', 'error' );
            return;
        }

        $config = DBA_LATAM_Registry::get_country( $country_key );
        if ( ! $config ) { $this->progress( 1, '❌ Config missing', $country_key, 'error' ); return; }

        $this->progress( 1, '🌎 Step 1 — ' . $config['label'], '✓ Country selected' );

        // ── 2. City ──
        $this->progress( 2, '🏙️ Step 2 — Selecting city...', $config['label'] );
        $city = $this->get_next_city_avoiding( $country_key, $config, $avoid_cities );
        if ( ! $city ) { $this->progress( 2, '⚠️ No cities', $config['label'], 'error' ); return; }
        $this->progress( 2, '🏙️ Step 2 — ' . $city['name'], '✓ ' . $city['region'] );

        // Store for batch-level "unique city" tracking
        update_option( 'dba_latam_last_city_generated', $city['name'], false );

        // ── 3. Theme ──
        $theme = $this->get_next_theme( $country_key );
        $this->progress( 3, '📋 Step 3 — ' . $theme['label'], '✓ Theme selected' );

        // ── 4. Category ──
        $this->progress( 4, '📁 Step 4 — Category...', '' );
        $category_id = ! empty( $settings['category_id'] ) ? intval( $settings['category_id'] ) : 0;
        if ( $category_id === 0 ) {
            $cat = get_term_by( 'slug', 'latam-investment', 'category' );
            if ( $cat ) {
                $category_id = $cat->term_id;
            } else {
                $r = wp_insert_term( 'LATAM Investment', 'category', array( 'slug' => 'latam-investment' ) );
                if ( ! is_wp_error( $r ) ) {
                    $category_id = $r['term_id'];
                    $settings['category_id'] = $category_id;
                    update_option( 'dba_latam_settings', $settings );
                }
            }
        }
        // Also assign to mainstream categories so posts appear on HOMEPAGE
        $homepage_cats = $this->get_homepage_categories( $theme['key'], $country_key, $city['name'] );
        $all_category_ids = $category_id > 0 ? array( $category_id ) : array();
        foreach ( $homepage_cats as $hc_id ) {
            if ( $hc_id > 0 && ! in_array( $hc_id, $all_category_ids, true ) ) {
                $all_category_ids[] = $hc_id;
            }
        }
        $cat_name = $category_id > 0 ? get_cat_name( $category_id ) : 'Uncategorized';
        $extra = count( $all_category_ids ) > 1 ? ' +' . ( count( $all_category_ids ) - 1 ) . ' homepage cats' : '';
        $this->progress( 4, '📁 Step 4 — ' . $cat_name . $extra, '✓ Category IDs: ' . implode( ',', $all_category_ids ) );

        // ── 5. News research ──
        $this->progress( 5, '🌐 Step 5 — Researching news...', 'Google News RSS for ' . $city['name'] . ' ' . $config['label'] );
        $news = $this->fetch_news( $config['label'], $city['name'], $theme['label'] );
        $news_count = $news ? substr_count( $news, '• ' ) : 0;
        $this->progress( 5, '🌐 Step 5 — ' . ( $news_count ? $news_count . ' news items' : 'No recent news' ),
            $news_count ? '✓ Fresh context for GPT' : '⚠️ Will use general knowledge' );

        // ── 6. Generate article via OpenAI ──
        $this->progress( 6, '🤖 Step 6 — Generating article...', 'Calling OpenAI API...' );

        $api_settings = get_option( 'dba_settings', array() );
        $api_key = ! empty( $api_settings['openai_api_key'] ) ? $api_settings['openai_api_key'] : '';
        $model   = ! empty( $api_settings['openai_model'] )   ? $api_settings['openai_model']   : 'gpt-4o-mini';

        if ( empty( $api_key ) ) {
            $this->progress( 6, '❌ No API key', 'Set OpenAI key in Core Settings.', 'error' );
            return;
        }

        $word_min = ! empty( $settings['word_count_min'] ) ? intval( $settings['word_count_min'] ) : $config['word_count_min'];
        $word_max = ! empty( $settings['word_count_max'] ) ? intval( $settings['word_count_max'] ) : $config['word_count_max'];

        // Subject key for tracking (unique per country+city+theme)
        $subject_key = $config['label'] . ' — ' . $city['name'] . ' — ' . $theme['label'];

        $system = 'You are a senior business journalist writing for Damalion, an international consulting firm. '
            . 'Write in English. Audience: international investors, entrepreneurs, family offices. '
            . "\n\n"
            . 'CONTENT QUALITY RULES (strict): '
            . "\n"
            . '1. ZERO SOURCE MENTIONS: Never name any source, organization, newspaper, agency, or institution. '
            . '   Never write "according to", "reported by", "as per", "data from", "published by", "study by". '
            . '   State facts directly: "GDP grew 3.2% in 2025" — never attribute to Reuters, World Bank, IMF, etc. '
            . "\n"
            . '2. ANTI-KEYWORD-STUFFING (critical for SEO): '
            . '   - Mention the city name maximum 3 times in the entire article (once in intro, once mid-body, once in conclusion). '
            . '   - Mention the country name maximum 4 times total. '
            . '   - Use pronouns ("the city", "this market", "the country", "its economy") instead of repeating proper nouns. '
            . '   - Never repeat any keyword or phrase more than 3 times in the whole article. '
            . '   - Vary your vocabulary: use synonyms, related terms, and different phrasing each time. '
            . '   - The article must read like it was written by a human journalist, not an AI. '
            . "\n"
            . '3. UNIQUE STRUCTURE — do NOT follow a template: '
            . '   - Vary the number of H2 sections (2-4, not always the same). '
            . '   - Vary paragraph lengths: mix short punchy paragraphs with longer analytical ones. '
            . '   - Include at least one specific, non-obvious insight that adds genuine value (a concrete number, a recent legal change, a practical tip). '
            . '   - Do NOT use generic filler paragraphs. Every paragraph must contain a specific fact, figure, or actionable insight. '
            . '   - Open with a compelling hook, not "City X is a leading hub for..." '
            . '   - NEVER prefix any heading with "Conclusion:", "Introduction:", "Summary:", or similar labels. Just write the heading directly. '
            . "\n"
            . '4. Write authoritatively with specific data: tax rates, law names, registration timelines, costs — but never cite a source. '
            . "\n"
            . '5. PROMOTE DAMALION SERVICES NATURALLY throughout the article (this is critical): '
            . '   Damalion is the consulting firm publishing this article. The reader is a potential client. '
            . '   When describing complex processes (company registration, account opening, tax structuring, compliance, visa applications, etc.), '
            . '   naturally weave in that Damalion facilitates, manages, or handles these tasks on behalf of clients. '
            . '   Examples of natural integration: '
            . '   - "Damalion facilitates the entire incorporation process, from entity selection to registration with local authorities." '
            . '   - "Through Damalion\'s local network, foreign investors can navigate KYC and due diligence requirements efficiently." '
            . '   - "Damalion\'s team coordinates document preparation, apostille certification, and liaison with banking institutions." '
            . '   - "Investors working with Damalion benefit from streamlined access to compliant corporate structures." '
            . '   DO: mention Damalion exactly 2 times in the body (not counting the CTA), always in the context of facilitating a specific service. '
            . '   DO NOT: make every sentence about Damalion. The article should be informative first, with Damalion positioned as the expert partner who executes. ';

        // ── Banned Words: read from main plugin settings ──
        $main_settings = get_option( 'dba_settings', array() );
        $banned_words  = ! empty( $main_settings['banned_words'] ) ? array_filter( array_map( 'trim', (array) $main_settings['banned_words'] ) ) : array();
        if ( ! empty( $banned_words ) ) {
            $system .= "\n" . '6. BANNED VOCABULARY — These words/phrases must NEVER appear anywhere in your output '
                . '(title, meta_description, focus_keyphrase, or body): '
                . implode( ', ', $banned_words ) . '. Use synonyms instead. ';
        }

        $system .= "\n\n" . 'OUTPUT FORMAT: Begin with these 3 XML tags, then HTML body: '
            . '<title>SEO TITLE RULES: '
            . 'The title MUST read like something a real human would type into Google. '
            . 'People do NOT search "Regulatory Environment & Compliance in São Paulo, Brazil: 2026 Outlook". '
            . 'People DO search "how to open a business in Brazil", "Brazil tax rates for foreign investors", "best cities to invest in Brazil 2026". '
            . 'Write the title as a natural search query or answer to one. '
            . 'Good examples: "How to Open a Corporate Bank Account in Fortaleza, Brazil", '
            . '"Brazil Free Trade Zones: What Foreign Investors Need to Know", '
            . '"Starting a Business in São Paulo — Costs, Steps, and Timelines", '
            . '"Why Medellín Is Attracting Foreign Investment in 2026". '
            . 'Bad examples: "Regulatory Environment & Compliance in São Paulo, Brazil: 2026 Outlook", '
            . '"Foreign Direct Investment Opportunities in Bogotá, Colombia: Strategic Analysis", '
            . '"Tax Structuring & Treaty Advantages in Lima, Peru: Comprehensive Overview". '
            . 'Keep it under 60 characters if possible. No colons followed by generic labels like "Overview", "Outlook", "Analysis".</title> '
            . '<meta_description>Max 135 chars for Yoast SEO — write as a compelling answer snippet</meta_description> '
            . '<focus_keyphrase>2-4 words matching a real Google search</focus_keyphrase> '
            . 'Then HTML body using <h2>, <h3>, <p>, <ul>. No <h1>, no markdown, no title repeated in body.';

        $user_prompt = $this->build_prompt( $config, $city, $theme, $word_min, $word_max, $news );

        // ── SEO Sniper: inject competitive intelligence ──
        // Priority: live transient (Sniper-triggered) → stored analysis (every cron run)
        if ( class_exists( 'DBA_SEO_Sniper_Router' ) ) {
            $sniper_brief = DBA_SEO_Sniper_Router::get_brief_for_prompt( $country_key, 'latam' );
            if ( $sniper_brief ) {
                $user_prompt .= "\n\n" . $sniper_brief;
            }
        }

        // ── TIS: inject strategic content gap priorities ──
        if ( class_exists( 'DBA_TIS_Recommender' ) ) {
            $tis_context = DBA_TIS_Recommender::get_generator_context( 'latam' );
            if ( $tis_context ) {
                $system .= "\n\n" . $tis_context;
            }
        }

        // ── PIPELINE: inject keyword focus from validated brief ──
        if ( class_exists( 'DBA_TIS_Pipeline' ) ) {
            $latam_brief = DBA_TIS_Pipeline::consume_brief( 'latam' );
            if ( $latam_brief ) {
                $user_prompt .= DBA_TIS_Pipeline::build_brief_context( $latam_brief );
                $this->_pipeline_brief = $latam_brief;
                error_log( 'LATAM: 🔬 Pipeline keyword focus injected' );
            }
        }

        $api_resp = wp_remote_post( 'https://api.openai.com/v1/chat/completions', array(
            'timeout' => 120,
            'headers' => array(
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ),
            'body' => wp_json_encode( array(
                'model'       => $model,
                'messages'    => array(
                    array( 'role' => 'system', 'content' => $system ),
                    array( 'role' => 'user',   'content' => $user_prompt ),
                ),
                'temperature' => 0.7,
                'max_tokens'  => 4000,
            ) ),
        ) );

        if ( is_wp_error( $api_resp ) ) {
            $this->progress( 6, '❌ Connection error', $api_resp->get_error_message(), 'error' );
            return;
        }

        $http_code = wp_remote_retrieve_response_code( $api_resp );
        if ( $http_code !== 200 ) {
            $err = wp_remote_retrieve_body( $api_resp );
            $this->progress( 6, '❌ API error (HTTP ' . $http_code . ')', mb_substr( $err, 0, 250 ), 'error' );
            return;
        }

        $result  = json_decode( wp_remote_retrieve_body( $api_resp ), true );
        $content = isset( $result['choices'][0]['message']['content'] ) ? $result['choices'][0]['message']['content'] : '';

        if ( empty( $content ) ) {
            $this->progress( 6, '❌ Empty response', 'GPT returned nothing.', 'error' );
            return;
        }

        $wc = str_word_count( wp_strip_all_tags( $content ) );
        $this->progress( 6, '🤖 Step 6 — ~' . $wc . ' words generated', '✓ ' . ( $news ? 'With news context' : 'General knowledge' ) );

        // ── 7. Parse SEO ──
        $this->progress( 7, '🔍 Step 7 — Extracting SEO...', '' );

        $title = '';
        if ( preg_match( '/<title>(.*?)<\/title>/si', $content, $m ) ) {
            $title = wp_strip_all_tags( $m[1] ); $content = str_replace( $m[0], '', $content );
        }
        if ( ! $title ) {
            // Search-friendly fallback titles by theme
            $fallback_titles = array(
                'business_formation'  => 'How to Start a Business in ' . $city['name'] . ', ' . $config['label'],
                'fdi_opportunities'   => 'Foreign Investment in ' . $city['name'] . ' — What You Need to Know',
                'real_estate'         => 'Buying Property in ' . $city['name'] . ' as a Foreign Investor',
                'tax_structuring'     => $config['label'] . ' Tax Rates and Incentives for Foreign Investors',
                'banking'             => 'How to Open a Corporate Bank Account in ' . $city['name'] . ', ' . $config['label'],
                'regulatory'          => 'Business Rules for Foreign Investors in ' . $city['name'] . ', ' . $config['label'],
                'residency_visa'      => 'Investor Visa for ' . $config['label'] . ' — Requirements and Process',
                'free_trade_zones'    => 'Free Trade Zones in ' . $config['label'] . ' for Foreign Businesses',
                'sector_opportunities'=> 'Best Sectors to Invest in ' . $city['name'] . ', ' . $config['label'],
            );
            $title = isset( $fallback_titles[ $theme['key'] ] ) ? $fallback_titles[ $theme['key'] ] : 'Investing in ' . $city['name'] . ', ' . $config['label'];
        }

        // ── 7a. DUPLICATE TITLE CHECK — avoid Google penalty for identical titles ──
        global $wpdb;
        $existing = $wpdb->get_var( $wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts}
             WHERE post_title = %s AND post_status IN ('publish','draft','pending','private')
             LIMIT 1",
            $title
        ) );
        if ( $existing ) {
            // Title already exists — make it unique
            $original_title = $title;

            // Strategy 1: Append year + month for freshness
            $date_suffix = date( 'F Y' ); // e.g. "February 2026"
            $title_v2 = $title . ' — ' . $date_suffix;

            // Strategy 2: Prepend city if not already in title
            if ( stripos( $title, $city['name'] ) === false ) {
                $title_v2 = $city['name'] . ': ' . $title;
            }

            // Check if v2 also exists
            $exists_v2 = $wpdb->get_var( $wpdb->prepare(
                "SELECT ID FROM {$wpdb->posts}
                 WHERE post_title = %s AND post_status IN ('publish','draft','pending','private')
                 LIMIT 1",
                $title_v2
            ) );
            if ( $exists_v2 ) {
                // Strategy 3: Add unique suffix
                $title = $title . ' — ' . $city['name'] . ' ' . date( 'Y' ) . ' Guide';
            } else {
                $title = $title_v2;
            }

            error_log( 'DBA LATAM: duplicate title "' . $original_title . '" (post #' . $existing . ') → renamed to "' . $title . '"' );
        }

        $meta = '';
        if ( preg_match( '/<meta_description>(.*?)<\/meta_description>/si', $content, $m ) ) {
            $meta = wp_strip_all_tags( trim( $m[1] ) ); $content = str_replace( $m[0], '', $content );
        }
        if ( mb_strlen( $meta ) > 135 ) $meta = mb_substr( $meta, 0, 132 ) . '...';
        if ( ! $meta ) $meta = mb_substr( 'Discover ' . $theme['label'] . ' in ' . $city['name'] . ', ' . $config['label'] . '. Insights for investors.', 0, 135 );

        $kw = '';
        if ( preg_match( '/<focus_keyphrase>(.*?)<\/focus_keyphrase>/si', $content, $m ) ) {
            $kw = wp_strip_all_tags( trim( $m[1] ) ); $content = str_replace( $m[0], '', $content );
        }
        if ( ! $kw ) $kw = implode( ' ', array_slice( explode( ' ', strtolower( $theme['label'] . ' ' . $city['name'] ) ), 0, 4 ) );

        $content = trim( $content ) . "\n\n" . $this->build_cta( $config, $settings );

        $this->progress( 7, '🔍 Step 7 — KW: "' . $kw . '"', '✓ Desc: ' . mb_strlen( $meta ) . ' chars' );

        // ── 7b. Banned words enforcement ──
        if ( ! empty( $banned_words ) ) {
            $bw_replaced = 0;
            foreach ( $banned_words as $bw ) {
                $bw = trim( $bw );
                if ( strlen( $bw ) < 2 ) continue;
                $pattern = '/\b' . preg_quote( $bw, '/' ) . '\b/i';
                // Replace in title
                if ( preg_match( $pattern, $title ) ) {
                    $title = preg_replace( $pattern, '', $title );
                    $title = preg_replace( '/\s{2,}/', ' ', trim( $title ) );
                    $title = preg_replace( '/^[\s:—–-]+|[\s:—–-]+$/', '', $title );
                    $bw_replaced++;
                }
                // Replace in meta
                if ( preg_match( $pattern, $meta ) ) {
                    $meta = preg_replace( $pattern, '', $meta );
                    $meta = preg_replace( '/\s{2,}/', ' ', trim( $meta ) );
                    $bw_replaced++;
                }
                // Replace in focus keyphrase
                if ( preg_match( $pattern, $kw ) ) {
                    $kw = preg_replace( $pattern, '', $kw );
                    $kw = preg_replace( '/\s{2,}/', ' ', trim( $kw ) );
                    $bw_replaced++;
                }
                // Replace in body content (only in text nodes, not HTML tags/attributes)
                $parts = preg_split( '/(<[^>]+>)/s', $content, -1, PREG_SPLIT_DELIM_CAPTURE );
                if ( $parts !== false ) {
                    for ( $pi = 0; $pi < count( $parts ); $pi++ ) {
                        if ( $pi % 2 === 0 && preg_match( $pattern, $parts[$pi] ) ) { // text node
                            $parts[$pi] = preg_replace( $pattern, '', $parts[$pi] );
                            $parts[$pi] = preg_replace( '/\s{2,}/', ' ', $parts[$pi] );
                            $bw_replaced++;
                        }
                    }
                    $content = implode( '', $parts );
                }
            }
            if ( $bw_replaced > 0 ) {
                error_log( 'DBA LATAM: removed ' . $bw_replaced . ' banned word occurrences' );
            }
        }

        // ── 7c. Strip heading labels like "Conclusion:", "Introduction:", "Summary:" ──
        $content = preg_replace( '/(<h[23][^>]*>)\s*(Conclusion|Introduction|Summary|Overview|Final Thoughts|In Conclusion)\s*:\s*/i', '$1', $content );

        // ── 7c. Keyword density check & reduction (Google spam compliance) ──
        $plain = strtolower( wp_strip_all_tags( $content ) );
        $total_wc = str_word_count( $plain );
        if ( $total_wc > 50 ) {
            $city_name_lower  = strtolower( $city['name'] );
            $country_lower    = strtolower( $config['label'] );
            $max_city    = 4;   // max city name occurrences
            $max_country = 5;   // max country name occurrences

            // City name density reduction
            $city_count = substr_count( $plain, $city_name_lower );
            if ( $city_count > $max_city ) {
                $replacements = array( 'the city', 'this market', 'the local market', 'the metropolitan area', 'the region' );
                $parts = preg_split( '/(<[^>]+>)/s', $content, -1, PREG_SPLIT_DELIM_CAPTURE );
                $reduced = 0;
                $kept = 0;
                if ( $parts !== false ) {
                    for ( $pi = 0; $pi < count( $parts ); $pi++ ) {
                        if ( $pi % 2 === 0 ) { // text node only
                            $parts[$pi] = preg_replace_callback(
                                '/\b(' . preg_quote( $city['name'], '/' ) . ')\b/iu',
                                function( $m ) use ( &$kept, &$reduced, $max_city, $replacements ) {
                                    $kept++;
                                    // Keep the first 3 occurrences, replace the rest
                                    if ( $kept <= 3 ) return $m[0];
                                    $reduced++;
                                    return $replacements[ $reduced % count( $replacements ) ];
                                },
                                $parts[$pi]
                            );
                        }
                    }
                    if ( $reduced > 0 ) {
                        $content = implode( '', $parts );
                        error_log( 'DBA LATAM density: reduced city name "' . $city['name'] . '" by ' . $reduced . ' occurrences' );
                    }
                }
            }

            // Country name density reduction
            $country_count = substr_count( $plain, $country_lower );
            if ( $country_count > $max_country ) {
                $c_replacements = array( 'the country', 'this economy', 'the nation', 'the domestic market' );
                $parts = preg_split( '/(<[^>]+>)/s', $content, -1, PREG_SPLIT_DELIM_CAPTURE );
                $reduced = 0;
                $kept = 0;
                if ( $parts !== false ) {
                    for ( $pi = 0; $pi < count( $parts ); $pi++ ) {
                        if ( $pi % 2 === 0 ) {
                            $parts[$pi] = preg_replace_callback(
                                '/\b(' . preg_quote( $config['label'], '/' ) . ')\b/iu',
                                function( $m ) use ( &$kept, &$reduced, $max_country, $c_replacements ) {
                                    $kept++;
                                    if ( $kept <= 4 ) return $m[0];
                                    $reduced++;
                                    return $c_replacements[ $reduced % count( $c_replacements ) ];
                                },
                                $parts[$pi]
                            );
                        }
                    }
                    if ( $reduced > 0 ) {
                        $content = implode( '', $parts );
                        error_log( 'DBA LATAM density: reduced country name "' . $config['label'] . '" by ' . $reduced . ' occurrences' );
                    }
                }
            }

            // Log final density for monitoring
            $final_plain = strtolower( wp_strip_all_tags( $content ) );
            $final_city  = substr_count( $final_plain, $city_name_lower );
            $final_country = substr_count( $final_plain, $country_lower );
            $this->progress( 7, '🔍 Step 7 — Density OK',
                '✓ ' . $city['name'] . ': ' . $final_city . 'x, ' . $config['label'] . ': ' . $final_country . 'x (in ' . $total_wc . ' words)' );
        }

        // ── 8. Image ──
        $this->progress( 8, '🖼️ Step 8 — Generating image...', 'DALL-E: ' . $city['name'] );
        $media_id = 0;
        if ( class_exists( 'DBA_Image_Generator' ) ) {
            try {
                // Varied image prompts to avoid identical-looking images
                $img_styles = array(
                    'Aerial photograph of %s, %s. Modern skyline with dramatic natural lighting. Sharp focus, vibrant colors. Editorial quality. No text, no logos, no watermarks, no people faces.',
                    'Cinematic wide-angle photograph of the business district in %s, %s. Architectural detail, warm golden hour light. Professional magazine quality. No text, no logos, no watermarks, no faces.',
                    'High-resolution photograph of %s, %s. Mix of modern architecture and local character. Natural daylight, clean composition. Professional quality. No text, no logos, no watermarks, no faces.',
                    'Professional editorial photograph: panoramic view of %s, %s. Urban landscape with depth. Rich colors, crisp detail. Magazine cover quality. No text, no logos, no watermarks, no faces.',
                );
                $img_prompt = sprintf( $img_styles[ array_rand( $img_styles ) ], $city['name'], $config['label'] );
                $img_filename = sanitize_file_name( 'latam-' . $country_key . '-' . sanitize_title( $city['name'] ) . '.jpg' );

                // SEO-friendly alt text derived from article title
                $img_alt = ! empty( $title ) ? $title : $city['name'] . ', ' . $config['label'];

                $media_id = DBA_Image_Generator::get_instance()->generate_and_upload( array(
                    'prompt'      => $img_prompt,
                    'title'       => $title,
                    'alt'         => $img_alt,
                    'caption'     => $city['name'] . ', ' . $config['label'],
                    'description' => $title,
                    'filename'    => $img_filename,
                ), 0 );

                if ( is_wp_error( $media_id ) ) {
                    error_log( 'DBA LATAM image: ' . $media_id->get_error_message() );
                    $media_id = 0;
                }
            } catch ( \Throwable $e ) {
                error_log( 'DBA LATAM image error: ' . $e->getMessage() );
                $media_id = 0;
            }
        }
        $this->progress( 8, '🖼️ Step 8 — ' . ( $media_id ? 'Image #' . $media_id : 'Image skipped' ), $media_id ? '✓' : '⚠️ Continuing' );

        // ── 8.5 SEO-OPTIMIZED INTERNAL LINKING (Content Scanner) ──
        // ── Link Sanitizer: strip/replace hallucinated damalion.com links ──
        if ( class_exists( 'DBA_Link_Sanitizer' ) && ! empty( $content ) ) {
            $content = DBA_Link_Sanitizer::sanitize( $content, $config['wpml_code'] ?? 'en' );
        }

        $this->progress( 8, '🔗 Step 8.5 — SEO link scanner...', 'Analyzing content + scanning pages' );
        $linked = false;

        try {
            $content_settings = get_option( 'dba_content_settings', array() );
            $pillar_url = ! empty( $content_settings['pillar_url'] ) ? $content_settings['pillar_url'] : 'https://www.damalion.com/blog/';

            // ── A. SCAN ARTICLE CONTENT — extract key phrases for matching ──
            $content_phrases = $this->scan_content_keyphrases( $content );
            error_log( 'DBA LATAM SEO scanner: extracted ' . count( $content_phrases ) . ' key phrases from article' );

            // ── B. FIND MATCHING PAGES — use content phrases + theme context ──
            $scanned_links = $this->seo_scan_for_links(
                $content_phrases,
                $config['label'],   // country
                $city['name'],      // city
                $theme['label'],    // theme
                $theme['key'],
                10                  // max results
            );
            error_log( 'DBA LATAM SEO scanner: found ' . count( $scanned_links ) . ' matching pages' );

            // ── C. SUPPLEMENT with sitemap registry if available ──
            $registry_links = array();
            if ( class_exists( 'DBA_Sitemap' ) ) {
                $sitemap = DBA_Sitemap::get_instance();
                $registry = array();

                if ( method_exists( $sitemap, 'get_registry' ) ) {
                    $registry = $sitemap->get_registry();
                }
                if ( empty( $registry ) && method_exists( $sitemap, 'refresh_from_sitemap' ) ) {
                    $sitemap->refresh_from_sitemap();
                    if ( method_exists( $sitemap, 'get_registry' ) ) {
                        $registry = $sitemap->get_registry();
                    }
                }
                if ( method_exists( $sitemap, 'select_contextual_links' ) ) {
                    $registry_links = $sitemap->select_contextual_links( array(), $theme['label'], $subject_key, 6, 'en' );
                }
                error_log( 'DBA LATAM SEO scanner: registry returned ' . count( $registry_links ) . ' supplementary links' );
            }

            // ── D. MERGE & DEDUPLICATE — scanned links take priority ──
            $all_links = $scanned_links;
            $used_urls = array_map( function( $l ) { return $l['url']; }, $all_links );
            foreach ( $registry_links as $rl ) {
                $rurl = is_array( $rl ) ? ( $rl['url'] ?? '' ) : '';
                if ( $rurl && ! in_array( $rurl, $used_urls, true ) && count( $all_links ) < 10 ) {
                    $all_links[] = $rl;
                    $used_urls[] = $rurl;
                }
            }

            // ── E. INJECT LINKS — using content-aware anchor text ──
            if ( ! empty( $all_links ) ) {
                $content = $this->seo_inject_links( $content, $pillar_url, $all_links, $content_phrases );
                $linked = true;
            }

            // ── F. Also run DBA_Auto_Linker::link_country_names if available ──
            if ( class_exists( 'DBA_Auto_Linker' ) ) {
                $auto_linker = DBA_Auto_Linker::get_instance();
                if ( method_exists( $auto_linker, 'link_country_names' ) ) {
                    $content = $auto_linker->link_country_names( $content );
                }
            }

            $this->progress( 8, '🔗 SEO links injected', '✓ ' . count( $all_links ) . ' links (scanned: ' . count( $scanned_links ) . ', registry: ' . count( $registry_links ) . ')' );

        } catch ( \Throwable $e ) {
            error_log( 'DBA LATAM SEO scanner error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() );
            // Last resort: try basic DB fallback
            try {
                $fallback = $this->fallback_internal_links( $config['label'], $city['name'], $theme['label'], 6 );
                if ( ! empty( $fallback ) ) {
                    $content = $this->seo_inject_links( $content, 'https://www.damalion.com/blog/', $fallback, array() );
                    $linked = true;
                    $this->progress( 8, '🔗 Links (fallback)', '✓ ' . count( $fallback ) . ' links from DB' );
                } else {
                    $this->progress( 8, '🔗 Links error', '⚠️ ' . $e->getMessage() );
                }
            } catch ( \Throwable $e2 ) {
                $this->progress( 8, '🔗 Links failed', '⚠️ ' . $e->getMessage() );
            }
        }

        // ── 9. Create post (matching core post creator pattern) ──
        $this->progress( 9, '💾 Step 9 — Creating post...', '' );

        $author   = ! empty( $settings['author_id'] )  ? intval( $settings['author_id'] )  : 1;
        $pstatus  = ! empty( $settings['post_status'] ) ? $settings['post_status']          : 'publish';

        // Step A: DISABLE all core dedup hooks (nuclear approach)
        global $wp_filter;
        $saved_hooks = array();
        foreach ( array( 'wp_insert_post', 'added_post_meta', 'wp_insert_post_data' ) as $tag ) {
            if ( isset( $wp_filter[ $tag ] ) ) {
                $saved_hooks[ $tag ] = clone $wp_filter[ $tag ];
            }
        }
        remove_all_actions( 'wp_insert_post' );
        remove_all_actions( 'added_post_meta' );
        remove_all_filters( 'wp_insert_post_data' );

        // Step B: Create as DRAFT first (same as core post creator)
        $post_data = array(
            'post_title'    => sanitize_text_field( $title ),
            'post_content'  => wp_kses_post( $content ),
            'post_status'   => 'draft',
            'post_type'     => 'post',
            'post_author'   => $author,
            'post_category' => ! empty( $all_category_ids ) ? $all_category_ids : array(),
        );

        $post_id = wp_insert_post( $post_data, true );
        if ( is_wp_error( $post_id ) ) {
            // Restore hooks
            foreach ( $saved_hooks as $tag => $filter_obj ) { $wp_filter[ $tag ] = $filter_obj; }
            $this->progress( 9, '❌ Post failed', $post_id->get_error_message(), 'error' );
            return;
        }

        // Step C: Set _dba_subject BEFORE _dba_auto_created
        // (dedup hook checks _dba_auto_created first — if not set, it skips)
        update_post_meta( $post_id, '_dba_subject',            $subject_key );
        update_post_meta( $post_id, '_dba_auto_created',       current_time( 'mysql' ) );
        update_post_meta( $post_id, '_dba_latam_auto_created', current_time( 'mysql' ) );
        update_post_meta( $post_id, '_dba_theme',              'latam_' . $country_key );
        update_post_meta( $post_id, '_dba_language',           'en' );
        update_post_meta( $post_id, '_dba_latam_country',      $country_key );
        update_post_meta( $post_id, '_dba_latam_city',         $city['name'] );
        update_post_meta( $post_id, '_dba_latam_region',       $city['region'] );
        update_post_meta( $post_id, '_dba_latam_theme',        $theme['key'] );
        update_post_meta( $post_id, $config['meta_prefix'] . 'auto_created', current_time( 'mysql' ) );
        update_post_meta( $post_id, $config['meta_prefix'] . 'commune',      $city['name'] );
        update_post_meta( $post_id, $config['meta_prefix'] . 'region',       $city['region'] );

        // Step D: Yoast SEO
        update_post_meta( $post_id, '_yoast_wpseo_metadesc', $meta );
        update_post_meta( $post_id, '_yoast_wpseo_focuskw',  $kw );

        // Step E: DIVI META — CRITICAL for frontend rendering
        update_post_meta( $post_id, '_et_pb_use_builder',          'off' );
        update_post_meta( $post_id, '_et_pb_old_content',          $content );
        update_post_meta( $post_id, '_et_pb_page_layout',          'et_right_sidebar' );
        update_post_meta( $post_id, '_et_pb_show_title',           'on' );
        update_post_meta( $post_id, '_et_pb_built_for_post_type',  'post' );
        update_post_meta( $post_id, '_wp_page_template',           'default' );

        // Step F: Featured image
        if ( $media_id ) set_post_thumbnail( $post_id, $media_id );

        // Step G: Publish (same as core — update from draft to publish)
        if ( $pstatus === 'publish' ) {
            wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );
        }

        // Step H: RESTORE all hooks + mark subject used
        foreach ( $saved_hooks as $tag => $filter_obj ) { $wp_filter[ $tag ] = $filter_obj; }
        if ( function_exists( 'dba_mark_subject_used_today' ) ) dba_mark_subject_used_today( $subject_key );
        $this->progress( 9, '💾 Step 9 — Post #' . $post_id, '✓ ' . ucfirst( $pstatus ) );

        // ── 10. Cross-link LATAM posts (bidirectional — no orphans) ──
        $this->progress( 10, '🔗 Step 10 — Cross-linking LATAM posts...', 'Finding related articles' );

        $cross_count = $this->crosslink_latam_posts( $post_id, $title, $country_key, $config['label'], $city['name'], $theme['key'], $theme['label'] );

        // ── 11. Flush ALL caches so post appears on homepage immediately ──
        $this->progress( 11, '🧹 Step 11 — Flushing caches...', 'Homepage + post cache' );

        $this->flush_all_caches( $post_id );

        // ── 12. Done ──
        $this->progress( 12, '🏁 Step 12 — Finalizing...', '' );

        if ( class_exists( 'DBA_Logger' ) ) {
            DBA_Logger::get_instance()->log( array( 'title' => $title, 'post_id' => $post_id, 'media_id' => $media_id, 'theme' => 'latam_' . $country_key, 'language' => 'en', 'status' => 'success' ) );
        }

        $summary = "✓ Post #{$post_id}\n"
            . "🌎 {$config['label']} → {$city['name']}\n"
            . "📋 {$theme['label']}\n"
            . "🔍 \"{$kw}\" | " . mb_strlen( $meta ) . "c desc\n"
            . ( $media_id ? "🖼️ #$media_id\n" : "" )
            . ( $linked ? "🔗 Site links added\n" : "" )
            . ( $cross_count ? "🔗 {$cross_count} LATAM cross-links\n" : "" )
            . ( $news ? "🌐 News-grounded\n" : "" )
            . "📄 \"" . mb_substr( $title, 0, 60 ) . "\"";

        $this->progress( 12, '✅ Published!', $summary, 'success' );
        error_log( 'DBA LATAM: ✓ #' . $post_id . ' "' . $title . '"' );

        // ── TIS: track directive progress ──
        if ( class_exists( 'DBA_TIS_Recommender' ) ) {
            DBA_TIS_Recommender::auto_track_progress( 'latam', $title );
        }

        // ── PIPELINE: mark brief as published ──
        if ( ! empty( $this->_pipeline_brief ) ) {
            DBA_TIS_Pipeline::mark_published( $title, $post_id );
            update_post_meta( $post_id, '_dba_pipeline_brief', wp_json_encode( $this->_pipeline_brief ) );
            $this->_pipeline_brief = null;
        }
    }

    /* ── Batch Rotation Helpers ───────────────────────────────────────── */

    /**
     * Select the next country for a cron batch, respecting the persistent
     * rotation index while skipping recently used / already-picked countries.
     *
     * @param array $enabled          Enabled country keys in rotation order.
     * @param array $recent_countries Map of country_key => true (cooldown window).
     * @param array $used_countries   Map of country_key => true (current batch).
     * @return string|false
     */
    private function select_next_country_for_batch( $enabled, $recent_countries, $used_countries ) {
        $enabled = array_values( array_filter( (array) $enabled ) );
        if ( empty( $enabled ) ) return false;

        $state = get_option( 'dba_latam_rotation_state', array( 'index' => 0 ) );
        $index = intval( $state['index'] ?? 0 );
        $count = count( $enabled );
        if ( $index >= $count ) $index = 0;

        for ( $tries = 0; $tries < $count; $tries++ ) {
            $pos = ( $index + $tries ) % $count;
            $key = $enabled[ $pos ];

            if ( isset( $used_countries[ $key ] ) ) continue;
            if ( isset( $recent_countries[ $key ] ) ) continue;

            // Advance rotation to the item AFTER the chosen one
            $state['index']        = ( $pos + 1 ) % $count;
            $state['last_run']     = current_time( 'mysql' );
            $state['last_country'] = $key;
            update_option( 'dba_latam_rotation_state', $state, false );

            return $key;
        }

        // No eligible country found
        return false;
    }
/**
 * ADMIN helper: recent usage for preview (cooldown-aware "Next").
 *
 * @param int $days
 * @param int $ref_ts
 * @return array
 */
public function get_recent_latam_usage_for_admin( $days, $ref_ts ) {
    return $this->get_recent_latam_usage( $days, $ref_ts );
}

/**
 * Return countries + cities used in the last N days relative to a reference timestamp
 * (calendar-day logic, site timezone).
 *
 * Includes 'publish' + 'future' posts so scheduled posts also block repeats.
 *
 * @param int $days
 * @param int|null $ref_ts
 * @return array { countries: map, cities: map }
 */
private function get_recent_latam_usage( $days, $ref_ts = null ) {
    global $wpdb;
    $days = max( 1, absint( $days ) );

    // Reference timestamp: when the batch is (or will be) published.
    $ref_ts = $ref_ts ? (int) $ref_ts : current_time( 'timestamp' );

    // Calendar-day cutoff in site timezone (midnight boundary).
    $cutoff = null;
    try {
        $tz  = function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( get_option( 'timezone_string' ) ?: 'UTC' );
        $ref = ( new DateTimeImmutable( "@{$ref_ts}" ) )->setTimezone( $tz )->setTime( 0, 0, 0 );
        $cut = $ref->sub( new DateInterval( "P{$days}D" ) );
        $cutoff = $cut->format( 'Y-m-d H:i:s' );
    } catch ( Exception $e ) {
        $cutoff_ts = current_time( 'timestamp' ) - ( $days * DAY_IN_SECONDS );
        $cutoff    = gmdate( 'Y-m-d H:i:s', $cutoff_ts );
    }

    $countries = array();
    $cities    = array();

    // Pull country + city meta for published/scheduled posts within the window.
    $sql = $wpdb->prepare(
        "SELECT c.meta_value AS country_key, ci.meta_value AS city_name
         FROM {$wpdb->posts} p
         INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = %s
         LEFT JOIN {$wpdb->postmeta} c  ON c.post_id  = p.ID AND c.meta_key  = %s
         LEFT JOIN {$wpdb->postmeta} ci ON ci.post_id = p.ID AND ci.meta_key = %s
         WHERE p.post_status IN ('publish','future')
           AND p.post_type   = 'post'
           AND p.post_date  >= %s",
        '_dba_latam_auto_created',
        '_dba_latam_country',
        '_dba_latam_city',
        $cutoff
    );

    $rows = $wpdb->get_results( $sql, ARRAY_A );
    if ( $rows ) {
        foreach ( $rows as $r ) {
            if ( ! empty( $r['country_key'] ) ) {
                $countries[ sanitize_key( $r['country_key'] ) ] = true;
            }
            if ( ! empty( $r['city_name'] ) ) {
                $cities[ mb_strtolower( trim( $r['city_name'] ) ) ] = true;
            }
        }
    }

    return array( 'countries' => $countries, 'cities' => $cities );
}


    /**
     * Get next city for a country, skipping cities in $avoid.
     * This advances the per-country city index until it finds an eligible city.
     */
    private function get_next_city_avoiding( $key, $cfg, $avoid = array() ) {
        $avoid = (array) $avoid;

        // Build the candidate list exactly as get_next_city() does.
        $cities = get_option( $cfg['communes_option'], array() );
        if ( empty( $cities ) ) $cities = DBA_LATAM_Cities::get_country_cities( $key );
        $s   = get_option( $cfg['settings_option'], array() );
        $act = ! empty( $s['active_communes'] ) ? $s['active_communes'] : array();
        if ( $act ) {
            $cities = array_values( array_filter( $cities, function( $c ) use ( $act ) {
                return in_array( $c['name'], $act, true );
            } ) );
        }
        if ( ! $cities ) return null;

        $ik = 'dba_latam_city_idx_' . $key;
        $i  = intval( get_option( $ik, 0 ) );
        $n  = count( $cities );
        if ( $i >= $n ) $i = 0;

        // Try up to N cities to find a non-avoided one.
        for ( $tries = 0; $tries < $n; $tries++ ) {
            $pos  = ( $i + $tries ) % $n;
            $city = $cities[ $pos ];
            $name_lc = mb_strtolower( trim( $city['name'] ?? '' ) );
            if ( $name_lc && isset( $avoid[ $name_lc ] ) ) {
                continue;
            }

            // Advance index to the next slot after selected city
            update_option( $ik, $pos + 1, false );
            return $city;
        }

        // If everything is avoided, fall back to the normal next city
        return $this->get_next_city( $key, $cfg );
    }

    /* ── News Research (Google News RSS) ── */

    private function fetch_news( $country, $city, $topic ) {
        $queries = array(
            $city . ' ' . $country . ' investment 2026',
            $country . ' ' . $topic . ' 2025 2026',
        );
        $items = array();

        foreach ( $queries as $q ) {
            $url = 'https://news.google.com/rss/search?' . http_build_query( array( 'q' => $q, 'hl' => 'en', 'gl' => 'US', 'ceid' => 'US:en' ) );
            $r = wp_remote_get( $url, array( 'timeout' => 8, 'sslverify' => false ) );
            if ( is_wp_error( $r ) || wp_remote_retrieve_response_code( $r ) !== 200 ) continue;

            libxml_use_internal_errors( true );
            $xml = simplexml_load_string( wp_remote_retrieve_body( $r ) );
            libxml_clear_errors();
            if ( ! $xml ) continue;

            $n = 0;
            foreach ( $xml->channel->item as $it ) {
                if ( $n >= 5 ) break;
                $h = wp_strip_all_tags( (string) $it->title );
                $src = '';
                if ( strpos( $h, ' - ' ) !== false ) {
                    $p = explode( ' - ', $h );
                    $src = array_pop( $p );
                    $h = implode( ' - ', $p );
                }
                $d = wp_strip_all_tags( (string) $it->description );
                $dt = '';
                if ( ! empty( (string) $it->pubDate ) ) {
                    $ts = strtotime( (string) $it->pubDate );
                    if ( $ts ) $dt = date( 'M j, Y', $ts );
                }
                $line = '• ' . $h;
                if ( $dt )  $line .= ' [' . $dt . ']';
                if ( $d )   $line .= "\n  " . mb_substr( $d, 0, 200 );
                $items[] = $line;
                $n++;
            }
        }

        if ( empty( $items ) ) return '';
        $seen = array(); $uniq = array();
        foreach ( $items as $i ) {
            $k = mb_substr( $i, 0, 40 );
            if ( ! isset( $seen[$k] ) ) { $seen[$k] = 1; $uniq[] = $i; }
        }
        return implode( "\n", array_slice( $uniq, 0, 8 ) );
    }

    /* ── Prompt Builder ── */

    private function build_prompt( $config, $city, $theme, $wmin, $wmax, $news = '' ) {
        // Sanitize theme label against banned words so GPT doesn't copy them
        $theme_label   = $theme['label'];
        $theme_context = $theme['prompt_context'];
        $main_settings = get_option( 'dba_settings', array() );
        $bw_list = ! empty( $main_settings['banned_words'] ) ? array_filter( array_map( 'trim', (array) $main_settings['banned_words'] ) ) : array();
        foreach ( $bw_list as $bw ) {
            if ( strlen( $bw ) < 2 ) continue;
            $pat = '/\b' . preg_quote( $bw, '/' ) . '\b/i';
            $theme_label   = trim( preg_replace( '/\s{2,}/', ' ', preg_replace( $pat, '', $theme_label ) ) );
            $theme_context = trim( preg_replace( '/\s{2,}/', ' ', preg_replace( $pat, '', $theme_context ) ) );
        }
        // Clean up dangling separators
        $theme_label = preg_replace( '/^[\s&,;]+|[\s&,;]+$/', '', $theme_label );
        if ( empty( $theme_label ) ) $theme_label = $theme['label']; // fallback if everything was banned

        // Vary the opening instruction to prevent template patterns
        $openers = array(
            "Write a {$wmin}–{$wmax} word article about {$theme_label} in {$city['name']}, {$config['label']}.",
            "Produce a {$wmin}–{$wmax} word analysis covering {$theme_label} for investors considering {$city['name']}, {$config['label']}.",
            "Draft a {$wmin}–{$wmax} word guide on {$theme_label}, focusing on {$city['name']}, {$config['label']}.",
        );
        $opener = $openers[ array_rand( $openers ) ];

        $p = "{$opener}\n\n"
            . "Location: {$city['name']} ({$city['region']})\nAngle: {$theme_context}\n"
            . "Audience: International investors, entrepreneurs, family offices\n"
            . "Date context: " . date( 'F Y' ) . "\n\n";

        if ( $news ) {
            $p .= "BACKGROUND RESEARCH (use facts only — do NOT name or cite any source):\n{$news}\n\n";
        }

        $p .= "CRITICAL REMINDERS:\n"
            . "- Mention the city name MAX 3 times, country name MAX 4 times. Use 'the city', 'this market', 'the country' instead.\n"
            . "- Every paragraph must contain a concrete fact, number, or actionable insight — no filler.\n"
            . "- Vary structure: do NOT always use the same heading pattern.\n"
            . "- Write like a knowledgeable journalist, not a template.\n"
            . "- NEVER invent or fabricate any damalion.com URL. Internal links are injected automatically after generation.\n\n"
            . "FORMAT: Return 3 XML tags first, then HTML body:\n"
            . "<title>Unique, compelling title</title>\n"
            . "<meta_description>Max 135 chars, include city+country</meta_description>\n"
            . "<focus_keyphrase>2-4 words</focus_keyphrase>\n\n"
            . "Then HTML (<h2>,<h3>,<p>,<ul> — no <h1>, no markdown).";
        return $p;
    }

    /* ── CTA ── */

    private function build_cta( $config, $s ) {
        $t = ! empty( $s['cta_text'] )      ? $s['cta_text']      : $config['cta_text'];
        $l = ! empty( $s['cta_link_text'] ) ? $s['cta_link_text'] : $config['cta_link_text'];
        $u = ! empty( $s['cta_link_url'] )  ? $s['cta_link_url']  : $config['cta_link_url'];
        return '<p>' . esc_html( str_replace( '{Country}', $config['label'], $t ) )
            . ' <a href="' . esc_url( $u ) . '" target="_blank" rel="noopener">' . esc_html( $l ) . '</a></p>';
    }

    /* ── City Rotation ── */

    private function get_next_city( $key, $cfg ) {
        $cities = get_option( $cfg['communes_option'], array() );
        if ( empty( $cities ) ) $cities = DBA_LATAM_Cities::get_country_cities( $key );
        $s = get_option( $cfg['settings_option'], array() );
        $act = ! empty( $s['active_communes'] ) ? $s['active_communes'] : array();
        if ( $act ) $cities = array_values( array_filter( $cities, function($c) use ($act) { return in_array($c['name'],$act); } ) );
        if ( ! $cities ) return null;
        $ik = 'dba_latam_city_idx_' . $key;
        $i = intval( get_option( $ik, 0 ) );
        if ( $i >= count( $cities ) ) $i = 0;
        update_option( $ik, $i + 1, false );
        return $cities[$i];
    }

    /* ── Theme Rotation ── */

    public static function get_themes() {
        return array(
            array( 'key'=>'business_formation',  'label'=>'Business Formation & Company Registration',  'prompt_context'=>'How to register a business. Steps, costs, timelines.' ),
            array( 'key'=>'fdi_opportunities',    'label'=>'Foreign Direct Investment Opportunities',    'prompt_context'=>'FDI landscape, incentives, sectors attracting foreign capital.' ),
            array( 'key'=>'real_estate',          'label'=>'Real Estate Investment',                     'prompt_context'=>'Real estate for international buyers. Rules, restrictions.' ),
            array( 'key'=>'tax_structuring',      'label'=>'Tax Structuring & Treaty Advantages',       'prompt_context'=>'Tax rates, treaties, transfer pricing, incentives.' ),
            array( 'key'=>'banking',              'label'=>'Banking & Corporate Account Opening',       'prompt_context'=>'Opening corporate accounts as foreign investor.' ),
            array( 'key'=>'regulatory',           'label'=>'Regulatory Environment & Compliance',       'prompt_context'=>'Regulatory bodies, compliance, AML, reporting.' ),
            array( 'key'=>'residency_visa',       'label'=>'Investor Visa & Residency Programs',        'prompt_context'=>'Investor visas, residency-by-investment.' ),
            array( 'key'=>'free_trade_zones',     'label'=>'Free Trade Zones & SEZs',                   'prompt_context'=>'Free trade zones, SEZs, tax-free zones.' ),
            array( 'key'=>'sector_opportunities', 'label'=>'Sector-Specific Opportunities',             'prompt_context'=>'Fintech, mining, agriculture, energy, tourism.' ),
        );
    }

    private function get_next_theme( $key ) {
        $t = self::get_themes();
        $ik = 'dba_latam_theme_idx_' . $key;
        $i = intval( get_option( $ik, 0 ) );
        if ( $i >= count( $t ) ) $i = 0;
        update_option( $ik, $i + 1, false );
        return $t[$i];
    }

    /* ── Collision Guard ── */

    private function wait_for_other_bots() {
        // FIX v2.11.1: No more sleep() loops — bail immediately if another bot is running.
        // The cron will retry on the next wp-cron tick (5 min with your cPanel cron).

        // Core batch: dba_generation_progress (status=running)
        $core = get_transient( 'dba_generation_progress' );
        if ( $core && isset( $core['current_status'] ) && $core['current_status'] === 'running'
             && isset( $core['updated'] ) && ( time() - $core['updated'] ) < 600 ) {
            $this->progress( 1, '⏳ Deferred', 'Core Damalion batch running — will retry next cron tick' );
            error_log( 'DBA LATAM: Core batch running — bailing (no sleep)' );
            delete_transient( self::LOCK_KEY );
            throw new \RuntimeException( 'LATAM_DEFERRED:core_running' );
        }

        // PE NewsBot: dba_pe_generation_progress (status=running)
        $pe = get_transient( 'dba_pe_generation_progress' );
        if ( $pe && isset( $pe['current_status'] ) && $pe['current_status'] === 'running'
             && isset( $pe['updated'] ) && ( time() - $pe['updated'] ) < 600 ) {
            $this->progress( 1, '⏳ Deferred', 'PE NewsBot batch running — will retry next cron tick' );
            error_log( 'DBA LATAM: PE NewsBot running — bailing (no sleep)' );
            delete_transient( self::LOCK_KEY );
            throw new \RuntimeException( 'LATAM_DEFERRED:pe_running' );
        }

        // LuxStrat: no need to wait — it's a transient lock, just means it ran today (not running now)
    }

    private static function next_occ( $d, $t, $tz ) {
        $n = new DateTime( 'now', $tz );
        $x = new DateTime( 'next ' . $d . ' ' . $t, $tz );
        if ( $x <= $n ) $x->modify( '+7 days' );
        return $x->getTimestamp();
    }

    /**
     * Nuclear fallback: query ALL published posts + pages from WP DB
     * and return the most relevant ones as internal links.
     */
    private function fallback_internal_links( $country, $city, $topic, $max = 8 ) {
        global $wpdb;
        $links = array();

        // Get all published posts and pages
        $rows = $wpdb->get_results(
            "SELECT ID, post_title, post_name, post_type FROM {$wpdb->posts}
             WHERE post_status = 'publish'
             AND post_type IN ('post', 'page')
             AND post_title NOT LIKE '%[DUPLICATE%'
             ORDER BY post_date DESC
             LIMIT 500",
            ARRAY_A
        );

        if ( empty( $rows ) ) return array();

        // Build keyword list: country, city, topic + LATAM-specific + generic investment terms
        $keywords = array_filter( array_map( 'trim', array_merge(
            explode( ' ', strtolower( $country ) ),
            explode( ' ', strtolower( $city ) ),
            explode( ' ', strtolower( $topic ) ),
            array( 'investment', 'business', 'company', 'tax', 'bank', 'visa', 'real estate', 'trade', 'corporate',
                   'latin america', 'latam', 'doing business', 'company formation', 'holding',
                   'fund', 'compliance', 'structuring', 'private equity', 'asset management' )
        ) ), function( $w ) { return strlen( $w ) > 2; } );

        $scored = array();
        foreach ( $rows as $row ) {
            $t = strtolower( $row['post_title'] . ' ' . $row['post_name'] );
            $score = 0;

            foreach ( $keywords as $kw ) {
                if ( strlen( $kw ) > 2 && strpos( $t, $kw ) !== false ) {
                    $score += ( strlen( $kw ) > 5 ) ? 3 : 1;
                }
            }

            // BOOST: "Doing Business in [Country]" pages are SEO gold for LATAM posts
            if ( strpos( $t, 'doing business in' ) !== false ) $score += 8;
            if ( strpos( $t, strtolower( $country ) ) !== false ) $score += 5;

            // BOOST: Service pages (pillar content) — pages are typically more valuable for SEO
            if ( $row['post_type'] === 'page' ) $score += 2;

            // BOOST: LATAM-related content
            if ( strpos( $t, 'latin america' ) !== false || strpos( $t, 'latam' ) !== false ) $score += 4;

            // BOOST: Country-specific service pages
            $latam_countries = array( 'brazil', 'mexico', 'colombia', 'argentina', 'chile', 'peru', 'costa rica', 'panama', 'uruguay', 'ecuador', 'guatemala', 'paraguay', 'bolivia', 'el salvador' );
            foreach ( $latam_countries as $lc ) {
                if ( strpos( $t, $lc ) !== false && strpos( strtolower( $country ), $lc ) !== false ) {
                    $score += 6;
                }
            }

            if ( $score > 0 ) {
                $url = get_permalink( $row['ID'] );
                if ( $url ) {
                    $scored[] = array(
                        'url'   => $url,
                        'title' => $row['post_title'],
                        'score' => $score,
                    );
                }
            }
        }

        // Sort by score descending, take top N
        usort( $scored, function( $a, $b ) { return $b['score'] - $a['score']; } );
        $links = array_slice( $scored, 0, $max );

        error_log( 'DBA LATAM fallback_internal_links: found ' . count( $links ) . ' relevant links (top scores: ' . implode( ',', array_map( function($l) { return $l['score']; }, array_slice( $links, 0, 3 ) ) ) . ') from ' . count( $rows ) . ' total pages' );
        return $links;
    }

    /**
     * Manual link injection: finds keywords in HTML paragraphs and wraps them in <a> tags.
     * Used when DBA_Auto_Linker is not available.
     */
    private function manual_inject_links( $content, $pillar_url, $links ) {
        if ( empty( $links ) ) return $content;

        $injected = 0;
        $max = 6;

        // Helper: inject ONE link into $content using tag-splitting (avoids broken lookbehinds)
        $safe_inject = function( &$html, $needle, $url ) {
            if ( strpos( $html, $url ) !== false ) return false; // already linked
            $parts = preg_split( '/(<a\s[^>]*>.*?<\/a>)/is', $html, -1, PREG_SPLIT_DELIM_CAPTURE );
            if ( $parts === false ) return false;
            $esc = preg_quote( $needle, '/' );
            for ( $i = 0; $i < count( $parts ); $i++ ) {
                if ( $i % 2 === 1 ) continue; // skip <a> tags
                if ( preg_match( '/\b' . $esc . '\b/i', $parts[$i] ) ) {
                    $parts[$i] = preg_replace(
                        '/\b(' . $esc . ')\b/i',
                        '<a href="' . esc_url( $url ) . '">$1</a>',
                        $parts[$i], 1, $c
                    );
                    if ( $c > 0 ) { $html = implode( '', $parts ); return true; }
                }
            }
            return false;
        };

        // First inject pillar link if not already present
        if ( $pillar_url && strpos( $content, $pillar_url ) === false ) {
            $anchor_words = array(
                'investment opportunities', 'business formation', 'international investors',
                'foreign investment', 'corporate structure', 'business in',
                'Damalion', 'consulting firm', 'professional advisory',
            );
            foreach ( $anchor_words as $aw ) {
                if ( $injected >= $max ) break;
                if ( stripos( $content, $aw ) !== false ) {
                    if ( $safe_inject( $content, $aw, $pillar_url ) ) { $injected++; break; }
                }
            }
        }

        // Then inject satellite links with SEO-optimized anchor text
        foreach ( $links as $link ) {
            if ( $injected >= $max ) break;
            $url   = $link['url'] ?? '';
            $title = $link['title'] ?? '';
            if ( empty( $url ) || empty( $title ) || strpos( $content, $url ) !== false ) continue;

            // Build phrases from link title: 3-word → 2-word → single long words
            $words = array_filter( explode( ' ', $title ), function( $w ) { return strlen( $w ) > 3; } );
            $words_arr = array_values( $words );
            $phrases = array();

            // 3-word phrases
            for ( $i = 0; $i < count( $words_arr ) - 2; $i++ ) {
                $phrases[] = $words_arr[$i] . ' ' . $words_arr[$i+1] . ' ' . $words_arr[$i+2];
            }
            // 2-word phrases
            for ( $i = 0; $i < count( $words_arr ) - 1; $i++ ) {
                $phrases[] = $words_arr[$i] . ' ' . $words_arr[$i+1];
            }
            // Single long words (6+ chars)
            foreach ( $words_arr as $w ) {
                if ( strlen( $w ) >= 6 ) $phrases[] = $w;
            }

            // Also add SEO anchor phrases from link URL slug
            if ( preg_match( '#/([^/]+?)/?$#', $url, $m ) ) {
                $slug_words = explode( '-', $m[1] );
                $slug_words = array_filter( $slug_words, function($w) { return strlen($w) > 3; } );
                $sw = array_values( $slug_words );
                for ( $i = 0; $i < count( $sw ) - 1; $i++ ) {
                    $phrases[] = $sw[$i] . ' ' . $sw[$i+1];
                }
            }

            foreach ( $phrases as $phrase ) {
                if ( $injected >= $max ) break;
                $phrase = trim( $phrase );
                if ( strlen( $phrase ) < 4 ) continue;
                if ( stripos( $content, $phrase ) !== false ) {
                    if ( $safe_inject( $content, $phrase, $url ) ) { $injected++; break; }
                }
            }
        }

        error_log( 'DBA LATAM manual_inject_links: injected ' . $injected . ' links' );
        return $content;
    }

    /* ══════════════════════════════════════════════════════════════════
     *  HOMEPAGE CATEGORY MAPPING
     *  Maps LATAM themes to mainstream categories visible on the homepage.
     * ══════════════════════════════════════════════════════════════════ */

    /**
     * Get ONE additional mainstream category so LATAM posts have broader visibility.
     * The homepage excludes only live sports. LATAM posts in "latam-investment" should appear,
     * but assigning one extra relevant mainstream category provides insurance if the Divi
     * blog modules have include_categories filters.
     */
    private function get_homepage_categories( $theme_key, $country_key, $city_name ) {

        // Map each LATAM theme to its single best-fit mainstream category
        $theme_to_cat = array(
            'business_formation'  => 'company-formation-business-registration',
            'fdi_opportunities'   => 'foreign-direct-investments',
            'real_estate'         => 'investments',
            'tax_structuring'     => 'investments',
            'banking'             => 'banking',
            'regulatory'          => 'investments',
            'residency_visa'      => 'investments',
            'free_trade_zones'    => 'foreign-direct-investments',
            'sector_opportunities'=> 'investments',
        );

        $slug = isset( $theme_to_cat[ $theme_key ] ) ? $theme_to_cat[ $theme_key ] : 'investments';

        $ids = array();
        $term = get_term_by( 'slug', $slug, 'category' );
        if ( $term && ! is_wp_error( $term ) ) {
            $ids[] = intval( $term->term_id );
        }

        // If primary didn't work, try fallback slugs
        if ( empty( $ids ) ) {
            $fallbacks = array( 'investments', 'foreign-direct-investments', 'banking', 'funds' );
            foreach ( $fallbacks as $fb ) {
                $term = get_term_by( 'slug', $fb, 'category' );
                if ( $term && ! is_wp_error( $term ) ) {
                    $ids[] = intval( $term->term_id );
                    break;
                }
            }
        }

        error_log( 'DBA LATAM homepage cat for theme "' . $theme_key . '": slug=' . $slug . ' ids=' . implode( ',', $ids ) );
        return $ids;
    }

    /* ══════════════════════════════════════════════════════════════════
     *  SEO CONTENT SCANNER — Extract key phrases from generated article
     *  to enable contextual, relevant internal linking.
     * ══════════════════════════════════════════════════════════════════ */

    /**
     * Scan the generated article content and extract key phrases for SEO matching.
     * Returns array of phrases scored by relevance and SEO value.
     *
     * This is the "scan process" that ensures links are contextually relevant
     * to what the article actually discusses.
     */
    private function scan_content_keyphrases( $html_content ) {
        // Strip HTML to get raw text
        $text = wp_strip_all_tags( $html_content );
        $text = preg_replace( '/\s+/', ' ', $text );
        $text_lower = strtolower( $text );

        $phrases = array();
        $seen = array();

        // ── 1. Extract H2/H3 headings as top-priority phrases ──
        if ( preg_match_all( '/<h[23][^>]*>(.+?)<\/h[23]>/si', $html_content, $hm ) ) {
            foreach ( $hm[1] as $heading ) {
                $h = trim( wp_strip_all_tags( $heading ) );
                if ( strlen( $h ) > 10 && strlen( $h ) < 80 ) {
                    // Extract 2-3 word chunks from headings
                    $hw = array_values( array_filter( explode( ' ', $h ), function( $w ) { return strlen( $w ) > 3; } ) );
                    for ( $i = 0; $i < count( $hw ) - 1; $i++ ) {
                        $p = $hw[$i] . ' ' . $hw[$i+1];
                        $k = strtolower( $p );
                        if ( ! isset( $seen[$k] ) && strlen( $p ) > 6 ) {
                            $phrases[] = array( 'phrase' => $p, 'weight' => 10 );
                            $seen[$k] = true;
                        }
                    }
                    for ( $i = 0; $i < count( $hw ) - 2; $i++ ) {
                        $p = $hw[$i] . ' ' . $hw[$i+1] . ' ' . $hw[$i+2];
                        $k = strtolower( $p );
                        if ( ! isset( $seen[$k] ) && strlen( $p ) > 10 ) {
                            $phrases[] = array( 'phrase' => $p, 'weight' => 12 );
                            $seen[$k] = true;
                        }
                    }
                }
            }
        }

        // ── 2. Extract SEO-valuable compound phrases from paragraph text ──
        // These patterns capture typical business/investment/legal terminology
        $seo_patterns = array(
            // Business & Investment
            '/\b((?:foreign|direct|private|institutional|real estate|venture|angel)\s+invest(?:ment|or|ing)s?)\b/i',
            '/\b((?:business|company|corporate|entity|branch|subsidiary)\s+(?:formation|registration|structuring|incorporation|setup))\b/i',
            '/\b((?:tax|fiscal|double taxation|transfer pricing)\s+(?:treaty|treaties|incentive|benefit|rate|regime|compliance|planning|structure|structuring)s?)\b/i',
            '/\b((?:free trade|special economic|export processing|duty-free)\s+zone?s?)\b/i',
            '/\b((?:investor|golden|residency|work|business)\s+visa(?:s)?)\b/i',
            '/\b((?:bank|corporate|merchant|escrow)\s+account(?:s)?)\b/i',
            '/\b((?:regulatory|legal|compliance|AML|KYC|due diligence)\s+(?:framework|requirement|environment|standard|procedure)s?)\b/i',
            '/\b((?:capital|financial|stock|bond|equity)\s+market(?:s)?)\b/i',
            '/\b((?:holding|shell|offshore|parent|subsidiary)\s+compan(?:y|ies))\b/i',
            '/\b((?:mergers?|acquisitions?)\s+(?:and|&)\s+(?:acquisitions?|mergers?))\b/i',
            '/\b((?:intellectual|industrial)\s+property(?:\s+rights?)?)\b/i',
            '/\b((?:labor|employment|workforce)\s+(?:law|regulation|market|code)s?)\b/i',
            '/\b((?:customs|import|export|trade)\s+(?:regulation|duty|duties|tariff|procedure)s?)\b/i',
            // Geography + Business
            '/\b(doing business in\s+\w+)\b/i',
            '/\b(open(?:ing)?\s+a\s+(?:bank|corporate|business)\s+account)\b/i',
            '/\b(company\s+formation\s+(?:in|services))\b/i',
            '/\b(fund\s+(?:structuring|management|administration))\b/i',
        );

        foreach ( $seo_patterns as $pattern ) {
            if ( preg_match_all( $pattern, $text, $pm ) ) {
                foreach ( $pm[1] as $match ) {
                    $m = trim( $match );
                    $k = strtolower( $m );
                    if ( ! isset( $seen[$k] ) && strlen( $m ) > 6 ) {
                        $phrases[] = array( 'phrase' => $m, 'weight' => 8 );
                        $seen[$k] = true;
                    }
                }
            }
        }

        // ── 3. Extract proper nouns and named entities (capitalized multi-word) ──
        if ( preg_match_all( '/\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3})\b/', $text, $nm ) ) {
            foreach ( $nm[1] as $name ) {
                $k = strtolower( $name );
                // Skip common titles, short words
                if ( isset( $seen[$k] ) || strlen( $name ) < 6 ) continue;
                $skip = array( 'the', 'this', 'that', 'which', 'these', 'those', 'with', 'from', 'into', 'about' );
                $first_word = strtolower( explode( ' ', $name )[0] );
                if ( in_array( $first_word, $skip ) ) continue;
                $phrases[] = array( 'phrase' => $name, 'weight' => 5 );
                $seen[$k] = true;
            }
        }

        // ── 4. Extract high-frequency 2-grams from body text ──
        $words = preg_split( '/[^\w]+/', $text_lower );
        $words = array_values( array_filter( $words, function( $w ) { return strlen( $w ) > 4; } ) );
        $bigrams = array();
        $stop_words = array( 'about', 'above', 'after', 'again', 'being', 'below', 'between', 'could',
            'doing', 'during', 'every', 'further', 'having', 'itself', 'might', 'other', 'shall',
            'should', 'their', 'there', 'these', 'those', 'under', 'until', 'where', 'which',
            'while', 'would', 'years', 'percent', 'number', 'include', 'including', 'however',
            'through', 'within', 'without', 'across', 'before', 'since', 'still', 'available',
            'according', 'offer', 'offers', 'based', 'provide', 'provides', 'require', 'requires' );

        for ( $i = 0; $i < count( $words ) - 1; $i++ ) {
            if ( in_array( $words[$i], $stop_words ) || in_array( $words[$i+1], $stop_words ) ) continue;
            $bg = $words[$i] . ' ' . $words[$i+1];
            if ( ! isset( $bigrams[$bg] ) ) $bigrams[$bg] = 0;
            $bigrams[$bg]++;
        }
        arsort( $bigrams );
        $top_bg = array_slice( $bigrams, 0, 20, true );
        foreach ( $top_bg as $bg => $count ) {
            if ( $count >= 2 && ! isset( $seen[$bg] ) && strlen( $bg ) > 8 ) {
                $phrases[] = array( 'phrase' => $bg, 'weight' => min( $count * 2, 6 ) );
                $seen[$bg] = true;
            }
        }

        // Sort by weight descending
        usort( $phrases, function( $a, $b ) { return $b['weight'] - $a['weight']; } );

        return array_slice( $phrases, 0, 50 ); // top 50 key phrases
    }

    /**
     * SEO Page Scanner: Find the most relevant internal pages for the extracted keyphrases.
     * Uses the article's own language to find contextually matching pages.
     */
    private function seo_scan_for_links( $content_phrases, $country, $city, $topic, $theme_key, $max = 10 ) {
        global $wpdb;

        // Get all published English-language posts and pages
        // Filter out obviously foreign-language content by slug pattern
        $rows = $wpdb->get_results(
            "SELECT ID, post_title, post_name, post_type FROM {$wpdb->posts}
             WHERE post_status = 'publish'
             AND post_type IN ('post', 'page')
             AND post_title NOT LIKE '%[DUPLICATE%'
             AND post_name NOT REGEXP '[^a-z0-9-]'
             ORDER BY post_date DESC
             LIMIT 800",
            ARRAY_A
        );

        if ( empty( $rows ) ) return array();

        // Build phrase index for fast matching
        $phrase_map = array();
        foreach ( $content_phrases as $cp ) {
            $words = array_filter( explode( ' ', strtolower( $cp['phrase'] ) ), function( $w ) { return strlen( $w ) > 3; } );
            foreach ( $words as $w ) {
                $phrase_map[ $w ] = max( isset( $phrase_map[ $w ] ) ? $phrase_map[ $w ] : 0, $cp['weight'] );
            }
        }

        // Also add contextual keywords
        $ctx_words = array_filter( array_map( 'trim', array_merge(
            explode( ' ', strtolower( $country ) ),
            explode( ' ', strtolower( $city ) ),
            explode( ' ', strtolower( $topic ) )
        ) ), function( $w ) { return strlen( $w ) > 2; } );

        foreach ( $ctx_words as $cw ) {
            $phrase_map[ $cw ] = max( isset( $phrase_map[ $cw ] ) ? $phrase_map[ $cw ] : 0, 4 );
        }

        // Score each page
        $scored = array();
        $country_lower = strtolower( $country );
        $latam_countries = array( 'brazil', 'mexico', 'colombia', 'argentina', 'chile', 'peru',
            'costa rica', 'panama', 'uruguay', 'ecuador', 'guatemala', 'paraguay', 'bolivia', 'el salvador' );
        // Detect non-English by checking for common foreign-language characters in title
        $foreign_pattern = '/[\x{00C0}-\x{024F}\x{0370}-\x{03FF}\x{0400}-\x{04FF}\x{4E00}-\x{9FFF}\x{3040}-\x{30FF}\x{AC00}-\x{D7AF}]/u';

        foreach ( $rows as $row ) {
            // ── LANGUAGE FILTER: skip non-English titles ──
            if ( preg_match( $foreign_pattern, $row['post_title'] ) ) {
                // Allow if it contains the current country/city name (e.g. "São Paulo" has accents)
                if ( stripos( $row['post_title'], $city ) === false && stripos( $row['post_title'], $country ) === false ) {
                    continue;
                }
            }

            $t = strtolower( $row['post_title'] . ' ' . str_replace( '-', ' ', $row['post_name'] ) );
            $score = 0;
            $matched_words = array();

            // ── SKIP: live sports content ──
            if ( preg_match( '/\b(live\s+coverage|live\s+stream|match\s+result|bundesliga|premier\s+league|nba|nfl|la\s+liga|serie\s+a|champions\s+league|vs\b)/i', $row['post_title'] ) ) {
                continue;
            }

            // Match article phrases against page title/slug
            foreach ( $phrase_map as $word => $weight ) {
                if ( strlen( $word ) > 3 && strpos( $t, $word ) !== false ) {
                    $score += $weight;
                    $matched_words[] = $word;
                }
            }

            // ══════════════════════════════════════════════════════════
            // #1 PRIORITY: COUNTRY MATCH — this bot is about LATAM countries
            // Country-relevant pages ALWAYS rank above generic ones
            // ══════════════════════════════════════════════════════════
            $is_same_country = false;
            if ( strpos( $t, $country_lower ) !== false ) {
                $score += 50; // dominant boost — country match is king
                $is_same_country = true;
            }
            foreach ( $latam_countries as $lc ) {
                if ( strpos( $t, $lc ) !== false && strpos( $country_lower, $lc ) !== false ) {
                    if ( ! $is_same_country ) $score += 40;
                    $is_same_country = true;
                }
            }

            // "Doing Business in [Country]" — top-tier link target
            if ( strpos( $t, 'doing business in' ) !== false ) {
                if ( $is_same_country ) {
                    $score += 20; // same country doing-business = best possible link
                } else {
                    $score += 2;
                }
            }

            // PENALTY: Pages about OTHER LATAM countries (not ours)
            foreach ( $latam_countries as $lc ) {
                if ( strpos( $t, $lc ) !== false && strpos( $country_lower, $lc ) === false ) {
                    $score -= 30; // hard penalty — wrong country
                }
            }
            // PENALTY: Luxembourg-specific content (unless it's a service page)
            $lux_markers = array( 'luxembourg', 'luxemburg', 'luxemburgo', 'soparfi', 'ucits', 'raif', 'sicav' );
            foreach ( $lux_markers as $lm ) {
                if ( strpos( $t, $lm ) !== false && $row['post_type'] !== 'page' ) {
                    $score -= 20;
                    break;
                }
            }
            // PENALTY: Content clearly in another language
            $foreign_slug_markers = array( 'strategie', 'und', 'dans', 'oder', 'pour', 'como', 'qual', 'delle', 'degli' );
            foreach ( $foreign_slug_markers as $fsm ) {
                if ( strpos( $row['post_name'], $fsm ) !== false ) {
                    $score -= 30;
                    break;
                }
            }

            // BOOST: Service/pillar pages (pages are typically higher-value SEO targets)
            if ( $row['post_type'] === 'page' ) $score += 3;

            // BOOST: Matches from high-value heading phrases
            foreach ( $content_phrases as $cp ) {
                if ( $cp['weight'] >= 10 ) { // heading phrase
                    $cp_lower = strtolower( $cp['phrase'] );
                    if ( strpos( $t, $cp_lower ) !== false ) {
                        $score += 10;
                    }
                }
            }

            // BOOST: LATAM/Latin America related content
            if ( strpos( $t, 'latin america' ) !== false || strpos( $t, 'latam' ) !== false ) $score += 5;

            if ( $score >= 6 ) { // raised minimum relevance threshold
                $url = get_permalink( $row['ID'] );
                if ( $url ) {
                    $scored[] = array(
                        'url'           => $url,
                        'title'         => $row['post_title'],
                        'score'         => $score,
                        'matched_words' => $matched_words,
                        'post_type'     => $row['post_type'],
                    );
                }
            }
        }

        // Sort by score descending
        usort( $scored, function( $a, $b ) { return $b['score'] - $a['score']; } );
        $results = array_slice( $scored, 0, $max );

        error_log( 'DBA LATAM SEO scan: top scores: ' . implode( ', ', array_map( function($l) {
            return $l['score'] . '(' . $l['title'] . ')';
        }, array_slice( $results, 0, 3 ) ) ) );

        return $results;
    }

    /**
     * SEO-OPTIMIZED LINK INJECTION.
     * Uses the article's own phrases as anchor text for maximum relevance.
     * Falls back to page title phrases if no article phrase matches.
     */
    private function seo_inject_links( $content, $pillar_url, $links, $content_phrases = array() ) {
        if ( empty( $links ) ) return $content;

        $injected = 0;
        // Dynamic max: ~1 link per 150 words, min 3, max 6
        $wc = str_word_count( wp_strip_all_tags( $content ) );
        $max = max( 3, min( 6, intval( $wc / 150 ) ) );

        // Build phrase-to-use map from content scanner results
        $phrase_list = array();
        foreach ( $content_phrases as $cp ) {
            $phrase_list[] = $cp['phrase'];
        }

        // Safe injector: splits HTML by existing <a> tags to avoid nested links
        $safe_inject = function( &$html, $needle, $url ) {
            if ( strpos( $html, $url ) !== false ) return false;
            $parts = preg_split( '/(<a\s[^>]*>.*?<\/a>)/is', $html, -1, PREG_SPLIT_DELIM_CAPTURE );
            if ( $parts === false ) return false;
            for ( $i = 0; $i < count( $parts ); $i += 2 ) {
                if ( ! isset( $parts[$i] ) ) continue;
                $c = 0;
                $parts[$i] = preg_replace(
                    '/(?<![<\/\w])(' . preg_quote( $needle, '/' ) . ')(?![^<]*<\/a>)/iu',
                    '<a href="' . esc_url( $url ) . '">$1</a>',
                    $parts[$i], 1, $c
                );
                if ( $c > 0 ) { $html = implode( '', $parts ); return true; }
            }
            return false;
        };

        // First: inject pillar link
        if ( $pillar_url && strpos( $content, $pillar_url ) === false ) {
            $pillar_anchors = array(
                'investment opportunities', 'business formation', 'international investors',
                'foreign investment', 'corporate structure', 'business in',
                'Damalion', 'consulting firm', 'professional advisory',
            );
            foreach ( $pillar_anchors as $pa ) {
                if ( $injected >= $max ) break;
                if ( stripos( $content, $pa ) !== false ) {
                    if ( $safe_inject( $content, $pa, $pillar_url ) ) { $injected++; break; }
                }
            }
        }

        // Then: inject each link using SEO-optimized anchor selection
        foreach ( $links as $link ) {
            if ( $injected >= $max ) break;
            $url   = $link['url'] ?? '';
            $title = $link['title'] ?? '';
            if ( empty( $url ) || strpos( $content, $url ) !== false ) continue;

            $anchors = array();

            // ── STRATEGY 1: Use content phrases that match the target page ──
            // These are phrases FROM the article that relate TO the target page.
            // This produces the most natural, contextually relevant anchor text.
            $title_lower = strtolower( $title );
            $slug_words = array();
            if ( preg_match( '#/([^/]+?)/?$#', $url, $sm ) ) {
                $slug_words = array_filter( explode( '-', $sm[1] ), function( $w ) { return strlen( $w ) > 3; } );
            }
            $title_words = array_filter( explode( ' ', $title_lower ), function( $w ) { return strlen( $w ) > 3; } );
            $target_keywords = array_unique( array_merge( $title_words, $slug_words ) );

            foreach ( $phrase_list as $phrase ) {
                $pl = strtolower( $phrase );
                // Check if any target keyword appears in this content phrase
                foreach ( $target_keywords as $tk ) {
                    if ( strlen( $tk ) > 3 && strpos( $pl, $tk ) !== false ) {
                        // Verify phrase exists in content
                        if ( stripos( $content, $phrase ) !== false && strlen( $phrase ) > 6 ) {
                            $anchors[] = $phrase;
                            break;
                        }
                    }
                }
            }

            // ── STRATEGY 2: Build phrases from target page title ──
            $tw = array_values( array_filter( explode( ' ', $title ), function( $w ) { return strlen( $w ) > 3; } ) );
            // 3-word phrases
            for ( $i = 0; $i < count( $tw ) - 2; $i++ ) {
                $anchors[] = $tw[$i] . ' ' . $tw[$i+1] . ' ' . $tw[$i+2];
            }
            // 2-word phrases
            for ( $i = 0; $i < count( $tw ) - 1; $i++ ) {
                $anchors[] = $tw[$i] . ' ' . $tw[$i+1];
            }

            // ── STRATEGY 3: Slug-derived phrases ──
            $sw = array_values( $slug_words );
            for ( $i = 0; $i < count( $sw ) - 1; $i++ ) {
                $anchors[] = $sw[$i] . ' ' . $sw[$i+1];
            }

            // ── STRATEGY 4: Single long keywords from title ──
            foreach ( $tw as $w ) {
                if ( strlen( $w ) >= 7 ) $anchors[] = $w;
            }

            // Try each anchor candidate
            $anchors = array_unique( $anchors );
            foreach ( $anchors as $anchor ) {
                if ( $injected >= $max ) break;
                $anchor = trim( $anchor );
                if ( strlen( $anchor ) < 5 ) continue;
                if ( stripos( $content, $anchor ) !== false ) {
                    if ( $safe_inject( $content, $anchor, $url ) ) {
                        $injected++;
                        error_log( 'DBA LATAM SEO inject: "' . $anchor . '" → ' . $url );
                        break;
                    }
                }
            }
        }

        error_log( 'DBA LATAM seo_inject_links: injected ' . $injected . ' SEO-optimized links' );
        return $content;
    }

    /**
     * BIDIRECTIONAL CROSS-LINKING between LATAM posts.
     *
     * 1. Find all existing LATAM posts
     * 2. Score by relevance (same country, same theme, same city)
     * 3. Pick top 2 related posts
     * 4. Inject link FROM new post → 2 related posts (inline anchor)
     * 5. Inject link FROM each related post → new post (inline anchor)
     *
     * Result: every LATAM post is connected to at least 2 others. NO ORPHANS.
     */
    private function crosslink_latam_posts( $new_id, $new_title, $country_key, $country_label, $city_name, $theme_key, $theme_label ) {
        global $wpdb;

        // Find all existing published LATAM posts (not the new one)
        $latam_posts = $wpdb->get_results( $wpdb->prepare(
            "SELECT p.ID, p.post_title, p.post_content
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_dba_latam_auto_created'
             WHERE p.post_status IN ('publish','draft','future')
             AND p.ID != %d
             ORDER BY p.post_date DESC
             LIMIT 50",
            $new_id
        ), ARRAY_A );

        if ( empty( $latam_posts ) ) {
            $this->progress( 10, '🔗 Cross-links', '⚠️ No other LATAM posts yet — this is the first' );
            error_log( 'DBA LATAM crosslink: 0 existing LATAM posts — skipping' );
            return 0;
        }

        // Score each existing post by relevance
        $scored = array();
        foreach ( $latam_posts as $lp ) {
            $lp_country = get_post_meta( $lp['ID'], '_dba_latam_country', true );
            $lp_theme   = get_post_meta( $lp['ID'], '_dba_latam_theme', true );
            $lp_city    = get_post_meta( $lp['ID'], '_dba_latam_city', true );

            $score = 1; // baseline: all LATAM posts are somewhat related
            if ( $lp_country === $country_key )                       $score += 5; // same country
            if ( $lp_theme === $theme_key )                           $score += 3; // same topic
            if ( strtolower( $lp_city ) === strtolower( $city_name ) ) $score += 2; // same city
            if ( $lp_country !== $country_key && $lp_theme === $theme_key ) $score += 2; // cross-country same topic

            $scored[] = array(
                'ID'      => intval( $lp['ID'] ),
                'title'   => $lp['post_title'],
                'content' => $lp['post_content'],
                'country' => $lp_country,
                'city'    => $lp_city,
                'theme'   => $lp_theme,
                'score'   => $score,
            );
        }

        usort( $scored, function( $a, $b ) { return $b['score'] - $a['score']; } );
        $related = array_slice( $scored, 0, 2 ); // top 2 most relevant

        $cross_count = 0;
        $new_url = get_permalink( $new_id );
        if ( ! $new_url ) {
            error_log( 'DBA LATAM crosslink: could not get permalink for new post #' . $new_id );
            return 0;
        }

        // ── A. Inject links INTO the NEW post → pointing to related posts ──
        $new_content = get_post_field( 'post_content', $new_id );
        $new_modified = false;

        foreach ( $related as $rel ) {
            $rel_url = get_permalink( $rel['ID'] );
            if ( ! $rel_url || strpos( $new_content, $rel_url ) !== false ) continue; // already linked

            $anchors = $this->find_anchor_phrases( $new_content, $rel['title'], $rel['city'], $rel['country'] );
            foreach ( $anchors as $anchor ) {
                if ( $this->inject_single_link( $new_content, $anchor, $rel_url ) ) {
                    $new_modified = true;
                    $cross_count++;
                    error_log( 'DBA LATAM crosslink: NEW #' . $new_id . ' → #' . $rel['ID'] . ' anchor="' . $anchor . '"' );
                    break; // one link per related post
                }
            }
        }

        if ( $new_modified ) {
            $wpdb->update(
                $wpdb->posts,
                array( 'post_content' => $new_content ),
                array( 'ID' => $new_id ),
                array( '%s' ),
                array( '%d' )
            );
            // Also update Divi backup
            update_post_meta( $new_id, '_et_pb_old_content', $new_content );
            clean_post_cache( $new_id );
        }

        // ── B. Inject links FROM related posts BACK → new post ──
        foreach ( $related as $rel ) {
            $rel_content = get_post_field( 'post_content', $rel['ID'] );
            if ( strpos( $rel_content, $new_url ) !== false ) continue; // already linked back

            $anchors = $this->find_anchor_phrases( $rel_content, $new_title, $city_name, $country_key );
            foreach ( $anchors as $anchor ) {
                if ( $this->inject_single_link( $rel_content, $anchor, $new_url ) ) {
                    $wpdb->update(
                        $wpdb->posts,
                        array( 'post_content' => $rel_content ),
                        array( 'ID' => $rel['ID'] ),
                        array( '%s' ),
                        array( '%d' )
                    );
                    update_post_meta( $rel['ID'], '_et_pb_old_content', $rel_content );
                    clean_post_cache( $rel['ID'] );
                    $cross_count++;
                    error_log( 'DBA LATAM crosslink: OLD #' . $rel['ID'] . ' → NEW #' . $new_id . ' anchor="' . $anchor . '"' );
                    break;
                }
            }
        }

        $this->progress( 10, '🔗 Cross-links done', '✓ ' . $cross_count . ' bidirectional links between LATAM posts' );
        error_log( 'DBA LATAM crosslink: ' . $cross_count . ' total links created' );
        return $cross_count;
    }

    /**
     * Build a list of candidate anchor phrases to search for in $content.
     * Tries specific → generic: city name, country name, topic keywords, generic investment terms.
     */
    private function find_anchor_phrases( $content, $title, $city, $country_key ) {
        $countries = DBA_LATAM_Registry::get_countries();
        $country_label = isset( $countries[ $country_key ]['label'] ) ? $countries[ $country_key ]['label'] : $country_key;

        $phrases = array();

        // From the target post title: extract 2-3 word phrases
        $title_words = array_filter( explode( ' ', $title ), function( $w ) { return strlen( $w ) > 3; } );
        $tw = array_values( $title_words );

        // 3-word phrases from title (best for SEO anchor text)
        for ( $i = 0; $i < count( $tw ) - 2; $i++ ) {
            $phrases[] = $tw[$i] . ' ' . $tw[$i+1] . ' ' . $tw[$i+2];
        }
        // 2-word phrases from title
        for ( $i = 0; $i < count( $tw ) - 1; $i++ ) {
            $phrases[] = $tw[$i] . ' ' . $tw[$i+1];
        }

        // City and country names
        if ( $city && strlen( $city ) > 2 )           $phrases[] = $city;
        if ( $country_label && strlen( $country_label ) > 2 ) $phrases[] = $country_label;

        // SEO-optimized investment terms commonly found in LATAM articles
        $generic = array(
            'foreign direct investment', 'business formation', 'company registration',
            'tax structuring', 'treaty advantages', 'real estate investment',
            'investor visa', 'residency program', 'free trade zone',
            'corporate account', 'regulatory environment', 'banking sector',
            'investment opportunities', 'international investors', 'foreign investors',
            'economic growth', 'tax incentives', 'special economic zone',
            'compliance requirements', 'tax compliance', 'business opportunities',
            'investment climate', 'market opportunities', 'corporate tax',
            'holding company', 'investment fund', 'capital markets',
            'financial services', 'legal framework', 'business registration',
        );
        foreach ( $generic as $g ) {
            if ( stripos( $content, $g ) !== false ) {
                $phrases[] = $g;
            }
        }

        // Deduplicate, filter too-short
        $seen = array();
        $unique = array();
        foreach ( $phrases as $p ) {
            $p = trim( $p );
            $k = strtolower( $p );
            if ( strlen( $p ) < 4 || isset( $seen[ $k ] ) ) continue;
            // Only include if actually found in content
            if ( stripos( $content, $p ) !== false ) {
                $seen[ $k ] = true;
                $unique[] = $p;
            }
        }

        return $unique;
    }

    /**
     * Flush ALL caches after post creation so homepage displays new LATAM post immediately.
     * Targets: WP Rocket, Divi, WP Super Cache, LiteSpeed, Cloudflare, WP object cache,
     * and WordPress transients.
     */
    private function flush_all_caches( $post_id = 0 ) {
        $flushed = array();

        // 1. WP Rocket — flush everything
        if ( function_exists( 'rocket_clean_domain' ) ) {
            rocket_clean_domain();
            $flushed[] = 'WP Rocket (full)';
        }
        if ( function_exists( 'rocket_clean_home' ) ) {
            rocket_clean_home();
            $flushed[] = 'WP Rocket (home)';
        }
        if ( $post_id && function_exists( 'rocket_clean_post' ) ) {
            rocket_clean_post( $post_id );
        }

        // 2. Divi — purge static CSS files
        if ( function_exists( 'et_core_page_resource_fallback' ) ) {
            et_core_page_resource_fallback( '', true );
            $flushed[] = 'Divi';
        }
        // Divi Builder cache
        if ( class_exists( 'ET_Core_PageResource' ) ) {
            ET_Core_PageResource::remove_static_resources( 'all', 'all' );
            $flushed[] = 'Divi resources';
        }

        // 3. WP Super Cache
        if ( function_exists( 'wp_cache_clear_cache' ) ) {
            wp_cache_clear_cache();
            $flushed[] = 'WP Super Cache';
        }

        // 4. LiteSpeed Cache
        if ( class_exists( 'LiteSpeed_Cache_API' ) && method_exists( 'LiteSpeed_Cache_API', 'purge_all' ) ) {
            LiteSpeed_Cache_API::purge_all();
            $flushed[] = 'LiteSpeed';
        }

        // 5. Perfmatters
        if ( function_exists( 'perfmatters_flush_cache' ) ) {
            perfmatters_flush_cache();
            $flushed[] = 'Perfmatters';
        }

        // 6. WordPress object cache + post cache
        wp_cache_flush();
        if ( $post_id ) clean_post_cache( $post_id );
        $flushed[] = 'WP object cache';

        // 7. Clear relevant transients
        global $wpdb;
        $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_et_%'" );
        $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_et_%'" );
        $flushed[] = 'Divi transients';

        // 8. Flush homepage specifically via direct URL purge
        $home_url = home_url( '/' );
        if ( function_exists( 'rocket_clean_files' ) ) {
            rocket_clean_files( array( $home_url ) );
        }

        error_log( 'DBA LATAM flush_all_caches: flushed — ' . implode( ', ', $flushed ) );
        $this->progress( 11, '🧹 Cache flushed', '✓ ' . implode( ', ', $flushed ) );
    }

    /**
     * Inject a single <a> link into HTML content for the first occurrence
     * of $anchor that is NOT already inside an <a> tag.
     * Uses tag-splitting approach (PHP PCRE does NOT support variable-width lookbehinds).
     * Modifies $content by reference. Returns true if link was injected.
     */
    private function inject_single_link( &$content, $anchor, $url ) {
        if ( strlen( $anchor ) < 3 ) return false;
        if ( strpos( $content, $url ) !== false ) return false; // URL already in content

        // Split content by <a...>...</a> tags — only modify text OUTSIDE links
        $parts = preg_split( '/(<a\s[^>]*>.*?<\/a>)/is', $content, -1, PREG_SPLIT_DELIM_CAPTURE );
        if ( $parts === false || count( $parts ) < 1 ) return false;

        $escaped = preg_quote( $anchor, '/' );
        $replaced = false;

        for ( $i = 0; $i < count( $parts ); $i++ ) {
            // Skip parts that are <a> tags (odd indices from split)
            if ( $i % 2 === 1 ) continue;

            // Only match inside <p> text, not inside HTML tags
            if ( preg_match( '/\b' . $escaped . '\b/i', $parts[$i] ) ) {
                $link_html = '<a href="' . esc_url( $url ) . '">$0</a>';
                $parts[$i] = preg_replace( '/\b(' . $escaped . ')\b/i', $link_html, $parts[$i], 1, $count );
                if ( $count > 0 ) {
                    $replaced = true;
                    break;
                }
            }
        }

        if ( $replaced ) {
            $content = implode( '', $parts );
            return true;
        }
        return false;
    }
}
