<?php
/**
 * DBA_EEurope_Scheduler
 *
 * Core generation engine for Eastern Europe GeoBot.
 * Sat/Sun 07:00 CET cron + manual "Generate Now" with live progress.
 * Default: Thu/Fri 09:00 CET (08:00 UTC) — avoids Sat/Sun congestion with Core/LATAM/GeoBot EU.
 *
 * ANTI-HALLUCINATION FORMULA (Best-of-Breed from LATAM + LuxStrat):
 * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 * 1. VERIFIED DATA INJECTION: Corporate formation data (legal form, CIT rate, min capital)
 *    from the CEE Corporate Comparison spreadsheet is injected directly into the GPT prompt.
 *    GPT cannot hallucinate what we explicitly provide as ground truth.
 *
 * 2. GOVERNMENT URL REGISTRY: Verified gov domains are provided to GPT as context.
 *    GPT is instructed to NEVER fabricate .gov/.eu URLs — only Damalion links allowed.
 *
 * 3. ZERO SOURCE POLICY: GPT must state facts directly without attribution.
 *    "CIT is 9%" not "According to the Ministry of Finance, CIT is 9%".
 *    This eliminates hallucinated source names entirely.
 *
 * 4. POST-GENERATION VALIDATION: DBA_EEurope_Validator checks every article for:
 *    - Slop phrases (AI filler)
 *    - Placeholders (unfilled templates)
 *    - Fake institutions (hallucinated org names)
 *    - Fabricated URLs (only damalion.com allowed)
 *    - Corporate data mismatches (wrong legal form for country)
 *    - Style violations (passive voice, sentence length, transitions)
 *
 * 5. SELF-LEARNING: DBA_EEurope_Corrections reads the 5 latest editor-corrected posts
 *    and feeds writing patterns back into the prompt. If the editor fixes a hallucination,
 *    GPT learns to avoid it in future articles.
 *
 * 6. DENSITY CONTROL (Google Anti-Spam):
 *    - City name max 3x in article
 *    - Country name max 4x in article
 *    - No keyword stuffing — pronouns and synonyms enforced
 *    - Natural search-query-style titles (not "Regulatory Environment & Compliance in X: Analysis")
 *
 * 7. RETRY WITH FEEDBACK: If validation fails, the specific errors are fed back to GPT
 *    as correction instructions. Up to 2 retries before aborting.
 *
 * @since 3.0.0
 */

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

class DBA_EEurope_Scheduler {

    const CRON_HOOK    = 'dba_eeurope_daily_run';
    const LOCK_KEY     = 'dba_eeurope_running';
    const LOCK_TTL     = 900;
    const PROGRESS_KEY = 'dba_eeurope_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_eeurope_progress',         array( $this, 'ajax_progress' ) );
        add_action( 'wp_ajax_dba_eeurope_generate_now',     array( $this, 'ajax_generate_now' ) );
        add_action( 'wp_ajax_dba_eeurope_reset_rotation',   array( $this, 'ajax_reset_rotation' ) );
        add_action( 'wp_ajax_dba_eeurope_cleanup_counter',  array( $this, 'ajax_cleanup_counter' ) );
        add_action( 'wp_ajax_dba_eeurope_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 ── */

    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 ── */

    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 );

        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' );

        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_EEurope_Registry::reset_rotation();
        wp_send_json_success( array( 'message' => 'Rotation reset. Next: Poland.' ) );
    }

    public function ajax_cleanup_counter() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error();
        global $wpdb;
        $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_eeurope_auto_created', '_dba_auto_created')" );
        $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_eeurope_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 EEurope 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;

        $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_eeurope_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 EEurope 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'];

            $country_key = get_post_meta( $pid, '_dba_eeurope_country', true );
            $city_name   = get_post_meta( $pid, '_dba_eeurope_city', true );
            $theme_key   = get_post_meta( $pid, '_dba_eeurope_theme', true );

            $countries = DBA_EEurope_Registry::get_countries();
            $country_label = isset( $countries[ $country_key ]['label'] ) ? $countries[ $country_key ]['label'] : $country_key;

            $existing_links = substr_count( $content, '<a href=' ) + substr_count( $content, '<a  href=' );
            if ( $existing_links >= 6 ) {
                $log[] = "⏭️ #{$pid} \"{$title}\" — already has {$existing_links} links, skipping";
                continue;
            }

            $scanned_links = $this->fallback_internal_links( $country_label, $city_name ?: '', '', 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->manual_inject_links( $content, $pillar_url, $scanned_links );

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

                $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 ) {
                    $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";
            }
        }

        $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 ── */

    const ALL_DAYS = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' );

    public static function activate() {
        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_eeurope_settings', array() );
        $posting_days = ! empty( $settings['posting_days'] ) ? (array) $settings['posting_days'] : array( 'thursday', 'friday' );
        $cron_hour    = isset( $settings['cron_hour'] ) ? intval( $settings['cron_hour'] ) : 9;
        $cron_time    = sprintf( '%02d:00', max( 0, min( 23, $cron_hour ) ) );
        $tz           = new DateTimeZone( 'Europe/Luxembourg' );

        foreach ( self::ALL_DAYS as $d ) {
            if ( ! in_array( $d, $posting_days, true ) ) {
                wp_clear_scheduled_hook( self::CRON_HOOK, array( $d ) );
            }
        }

        $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 EEurope activate: ' . implode( ' | ', $log ) );
    }

    public static function reschedule() {
        foreach ( self::ALL_DAYS as $day ) {
            wp_clear_scheduled_hook( self::CRON_HOOK, array( $day ) );
        }
        wp_clear_scheduled_hook( self::CRON_HOOK );
        self::activate();
    }

    public static function deactivate() {
        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_eeurope_settings', array() );
        if ( ! $this->is_manual && empty( $settings['enabled'] ) ) return;

        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 EEurope: Cron for {$day} fired on {$actual_day} — skipping." );
                    return;
                }
                $active_days = ! empty( $settings['posting_days'] ) ? (array) $settings['posting_days'] : array( 'thursday', 'friday' );
                if ( ! in_array( $day, $active_days, true ) ) {
                    error_log( "DBA EEurope: Cron for {$day} fired but {$day} is no longer a posting day — skipping." );
                    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' );

        if ( class_exists( 'DBA_Conflict_Center' ) && ! DBA_Conflict_Center::acquire( 'eeurope', self::CRON_HOOK ) ) {
            $this->progress( 0, '⏳ Deferred', 'Another bot was running. Will retry in 10 min.', 'deferred' );
            error_log( 'DBA EEurope: 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 EEurope Fatal: ' . $e->getMessage() );
            $this->progress( 0, '❌ Fatal error', $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(), 'error' );
        }

        if ( class_exists( 'DBA_Conflict_Center' ) ) DBA_Conflict_Center::release( 'eeurope' );
        delete_transient( self::LOCK_KEY );
    }

    /* ══════════════════════════════════════════════════════════════
     *  GENERATE — batch runner
     * ══════════════════════════════════════════════════════════════ */

    private function do_generate_batch() {
        if ( $this->is_manual ) {
            $this->do_generate_one();
            return;
        }

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

        $settings      = get_option( 'dba_eeurope_settings', array() );
        $posts_per_run = max( 1, absint( $settings['posts_per_run'] ?? 3 ) );
        $posts_per_run = min( $posts_per_run, count( $enabled ) );

        $cooldown_days = max( 0, absint( $settings['cooldown_days'] ?? 5 ) );
        $recent        = $cooldown_days > 0 ? $this->get_recent_eeurope_usage( $cooldown_days ) : array( 'countries' => array(), 'cities' => array() );

        $used_countries = array();
        $used_cities    = array();

        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 ) {
                $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_cities = $recent['cities'] + $used_cities;
            $this->do_generate_one( $country_key, $avoid_cities );

            $last_city = get_option( 'dba_eeurope_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 ONE POST
     * ══════════════════════════════════════════════════════════════ */

    private function do_generate_one( $forced_country_key = null, $avoid_cities = array() ) {
        $settings = get_option( 'dba_eeurope_settings', array() );

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

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

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

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

        $config = DBA_EEurope_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'] );

        update_option( 'dba_eeurope_last_city_generated', $city['name'], false );

        // ── 3. Theme ──
        $theme = $this->get_next_theme( $country_key );
        $this->progress( 3, '📋 Step 3 — ' . $theme['label'], '✓ Theme selected' . ( ! empty( $theme['allow_comparison'] ) ? ' (comparison mode)' : ' (single-country)' ) );

        // ── 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', 'eeurope-investment', 'category' );
            if ( $cat ) {
                $category_id = $cat->term_id;
            } else {
                $r = wp_insert_term( 'Eastern Europe Investment', 'category', array( 'slug' => 'eeurope-investment' ) );
                if ( ! is_wp_error( $r ) ) {
                    $category_id = $r['term_id'];
                    $settings['category_id'] = $category_id;
                    update_option( 'dba_eeurope_settings', $settings );
                }
            }
        }

        $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';
        $this->progress( 4, '📁 Step 4 — ' . $cat_name, '✓ 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 = $config['label'] . ' — ' . $city['name'] . ' — ' . $theme['label'];

        // ── Check custom title queue (country-aware) ──
        $custom_title_override = '';
        $queue = get_option( 'dba_eeurope_custom_title_queue', array() );
        if ( ! empty( $queue ) ) {
            $matched_idx = null;

            foreach ( $queue as $idx => $item ) {
                $item_title   = trim( $item['title'] ?? '' );
                $item_country = trim( $item['country'] ?? '' ); // NEW: country field

                if ( empty( $item_title ) ) continue;

                if ( empty( $item_country ) || $item_country === 'any' ) {
                    // Legacy items with no country — match ANY, but warn
                    // Check if the title itself mentions a DIFFERENT country
                    $title_mentions_wrong = false;
                    $all_labels = array( 'Poland','Czech Republic','Serbia','Croatia','Slovenia','Slovakia','Hungary','Romania','Bulgaria','Estonia','Latvia','Lithuania' );
                    foreach ( $all_labels as $lbl ) {
                        if ( strcasecmp( $lbl, $config['label'] ) === 0 ) continue;
                        if ( stripos( $item_title, $lbl ) !== false ) {
                            $title_mentions_wrong = true;
                            error_log( 'DBA EEurope: Queue title "' . $item_title . '" mentions ' . $lbl . ' but rotation is ' . $config['label'] . ' — SKIPPING (will wait for correct country)' );
                            break;
                        }
                    }
                    if ( ! $title_mentions_wrong ) {
                        $matched_idx = $idx;
                        break;
                    }
                } elseif ( sanitize_key( $item_country ) === $country_key ) {
                    // Country matches — use this title
                    $matched_idx = $idx;
                    break;
                }
                // else: country doesn't match — skip, leave in queue for later
            }

            if ( $matched_idx !== null ) {
                $custom_title_override = trim( $queue[ $matched_idx ]['title'] );
                unset( $queue[ $matched_idx ] );
                $queue = array_values( $queue ); // Re-index
                update_option( 'dba_eeurope_custom_title_queue', $queue );
                if ( $custom_title_override ) {
                    $this->progress( 6, '✏️ Custom title', '→ ' . $custom_title_override . ' (matched ' . $config['label'] . ')' );
                }
            } else {
                $this->progress( 6, 'ℹ️ Queue skipped', count( $queue ) . ' title(s) in queue but none match ' . $config['label'] . ' — using auto-generated title' );
            }
        }

        // Comparison mode: DB override (your choice) → hardcoded default fallback
        $comp_overrides = get_option( 'dba_eeurope_theme_comparison', array() );
        $theme_key = $theme['key'] ?? '';
        if ( isset( $comp_overrides[ $theme_key ] ) ) {
            $allow_comparison = (bool) $comp_overrides[ $theme_key ];
        } else {
            $allow_comparison = ! empty( $theme['allow_comparison'] );
        }
        // Global override from settings
        $comp_override = $settings['comparison_mode'] ?? 'auto'; // auto | always | never
        if ( $comp_override === 'always' ) $allow_comparison = true;
        elseif ( $comp_override === 'never' ) $allow_comparison = false;

        $system  = $this->build_system_prompt( $config, $city, $country_key, $allow_comparison );
        $user_prompt = $custom_title_override
            ? $this->build_prompt_from_title( $custom_title_override, $config, $city, $word_min, $word_max, $news, $country_key, $allow_comparison )
            : $this->build_prompt( $config, $city, $theme, $word_min, $word_max, $news, $country_key, $allow_comparison );

        // ── Self-learning from editor corrections ──
        $corrections_block = '';
        if ( class_exists( 'DBA_EEurope_Corrections' ) ) {
            $corrections_block = DBA_EEurope_Corrections::build_learning_prompt( $country_key );
            if ( $corrections_block ) {
                $user_prompt .= $corrections_block;
            }
        }

        // ── 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, 'eeurope' );
            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( 'eeurope' );
            if ( $tis_context ) {
                $system .= "\n\n" . $tis_context;
            }
        }

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

        // ── API call with retry-on-validation-failure ──
        $max_attempts = 3;
        $title = '';
        $content = '';
        $meta = '';
        $kw = '';
        $feedback = array();

        for ( $attempt = 1; $attempt <= $max_attempts; $attempt++ ) {
            $effective_prompt = $user_prompt;
            if ( ! empty( $feedback ) ) {
                $effective_prompt .= "\n\n⚠️ FIX THESE ISSUES FROM THE PREVIOUS ATTEMPT:\n- " . implode( "\n- ", $feedback );
            }

            $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' => $effective_prompt ),
                    ),
                    'temperature' => 0.7,
                    'max_tokens'  => 6000,
                ) ),
            ) );

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

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

            $body = json_decode( wp_remote_retrieve_body( $api_resp ), true );
            $raw  = $body['choices'][0]['message']['content'] ?? '';

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

            // Parse XML tags
            preg_match( '/<title>(.*?)<\/title>/s', $raw, $tm );
            preg_match( '/<meta_description>(.*?)<\/meta_description>/s', $raw, $mm );
            preg_match( '/<focus_keyphrase>(.*?)<\/focus_keyphrase>/s', $raw, $km );

            $title   = isset( $tm[1] ) ? trim( wp_strip_all_tags( $tm[1] ) ) : '';

            // Override with custom title if queued
            if ( $custom_title_override ) {
                $title = $custom_title_override;
            }

            $meta    = isset( $mm[1] ) ? trim( wp_strip_all_tags( $mm[1] ) ) : '';
            $kw      = isset( $km[1] ) ? trim( wp_strip_all_tags( $km[1] ) ) : '';

            // Extract HTML body (everything after the last XML tag)
            $content = $raw;
            $content = preg_replace( '/<title>.*?<\/title>/s', '', $content );
            $content = preg_replace( '/<meta_description>.*?<\/meta_description>/s', '', $content );
            $content = preg_replace( '/<focus_keyphrase>.*?<\/focus_keyphrase>/s', '', $content );
            $content = trim( $content );

            // ── VALIDATION ──
            $validation = DBA_EEurope_Validator::validate( $country_key, array(
                'title'            => $title,
                'content'          => $content,
                'meta_description' => $meta,
                'focus_keyphrase'  => $kw,
            ), $config );

            if ( $validation['valid'] ) {
                // ── COUNTRY-MISMATCH CHECK: Only reject if comparison mode is OFF ──
                if ( ! $allow_comparison ) {
                    $wrong_countries = array();
                    $all_eeurope_labels = array( 'Poland','Czech Republic','Serbia','Croatia','Slovenia','Slovakia','Hungary','Romania','Bulgaria','Estonia','Latvia','Lithuania' );
                    foreach ( $all_eeurope_labels as $other_label ) {
                        if ( strcasecmp( $other_label, $config['label'] ) === 0 ) continue;
                        if ( stripos( $title, $other_label ) !== false ) $wrong_countries[] = $other_label . ' in title';
                        if ( stripos( $kw, $other_label ) !== false )    $wrong_countries[] = $other_label . ' in keyphrase';
                        if ( stripos( $meta, $other_label ) !== false )  $wrong_countries[] = $other_label . ' in meta';
                    }
                    if ( ! empty( $wrong_countries ) ) {
                        $feedback = array(
                            'WRONG COUNTRY DETECTED: This article must be about ' . strtoupper( $config['label'] ) . ' ONLY.',
                            'Found references to: ' . implode( ', ', $wrong_countries ),
                            'Remove ALL mentions of other countries from title, meta_description, and focus_keyphrase.',
                            'The focus_keyphrase MUST contain "' . $config['label'] . '" or "' . $city['name'] . '".',
                        );
                        error_log( 'DBA EEurope: COUNTRY MISMATCH in attempt ' . $attempt . ' — ' . implode( ' | ', $wrong_countries ) );
                        $this->progress( 6, '⚠️ Wrong country in output — retry ' . $attempt, implode( ', ', $wrong_countries ) );
                        continue;
                    }
                }
                // Also run style check
                $style_errors = DBA_EEurope_Validator::validate_style( array( 'content' => $content ) );
                if ( empty( $style_errors ) ) {
                    break; // All good!
                }
                $feedback = $style_errors;
                $this->progress( 6, '🤖 Step 6 — Retry ' . $attempt . '/' . $max_attempts, 'Style issues: ' . implode( '; ', $style_errors ) );
            } else {
                $feedback = $validation['errors'];
                $this->progress( 6, '🤖 Step 6 — Retry ' . $attempt . '/' . $max_attempts, 'Validation: ' . implode( '; ', $validation['errors'] ) );
            }
        }

        if ( empty( $title ) || empty( $content ) ) {
            $this->progress( 6, '❌ Generation failed', 'No valid content after ' . $max_attempts . ' attempts.', 'error' );
            return;
        }

        // ── Dedup title check ──
        if ( DBA_EEurope_Dedupe::title_is_duplicate( $title ) ) {
            $this->progress( 6, '⚠️ Duplicate title', 'Title too similar to recent post. Aborting.', 'error' );
            return;
        }

        $this->progress( 6, '🤖 Step 6 — Article ready', '✓ ' . str_word_count( wp_strip_all_tags( $content ) ) . ' words' );

        // ── 7. Density control (Google anti-spam) ──
        $this->progress( 7, '🔍 Step 7 — Density check...', 'City/country keyword frequency' );

        $density_result = DBA_EEurope_Validator::enforce_density( $content, $city['name'], $config['label'] );

        $total_wc = str_word_count( wp_strip_all_tags( $content ) );
        $final_plain = strtolower( wp_strip_all_tags( $content ) );
        $final_city  = substr_count( $final_plain, strtolower( $city['name'] ) );
        $final_country = substr_count( $final_plain, strtolower( $config['label'] ) );
        $this->progress( 7, '🔍 Step 7 — Density OK',
            '✓ ' . $city['name'] . ': ' . $final_city . 'x, ' . $config['label'] . ': ' . $final_country . 'x (in ' . $total_wc . ' words)' );

        // ── 7.5 Append CTA ──
        if ( empty( $settings['disable_cta'] ) ) {
            $cta = $this->build_cta( $config, $settings );
            $content .= "\n" . $cta;
        }

        // ── 7.8 Plagiarism / Uniqueness — 5-Gate System ──
        // Gate A: Internal Duplicate Detection (Jaccard 3-gram shingling)
        $this->progress( 7, '🛡️ Gate A — Internal duplicate check...', 'Jaccard shingling against all existing posts' );
        if ( class_exists( 'DBA_Plagiarism' ) ) {
            $plagiarism = DBA_Plagiarism::get_instance()->check( $content, 0 );
            if ( ! $plagiarism['pass'] ) {
                $plag_detail = implode( ' | ', $plagiarism['details'] ?? array() );
                error_log( 'DBA EEurope: GATE A BLOCKED — score ' . ( $plagiarism['score'] ?? '?' ) . '% — ' . $plag_detail );
                $this->progress( 7, '🛡️ Gate A — REJECTED', '⛔ Score: ' . ( $plagiarism['score'] ?? '?' ) . '% — ' . $plag_detail, 'error' );
                return;
            }
            $this->progress( 7, '🛡️ Gate A — Passed',
                '✓ Uniqueness: ' . $plagiarism['score'] . '% (' . ( $plagiarism['method'] ?? 'internal' ) . ')' );
        } else {
            $this->progress( 7, '🛡️ Gate A — Skipped', '⚠️ DBA_Plagiarism class not loaded' );
        }

        // Gate B: Sentence-Level Uniqueness (compare individual sentences against recent EEurope posts)
        $this->progress( 7, '🛡️ Gate B — Sentence-level check...', 'Comparing sentences against last 30 EEurope posts' );
        $gate_b = $this->check_sentence_uniqueness( $content );
        if ( ! $gate_b['pass'] ) {
            error_log( 'DBA EEurope: GATE B BLOCKED — ' . $gate_b['detail'] );
            $this->progress( 7, '🛡️ Gate B — REJECTED', '⛔ ' . $gate_b['detail'], 'error' );
            return;
        }
        $this->progress( 7, '🛡️ Gate B — Passed', '✓ ' . $gate_b['detail'] );

        // ── 8. Image ──
        $this->progress( 8, '🖼️ Step 8 — Generating image...', 'DALL-E: ' . $city['name'] );
        $media_id = 0;
        if ( class_exists( 'DBA_Image_Generator' ) ) {
            try {
                $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( 'eeurope-' . $country_key . '-' . sanitize_title( $city['name'] ) . '.jpg' );
                $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 EEurope image: ' . $media_id->get_error_message() );
                    $media_id = 0;
                }
            } catch ( \Throwable $e ) {
                error_log( 'DBA EEurope 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 Internal Linking ──
        // ── 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...', 'Finding relevant internal 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/';

            $links = $this->fallback_internal_links( $config['label'], $city['name'], $theme['label'], 8 );

            if ( ! empty( $links ) ) {
                $content = $this->manual_inject_links( $content, $pillar_url, $links );
                $linked = true;
            }

            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( $links ) . ' links' );
        } catch ( \Throwable $e ) {
            $this->progress( 8, '🔗 Links error', '⚠️ ' . $e->getMessage() );
        }

        // ── 9. Create post ──
        $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';

        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' );

        $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 ) ) {
            foreach ( $saved_hooks as $tag => $filter_obj ) { $wp_filter[ $tag ] = $filter_obj; }
            $this->progress( 9, '❌ Post failed', $post_id->get_error_message(), 'error' );
            return;
        }

        // Post meta
        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_eeurope_auto_created', current_time( 'mysql' ) );
        update_post_meta( $post_id, '_dba_theme',                'eeurope_' . $country_key );
        update_post_meta( $post_id, '_dba_language',             'en' );
        update_post_meta( $post_id, '_dba_eeurope_country',      $country_key );
        update_post_meta( $post_id, '_dba_eeurope_city',         $city['name'] );
        update_post_meta( $post_id, '_dba_eeurope_region',       $city['region'] );
        update_post_meta( $post_id, '_dba_eeurope_theme',        $theme['key'] );
        update_post_meta( $post_id, '_dba_eeurope_comparison',   $allow_comparison ? '1' : '0' );
        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'] );

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

        // DIVI META
        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' );

        if ( $media_id ) set_post_thumbnail( $post_id, $media_id );

        if ( $pstatus === 'publish' ) {
            wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );
        }

        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 );
        DBA_EEurope_Dedupe::mark_used_today( $country_key );
        $this->progress( 9, '💾 Step 9 — Post #' . $post_id, '✓ ' . ucfirst( $pstatus ) );

        // ── 10. Cross-link ──
        $this->progress( 10, '🔗 Step 10 — Cross-linking EEurope posts...', 'Finding related articles' );
        $cross_count = $this->crosslink_eeurope_posts( $post_id, $title, $country_key, $config['label'], $city['name'], $theme['key'], $theme['label'] );

        // ── 11. Flush caches ──
        $this->progress( 11, '🧹 Step 11 — Flushing caches...', '' );
        $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' => 'eeurope_' . $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 ? 'SEO links injected' : 'No links' ) . " | {$cross_count} cross-links";

        $this->progress( 12, '✅ Done', $summary, 'done' );

        // ── TIS: track directive progress ──
        if ( class_exists( 'DBA_TIS_Recommender' ) ) {
            DBA_TIS_Recommender::auto_track_progress( 'eeurope', $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;
        }
    }

    /* ══════════════════════════════════════════════════════════════
     *  SYSTEM PROMPT — anti-hallucination core
     * ══════════════════════════════════════════════════════════════ */

    private function build_system_prompt( $config, $city, $country_key, $allow_comparison = false ) {
        // Get verified corporate data
        $corp = DBA_EEurope_Cities::get_corporate_data();
        $cd = isset( $corp[ $country_key ] ) ? $corp[ $country_key ] : null;

        // Get EU status
        $eu_data = DBA_EEurope_Registry::get_eu_status();
        $eu = isset( $eu_data[ $country_key ] ) ? $eu_data[ $country_key ] : null;

        // Get gov portal
        $portals = DBA_EEurope_Cities::get_government_portals();
        $gov = isset( $portals[ $country_key ] ) ? $portals[ $country_key ] : '';

        // ── COUNTRY FOCUS (single vs comparison) ──
        if ( $allow_comparison ) {
            $system = '═══ PRIMARY FOCUS: ' . strtoupper( $config['label'] ) . ', city: ' . $city['name'] . ' ═══'
                . "\n"
                . 'This article\'s primary subject is ' . $config['label'] . '. '
                . 'You MAY reference other Eastern European countries for comparison or context, '
                . 'but ' . $config['label'] . ' must remain the MAIN focus (at least 70% of the article). '
                . 'The title and focus keyphrase MUST primarily reference ' . $config['label'] . ' or ' . $city['name'] . '.'
                . "\n\n";
        } else {
            $system = '═══ COUNTRY LOCK: ' . strtoupper( $config['label'] ) . ', city: ' . $city['name'] . ' ═══'
                . "\n"
                . 'This article is EXCLUSIVELY about ' . $config['label'] . '. '
                . 'Do NOT mention any other country by name in the title, meta description, or focus keyphrase. '
                . 'Brief factual references to the EU or regional context are acceptable in the body, '
                . 'but the article must be 100% focused on ' . $config['label'] . '.'
                . "\n\n";
        }

        $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: "CIT stands at 9%" — never attribute to any source. '
            . "\n"
            . '2. ANTI-KEYWORD-STUFFING (critical for SEO — Google penalizes this): '
            . '   - 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. '
            . '   - 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. '
            . "\n"
            . '4. Write authoritatively with specific data: tax rates, law names, registration timelines, costs. '
            . "\n"
            . '5. PROMOTE DAMALION SERVICES NATURALLY (2 mentions in body — not counting CTA): '
            . '   Damalion is the consulting firm publishing this article. '
            . '   When describing complex processes, naturally weave in that Damalion facilitates these tasks. '
            . '   DO NOT make every sentence about Damalion. '
            . "\n"
            . '6. ZERO EXTERNAL LINKS: Do NOT include any URLs in the article body. '
            . '   No government links, no Wikipedia, no external sources. '
            . '   Only Damalion URLs will be added programmatically. '
            . "\n"
            . '7. ACTIVE VOICE: At least 90% of sentences must use active voice. '
            . "\n"
            . '8. TRANSITION WORDS: At least 30% of sentences must contain a transition word '
            . '   (however, therefore, moreover, in contrast, for example, notably, etc.). ';

        // ── VERIFIED DATA INJECTION (anti-hallucination anchor) ──
        if ( $cd ) {
            $system .= "\n\n"
                . 'VERIFIED CORPORATE DATA FOR ' . strtoupper( $config['label'] ) . ' (USE THESE EXACT FACTS — do not invent different ones): '
                . "\n"
                . '- Standard LLC equivalent: ' . $cd['legal_form'] . "\n"
                . '- Minimum share capital: ' . $cd['min_capital'] . "\n"
                . '- Shareholder liability: ' . $cd['liability'] . "\n"
                . '- Corporate income tax (CIT): ' . $cd['cit_rate'] . "\n"
                . '- Management structure: ' . $cd['management'];
        }

        if ( $eu ) {
            $system .= "\n\n"
                . 'EU STATUS: '
                . ( $eu['eu_member'] ? 'EU member since ' . $eu['eu_year'] : 'EU candidate state' )
                . '. Currency: ' . $eu['currency']
                . ( $eu['eurozone'] ? ' (Eurozone since ' . ( $eu['eurozone_year'] ?? '' ) . ')' : ' (not in Eurozone)' )
                . '.';
        }

        if ( $gov ) {
            $system .= "\n"
                . 'Government portal: ' . $gov . ' (for your knowledge only — do NOT link to it).';
        }

        // ── Banned Words ──
        $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\n" . '9. BANNED VOCABULARY — These words/phrases must NEVER appear: '
                . implode( ', ', $banned_words ) . '. Use synonyms instead. ';
        }

        $kw_rule = $allow_comparison
            ? 'MUST primarily reference ' . $config['label'] . ' or ' . $city['name'] . ' (may include comparison aspect).'
            : 'MUST include "' . $config['label'] . '" or "' . $city['name'] . '".';

        $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 search "how to open a business in ' . $config['label'] . '", "' . $config['label'] . ' corporate tax rate", "invest in ' . $city['name'] . '". '
            . 'Write the title as a natural search query or answer to one. '
            . ( $allow_comparison ? '' : 'CRITICAL: The title MUST mention ' . $config['label'] . ' or ' . $city['name'] . ' — NEVER any other country as the main subject. ' )
            . 'Keep it under 60 characters if possible. No colons followed by generic labels.</title> '
            . '<meta_description>Max 135 chars for Yoast SEO. MUST reference ' . $config['label'] . ' or ' . $city['name'] . '.</meta_description> '
            . '<focus_keyphrase>2-4 words matching a real Google search. ' . $kw_rule . '</focus_keyphrase> '
            . 'Then HTML body using <h2>, <h3>, <p>, <ul>. No <h1>, no markdown, no title repeated in body.';

        return $system;
    }

    /* ── Prompt Builder ── */

    private function build_prompt( $config, $city, $theme, $wmin, $wmax, $news, $country_key, $allow_comparison = false ) {
        $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 ) ) );
        }
        $theme_label = preg_replace( '/^[\s&,;]+|[\s&,;]+$/', '', $theme_label );
        if ( empty( $theme_label ) ) $theme_label = $theme['label'];

        $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";
        }

        $country_rule = $allow_comparison
            ? "- PRIMARY FOCUS is {$config['label']} / {$city['name']}. You MAY compare with other Eastern European countries for context, but {$config['label']} must be the main subject (70%+ of content).\n"
            : "- THIS ARTICLE IS ABOUT " . strtoupper( $config['label'] ) . " AND " . strtoupper( $city['name'] ) . " ONLY.\n"
            . "- NEVER mention any other country by name (no Czech Republic, no Hungary, no Estonia, etc.).\n";

        $kw_rule = $allow_comparison
            ? "- The title and focus keyphrase must primarily reference \"{$config['label']}\" or \"{$city['name']}\".\n"
            : "- The title, meta description, and focus keyphrase MUST include \"{$config['label']}\" or \"{$city['name']}\".\n";

        $p .= "CRITICAL REMINDERS:\n"
            . $country_rule
            . $kw_rule
            . "- 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 include any external URLs. Only Damalion links will be added later.\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 about {$config['label']}</title>\n"
            . "<meta_description>Max 135 chars, MUST include {$city['name']} or {$config['label']}</meta_description>\n"
            . "<focus_keyphrase>2-4 words about {$config['label']}</focus_keyphrase>\n\n"
            . "Then HTML (<h2>,<h3>,<p>,<ul> — no <h1>, no markdown).";
        return $p;
    }

    /**
     * Build prompt when a custom title is provided by the user.
     * The title is fixed — GPT generates content to match it.
     */
    private function build_prompt_from_title( $custom_title, $config, $city, $wmin, $wmax, $news, $country_key, $allow_comparison = false ) {
        $p = "Write a {$wmin}–{$wmax} word article with EXACTLY this title:\n\n"
            . "\"{$custom_title}\"\n\n"
            . "Location context: {$city['name']} ({$city['region']}), {$config['label']}\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";
        }

        $country_rule = $allow_comparison
            ? "- PRIMARY FOCUS is {$config['label']}. You MAY compare with other countries for context.\n"
            : "- THIS ARTICLE IS ABOUT " . strtoupper( $config['label'] ) . " ONLY. NEVER mention any other country by name.\n";

        $p .= "CRITICAL REMINDERS:\n"
            . "- Use the EXACT title provided above — do NOT change it.\n"
            . $country_rule
            . "- 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"
            . "- Write like a knowledgeable journalist, not a template.\n"
            . "- NEVER include any external URLs. Only Damalion links will be added later.\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>{$custom_title}</title>\n"
            . "<meta_description>Max 135 chars, MUST reference {$config['label']} or {$city['name']}</meta_description>\n"
            . "<focus_keyphrase>2-4 words about {$config['label']}</focus_keyphrase>\n\n"
            . "Then HTML (<h2>,<h3>,<p>,<ul> — no <h1>, no markdown).";
        return $p;
    }

    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_EEurope_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_eeurope_city_idx_' . $key;
        $i = intval( get_option( $ik, 0 ) );
        if ( $i >= count( $cities ) ) $i = 0;
        update_option( $ik, $i + 1, false );
        return $cities[$i];
    }

    private function get_next_city_avoiding( $key, $cfg, $avoid = array() ) {
        $avoid = (array) $avoid;
        $cities = get_option( $cfg['communes_option'], array() );
        if ( empty( $cities ) ) $cities = DBA_EEurope_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_eeurope_city_idx_' . $key;
        $i  = intval( get_option( $ik, 0 ) );
        $n  = count( $cities );
        if ( $i >= $n ) $i = 0;

        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;
            update_option( $ik, $pos + 1, false );
            return $city;
        }

        return $this->get_next_city( $key, $cfg );
    }

    /* ── Theme Rotation (adapted for Eastern Europe context) ── */

    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. Use the verified legal form and capital data provided.' ),
            array( 'key'=>'fdi_opportunities',    'label'=>'Foreign Direct Investment Opportunities',    'prompt_context'=>'FDI landscape, EU single market access, incentives, sectors attracting foreign capital.' ),
            array( 'key'=>'real_estate',          'label'=>'Real Estate Investment',                     'prompt_context'=>'Real estate for international buyers. Rules, restrictions, property rights for EU and non-EU nationals.' ),
            array( 'key'=>'tax_structuring',      'label'=>'Tax Structuring & Treaty Advantages',       'prompt_context'=>'Tax rates (use verified CIT data), EU directives, double tax treaties, transfer pricing.' ),
            array( 'key'=>'banking',              'label'=>'Banking & Corporate Account Opening',       'prompt_context'=>'Opening corporate accounts as foreign investor. KYC, AML requirements, digital banking options.' ),
            array( 'key'=>'regulatory',           'label'=>'Regulatory Environment & Compliance',       'prompt_context'=>'Regulatory bodies, EU compliance, GDPR, AML directives, financial reporting.' ),
            array( 'key'=>'eu_single_market',     'label'=>'EU Single Market Access',                   'prompt_context'=>'Benefits of EU membership for investors. Free movement, single market, CE marking, EU funding.' ),
            array( 'key'=>'digital_economy',      'label'=>'Digital Economy & Tech Investment',          'prompt_context'=>'Tech ecosystem, digital nomad visas, e-residency programs, startup incentives, IT talent pool.' ),
            array( 'key'=>'sector_opportunities', 'label'=>'Sector-Specific Opportunities',             'prompt_context'=>'Manufacturing, automotive, fintech, agriculture, energy, shared services, logistics.' ),
        );
    }

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

    /* ── News Research ── */

    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 ) );
    }

    /* ── Homepage Category Mapping ── */

    private function get_homepage_categories( $theme_key, $country_key, $city_name ) {
        $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',
            'eu_single_market'    => 'foreign-direct-investments',
            'digital_economy'     => '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 ( 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; }
            }
        }

        return $ids;
    }

    /* ── Internal Links (fallback) ── */

    private function fallback_internal_links( $country, $city, $topic, $max = 8 ) {
        global $wpdb;
        $links = array();

        $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();

        $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',
                   'eastern europe', 'central europe', 'doing business', 'company formation', 'holding',
                   'fund', 'compliance', 'structuring', 'private equity', 'asset management', 'eu', 'european' )
        ) ), function( $w ) { return strlen( $w ) > 2; } );

        $ee_countries = array( 'poland', 'czech', 'serbia', 'croatia', 'slovenia', 'slovakia', 'hungary', 'romania', 'bulgaria', 'estonia', 'latvia', 'lithuania' );

        $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;
                }
            }
            if ( strpos( $t, 'doing business in' ) !== false ) $score += 8;
            if ( strpos( $t, strtolower( $country ) ) !== false ) $score += 5;
            if ( $row['post_type'] === 'page' ) $score += 2;

            // Penalty for OTHER EE countries
            foreach ( $ee_countries as $ec ) {
                if ( strpos( $t, $ec ) !== false && strpos( strtolower( $country ), $ec ) === false ) {
                    $score -= 15;
                }
            }

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

        usort( $scored, function( $a, $b ) { return $b['score'] - $a['score']; } );
        return array_slice( $scored, 0, $max );
    }

    /* ── Manual link injection ── */

    private function manual_inject_links( $content, $pillar_url, $links ) {
        if ( empty( $links ) ) return $content;
        $injected = 0;
        $max = 6;

        $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;
            $esc = preg_quote( $needle, '/' );
            for ( $i = 0; $i < count( $parts ); $i++ ) {
                if ( $i % 2 === 1 ) continue;
                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;
        };

        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; }
                }
            }
        }

        foreach ( $links as $link ) {
            if ( $injected >= $max ) break;
            $url   = $link['url'] ?? '';
            $title = $link['title'] ?? '';
            if ( empty( $url ) || empty( $title ) || strpos( $content, $url ) !== false ) continue;

            $words = array_filter( explode( ' ', $title ), function( $w ) { return strlen( $w ) > 3; } );
            $words_arr = array_values( $words );
            $phrases = array();
            for ( $i = 0; $i < count( $words_arr ) - 2; $i++ ) $phrases[] = $words_arr[$i] . ' ' . $words_arr[$i+1] . ' ' . $words_arr[$i+2];
            for ( $i = 0; $i < count( $words_arr ) - 1; $i++ ) $phrases[] = $words_arr[$i] . ' ' . $words_arr[$i+1];
            foreach ( $words_arr as $w ) { if ( strlen( $w ) >= 6 ) $phrases[] = $w; }

            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; }
                }
            }
        }

        return $content;
    }

    /* ── Cross-linking ── */

    private function crosslink_eeurope_posts( $new_id, $title, $country_key, $country_label, $city, $theme_key, $theme_label ) {
        global $wpdb;
        $cross_count = 0;

        $related = $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_eeurope_auto_created'
             WHERE p.post_status = 'publish' AND p.ID != %d
             ORDER BY p.post_date DESC LIMIT 20",
            $new_id
        ), ARRAY_A );

        if ( empty( $related ) ) return 0;

        $new_url = get_permalink( $new_id );
        if ( ! $new_url ) return 0;

        $phrases = $this->find_anchor_phrases( '', $title, $city, $country_key );
        if ( empty( $phrases ) ) return 0;

        foreach ( $related as $rel ) {
            if ( $cross_count >= 3 ) break;
            if ( strpos( $rel['post_content'], $new_url ) !== false ) continue;
            if ( substr_count( $rel['post_content'], '<a href=' ) >= 10 ) continue;

            foreach ( $phrases as $anchor ) {
                if ( stripos( $rel['post_content'], $anchor ) !== false ) {
                    $updated = $rel['post_content'];
                    if ( $this->inject_single_link( $updated, $anchor, $new_url ) ) {
                        $wpdb->update( $wpdb->posts, array( 'post_content' => $updated ), array( 'ID' => $rel['ID'] ) );
                        update_post_meta( $rel['ID'], '_et_pb_old_content', $updated );
                        clean_post_cache( $rel['ID'] );
                        $cross_count++;
                        break;
                    }
                }
            }
        }

        return $cross_count;
    }

    private function find_anchor_phrases( $content, $title, $city, $country_key ) {
        $countries = DBA_EEurope_Registry::get_countries();
        $country_label = isset( $countries[ $country_key ]['label'] ) ? $countries[ $country_key ]['label'] : $country_key;

        $phrases = array();
        $title_words = array_filter( explode( ' ', $title ), function( $w ) { return strlen( $w ) > 3; } );
        $tw = array_values( $title_words );

        for ( $i = 0; $i < count( $tw ) - 2; $i++ ) $phrases[] = $tw[$i] . ' ' . $tw[$i+1] . ' ' . $tw[$i+2];
        for ( $i = 0; $i < count( $tw ) - 1; $i++ ) $phrases[] = $tw[$i] . ' ' . $tw[$i+1];

        if ( $city && strlen( $city ) > 2 ) $phrases[] = $city;
        if ( $country_label && strlen( $country_label ) > 2 ) $phrases[] = $country_label;

        $generic = array(
            'foreign direct investment', 'business formation', 'company registration',
            'tax structuring', 'treaty advantages', 'real estate investment',
            'investor visa', 'residency program', 'EU single market',
            'corporate account', 'regulatory environment', 'banking sector',
            'investment opportunities', 'international investors', 'foreign investors',
            'economic growth', 'tax incentives', 'digital economy', 'e-residency',
            'compliance requirements', 'tax compliance', 'business opportunities',
            'investment climate', 'market opportunities', 'corporate tax',
            'holding company', 'investment fund', 'capital markets',
            'financial services', 'legal framework', 'business registration',
        );

        return array_unique( array_filter( $phrases, function($p) { return strlen($p) > 3; } ) );
    }

    private function inject_single_link( &$content, $anchor, $url ) {
        if ( strlen( $anchor ) < 3 ) return false;
        if ( strpos( $content, $url ) !== false ) return false;

        $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++ ) {
            if ( $i % 2 === 1 ) continue;
            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;
    }

    /* ── Cache Flush ── */

    private function flush_all_caches( $post_id = 0 ) {
        $flushed = array();
        if ( function_exists( 'rocket_clean_domain' ) ) { rocket_clean_domain(); $flushed[] = 'WP Rocket'; }
        if ( function_exists( 'rocket_clean_home' ) ) { rocket_clean_home(); }
        if ( $post_id && function_exists( 'rocket_clean_post' ) ) rocket_clean_post( $post_id );
        if ( function_exists( 'et_core_page_resource_fallback' ) ) { et_core_page_resource_fallback( '', true ); $flushed[] = 'Divi'; }
        if ( class_exists( 'ET_Core_PageResource' ) ) ET_Core_PageResource::remove_static_resources( 'all', 'all' );
        if ( function_exists( 'wp_cache_clear_cache' ) ) { wp_cache_clear_cache(); $flushed[] = 'WP Super Cache'; }
        if ( class_exists( 'LiteSpeed_Cache_API' ) && method_exists( 'LiteSpeed_Cache_API', 'purge_all' ) ) { LiteSpeed_Cache_API::purge_all(); $flushed[] = 'LiteSpeed'; }
        wp_cache_flush();
        if ( $post_id ) clean_post_cache( $post_id );
        $flushed[] = 'WP object cache';
        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_%'" );
        error_log( 'DBA EEurope flush_all_caches: ' . implode( ', ', $flushed ) );
        $this->progress( 11, '🧹 Cache flushed', '✓ ' . implode( ', ', $flushed ) );
    }

    /* ── Helpers ── */

    private function select_next_country_for_batch( $enabled, $recent_countries, $used_countries ) {
        $state = get_option( 'dba_eeurope_rotation_state', array( 'index' => 0 ) );
        $start = intval( $state['index'] );
        if ( $start >= count( $enabled ) ) $start = 0;

        for ( $i = 0; $i < count( $enabled ); $i++ ) {
            $idx = ( $start + $i ) % count( $enabled );
            $key = $enabled[ $idx ];
            if ( isset( $recent_countries[ $key ] ) || isset( $used_countries[ $key ] ) ) continue;
            $state['index'] = $idx + 1;
            $state['last_run'] = current_time( 'mysql' );
            $state['last_country'] = $key;
            update_option( 'dba_eeurope_rotation_state', $state, false );
            return $key;
        }
        return false;
    }

    private function get_recent_eeurope_usage( $days, $ref_ts = null ) {
        global $wpdb;
        $days = max( 1, absint( $days ) );
        $ref_ts = $ref_ts ? (int) $ref_ts : current_time( 'timestamp' );

        $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 = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - ( $days * DAY_IN_SECONDS ) );
        }

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

        $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_eeurope_auto_created', '_dba_eeurope_country', '_dba_eeurope_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 );
    }

    public function get_recent_eeurope_usage_for_admin( $days, $ref_ts = null ) {
        return $this->get_recent_eeurope_usage( $days, $ref_ts );
    }

    /* ── Gate B: Sentence-Level Uniqueness ──
     * Extracts sentences (>30 chars) from new content, compares each against
     * the last 30 EEurope posts. If a sentence matches at 90%+ (similar_text),
     * it's flagged. If >15% of sentences are flagged → REJECT.
     */
    private function check_sentence_uniqueness( $new_content_html ) {
        global $wpdb;
        $result = array( 'pass' => true, 'detail' => '' );

        $new_plain = wp_strip_all_tags( $new_content_html );
        // Remove CTA boilerplate before comparing
        $new_plain = preg_replace( '/Damalion\s+(supports|helps|assists|accompagne).*$/si', '', $new_plain );

        $new_sentences = $this->extract_sentences( $new_plain );
        if ( count( $new_sentences ) < 5 ) {
            $result['detail'] = 'Too few sentences to check (' . count( $new_sentences ) . ')';
            return $result;
        }

        // Get last 30 EEurope posts' content
        $existing = $wpdb->get_col(
            "SELECT p.post_content
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_dba_eeurope_auto_created'
             WHERE p.post_status IN ('publish','draft','future')
             ORDER BY p.post_date DESC
             LIMIT 30"
        );

        if ( empty( $existing ) ) {
            $result['detail'] = 'No existing EEurope posts to compare (first post).';
            return $result;
        }

        // Build a pool of existing sentences
        $existing_sentences = array();
        foreach ( $existing as $ex_html ) {
            $ex_plain = wp_strip_all_tags( $ex_html );
            $ex_plain = preg_replace( '/Damalion\s+(supports|helps|assists|accompagne).*$/si', '', $ex_plain );
            foreach ( $this->extract_sentences( $ex_plain ) as $es ) {
                $existing_sentences[] = mb_strtolower( $es );
            }
        }

        if ( empty( $existing_sentences ) ) {
            $result['detail'] = 'No comparable sentences found in existing posts.';
            return $result;
        }

        $flagged = 0;
        $checked = count( $new_sentences );

        foreach ( $new_sentences as $ns ) {
            $ns_lower = mb_strtolower( $ns );
            foreach ( $existing_sentences as $es ) {
                similar_text( $ns_lower, $es, $pct );
                if ( $pct >= 90.0 ) {
                    $flagged++;
                    break; // One match is enough to flag this sentence
                }
            }
        }

        $dup_ratio = $checked > 0 ? $flagged / $checked : 0;

        if ( $dup_ratio > 0.15 ) {
            $result['pass'] = false;
            $result['detail'] = sprintf(
                '%d/%d sentences (%.0f%%) match existing EEurope posts at 90%%+ similarity. Max allowed: 15%%.',
                $flagged, $checked, $dup_ratio * 100
            );
        } else {
            $result['detail'] = sprintf(
                '%d/%d sentences flagged (%.0f%%) — below 15%% threshold. OK.',
                $flagged, $checked, $dup_ratio * 100
            );
        }

        return $result;
    }

    /**
     * Extract meaningful sentences (>30 chars) from plain text.
     */
    private function extract_sentences( $plain_text ) {
        $raw = preg_split( '/(?<=[.!?])\s+/', trim( $plain_text ), -1, PREG_SPLIT_NO_EMPTY );
        $out = array();
        foreach ( $raw as $s ) {
            $s = trim( $s );
            if ( mb_strlen( $s ) > 30 && str_word_count( $s ) >= 6 ) {
                $out[] = $s;
            }
        }
        return $out;
    }

    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();
    }
}
