<?php
namespace Avision;

/**
 * Routeur OpenRouter AVI : contextes STANDARD vs PREMIUM, températures par profil.
 *
 * MODE STANDARD (openai/gpt-4o-mini) : temp 0.5 — rapidité, précision factuelle, navigation, statut.
 * MODE PREMIUM (deepseek/deepseek-chat) : temp 0.65 — analyse fine, persuasion, analogies, rigueur technique.
 * Fallback : en cas de doute → MODE STANDARD pour préserver les crédits.
 */
class Avision_OpenRouter_Router {

    const MODEL_STANDARD = 'openai/gpt-4o-mini';
    const MODEL_PREMIUM  = 'deepseek/deepseek-chat';

    /** Températures par profil (passées dans le corps JSON de la requête API) */
    const TEMPERATURE_STANDARD = 0.5;
    const TEMPERATURE_PREMIUM  = 0.65;

    /** Historique : nombre max de messages de conversation envoyés à l'API (optimisation tokens) */
    const MAX_HISTORY_MESSAGES = 5;

    /** Contextes MODE STANDARD : salutations, statut, définitions, navigation */
    private static $standard_phrases = [
        // Salutations
        'bonjour', 'salut', 'ça va', 'ca va', 'hello', 'hi', 'coucou', 'hey',
        // Statut
        'c\'est quoi mon score', 'quel est mon niveau', 'quel sprint', 'mon score', 'mon niveau',
        'what\'s my score', 'what is my level', 'my score', 'my level',
        // Définitions
        'c\'est quoi le level', 'c\'est quoi le mode gardien', 'qu\'est-ce que le level',
        'what is level', 'what is guardian mode', 'what is the mode gardien',
        // Navigation
        'comment faire pour', 'où se trouve', 'ou se trouve', 'aide-moi à trouver', 'aide moi a trouver',
        'how to', 'where is', 'help me find', 'comment aller', 'où est ', 'ou est ',
    ];

    /** Mots-clés MODE PREMIUM : rédaction, analyse, crise, comparaison */
    private static $premium_phrases = [
        // Rédaction
        'répondre à cet avis', 'repondre a cet avis', 'rédige un post', 'redige un post',
        'écris un message', 'ecris un message', 'rédige une réponse', 'redige une reponse',
        'draft a response', 'write a post', 'reply to this review',
        // Analyse
        'pourquoi ', 'analyse mes concurrents', 'explique la stratégie', 'explique la strategie',
        'analyze my competitors', 'explain the strategy', 'why ',
        // Gestion de crise
        'avis négatif', 'avis negatif', 'suspension', 'problème urgent', 'probleme urgent',
        '1 étoile', '1 etoile', 'spam', 'negative review', 'urgent', 'crise',
        // Comparaison
        'quelle est la différence entre', 'quelle est la difference entre', 'mieux que',
        'what is the difference between', 'better than', 'compare ',
    ];

    /** Termes analytiques (présence = pas "question simple" → peut rester standard si < 10 mots mais on privilégie le fallback) */
    private static $analytical_terms = [
        'analyser', 'stratégie', 'concurrence', 'optimiser', 'audit', 'diagnostic',
        'recommandation', 'comparer', 'pourquoi', 'analyse ',
    ];

    /** Actions déclenchant MODE PREMIUM (ex: génération réponse avis) */
    private static $premium_actions = [
        'reponse_critique', 'gbp_review', 'generation de reponse critique', 'reponse critique',
        'bulk_google_post',
    ];

    /**
     * Retourne le modèle à utiliser pour cette requête.
     * Ordre : force standard → contexte STANDARD → contexte PREMIUM → fallback STANDARD.
     *
     * @param string $user_message Message utilisateur (contenu brut ou avec contexte)
     * @param string $action       Action demandée (ex: 'reponse_critique', 'chat', '')
     * @return string Identifiant du modèle (MODEL_STANDARD ou MODEL_PREMIUM)
     */
    /**
     * Retourne le modèle et la température pour cette requête (pour le corps JSON de l'API).
     *
     * @param string $user_message Message utilisateur
     * @param string $action       Action demandée
     * @return array { 'model' => string, 'temperature' => float }
     */
    public static function get_model_and_temperature( $user_message, $action = '' ) {
        $model = self::get_model_for_request( $user_message, $action );
        $temperature = self::get_temperature_for_model( $model );
        self::log_route( $model, $temperature );
        return [ 'model' => $model, 'temperature' => $temperature ];
    }

    /**
     * Température associée au modèle (rapidité/précision pour STANDARD, analyse/persuasion pour PREMIUM).
     *
     * @param string $model Identifiant du modèle
     * @return float
     */
    public static function get_temperature_for_model( $model ) {
        if ( $model === self::MODEL_PREMIUM ) {
            return self::TEMPERATURE_PREMIUM;
        }
        return self::TEMPERATURE_STANDARD;
    }

    public static function get_model_for_request( $user_message, $action = '' ) {
        if ( self::is_force_standard_enabled() ) {
            return self::MODEL_STANDARD;
        }

        $text = is_string( $user_message ) ? $user_message : '';
        $clean = self::extract_user_question_for_routing( $text );

        if ( self::matches_standard_context( $clean, $text ) ) {
            return self::MODEL_STANDARD;
        }
        if ( self::matches_premium_context( $clean, $text, $action ) ) {
            return self::MODEL_PREMIUM;
        }

        return self::MODEL_STANDARD;
    }

    /**
     * Extrait la partie "Question utilisateur" du message si présente (contexte dashboard).
     *
     * @param string $full_message
     * @return string
     */
    private static function extract_user_question_for_routing( $full_message ) {
        $delimiters = [ 'User question:', 'User question: ', "\n\nUser question:\n\n", 'Question utilisateur :', 'Question utilisateur : ' ];
        foreach ( $delimiters as $delimiter ) {
            $pos = mb_strpos( $full_message, $delimiter, 0, 'UTF-8' );
            if ( $pos !== false ) {
                return trim( mb_substr( $full_message, $pos + mb_strlen( $delimiter, 'UTF-8' ), null, 'UTF-8' ) );
            }
        }
        return trim( $full_message );
    }

    /**
     * Normalise le texte pour la comparaison : apostrophes typographiques → ASCII, accents optionnels.
     *
     * @param string $text
     * @return string
     */
    private static function normalize_for_routing( $text ) {
        $t = mb_strtolower( trim( $text ), 'UTF-8' );
        // Apostrophe typographique (U+2019) et variantes → ASCII
        $t = str_replace( [ "\u{2019}", "'", "'", "`" ], "'", $t );
        return $t;
    }

    /**
     * Vérifie si le message relève du MODE STANDARD (maintenance, info simple).
     *
     * @param string $user_question Partie question utilisateur
     * @param string $full_message   Message complet
     * @return bool
     */
    private static function matches_standard_context( $user_question, $full_message ) {
        $lower = self::normalize_for_routing( $user_question );

        foreach ( self::$standard_phrases as $phrase ) {
            $phrase_normalized = str_replace( "\u{2019}", "'", $phrase );
            if ( strpos( $lower, $phrase_normalized ) !== false ) {
                return true;
            }
        }

        // Question simple : moins de 10 mots et sans termes analytiques
        $words = preg_split( '/\s+/u', trim( $user_question ), -1, PREG_SPLIT_NO_EMPTY );
        if ( count( $words ) < 10 ) {
            $lower_full = self::normalize_for_routing( $full_message );
            foreach ( self::$analytical_terms as $term ) {
                if ( strpos( $lower_full, $term ) !== false ) {
                    return false; // terme analytique présent → pas "simple"
                }
            }
            return true; // < 10 mots et pas analytique → STANDARD
        }

        return false;
    }

    /**
     * Vérifie si le message relève du MODE PREMIUM (rédaction, analyse, crise, comparaison, complexité).
     *
     * @param string $user_question Partie question utilisateur
     * @param string $full_message   Message complet
     * @param string $action        Action demandée
     * @return bool
     */
    private static function matches_premium_context( $user_question, $full_message, $action ) {
        $action_lower = trim( strtolower( $action ) );
        foreach ( self::$premium_actions as $a ) {
            if ( $action_lower !== '' && ( $action_lower === strtolower( $a ) || strpos( $action_lower, $a ) !== false ) ) {
                return true;
            }
        }

        $lower = self::normalize_for_routing( $user_question );
        $lower_full = self::normalize_for_routing( $full_message );

        foreach ( self::$premium_phrases as $phrase ) {
            $phrase_normalized = str_replace( "\u{2019}", "'", $phrase );
            if ( strpos( $lower, $phrase_normalized ) !== false || strpos( $lower_full, $phrase_normalized ) !== false ) {
                return true;
            }
        }

        // Complexité : plus de 3 questions OU phrase(s) de plus de 20 mots
        $question_marks = substr_count( $user_question, '?' );
        if ( $question_marks >= 3 ) {
            return true;
        }
        $sentences = preg_split( '/[.!?]+/u', $user_question, -1, PREG_SPLIT_NO_EMPTY );
        foreach ( $sentences as $s ) {
            $words = preg_split( '/\s+/u', trim( $s ), -1, PREG_SPLIT_NO_EMPTY );
            if ( count( $words ) > 20 ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Log de contrôle : modèle utilisé et température (chaque appel).
     */
    private static function log_route( $model, $temperature ) {
        if ( function_exists( 'error_log' ) ) {
            error_log( 'AVI-ROUTE : Modèle utilisé [' . $model . '] | Température [' . $temperature . ']' );
        }
    }

    /**
     * Vérifie si le passage en MODEL_STANDARD est forcé (option WP ou constante).
     *
     * @return bool
     */
    public static function is_force_standard_enabled() {
        if ( defined( 'AVISION_OPENROUTER_FORCE_STANDARD' ) && AVISION_OPENROUTER_FORCE_STANDARD ) {
            return true;
        }
        if ( function_exists( 'get_option' ) ) {
            return (bool) get_option( 'avision_openrouter_force_standard', false );
        }
        return false;
    }

    /**
     * Limite l'historique aux N derniers messages de la conversation (optimisation tokens).
     *
     * @param array $messages Liste [ ['role' => 'user'|'assistant', 'content' => '...'], ... ]
     * @param int   $max_messages Nombre max de messages (défaut 5, voir MAX_HISTORY_MESSAGES)
     * @return array Derniers $max_messages messages au max
     */
    public static function trim_messages_to_last_messages( array $messages, $max_messages = null ) {
        $max = $max_messages !== null ? (int) $max_messages : self::MAX_HISTORY_MESSAGES;
        if ( $max <= 0 || empty( $messages ) ) {
            return [];
        }
        $count = count( $messages );
        if ( $count <= $max ) {
            return $messages;
        }
        return array_slice( $messages, -$max, $max, true );
    }

    /**
     * Limite l'historique aux N derniers échanges (1 échange = user + assistant).
     * @deprecated Préférer trim_messages_to_last_messages() pour limiter à 5 messages.
     */
    public static function trim_messages_to_last_exchanges( array $messages, $max_exchanges = 5 ) {
        if ( $max_exchanges <= 0 || empty( $messages ) ) {
            return [];
        }
        $max_messages = (int) $max_exchanges * 2;
        $count        = count( $messages );
        if ( $count <= $max_messages ) {
            return $messages;
        }
        return array_slice( $messages, -$max_messages, $max_messages, true );
    }

    /**
     * Option wp_options « vertical » Knowledge Center selon le secteur détecté (FOOD, HOSPITALITY, etc.).
     *
     * @param string $sector Code ou libellé secteur (ex. FOOD, SERVICE).
     * @return string|null Nom d’option ou null si aucune règle dédiée.
     */
    private static function resolve_vertical_option_key( $sector ) {
        $s = strtoupper( trim( (string) $sector ) );
        if ( $s === '' ) {
            return null;
        }
        if ( strpos( $s, 'FOOD' ) !== false || strpos( $s, 'RESTAURANT' ) !== false ) {
            return 'avi_prompt_b2c_restaurant';
        }
        if ( strpos( $s, 'HOSPITALITY' ) !== false || strpos( $s, 'HOTEL' ) !== false || strpos( $s, 'HÔTEL' ) !== false ) {
            return 'avi_prompt_b2c_hotel';
        }
        if ( strpos( $s, 'REAL' ) !== false && strpos( $s, 'ESTATE' ) !== false ) {
            return 'avi_kc_vert_real_estate';
        }
        if ( strpos( $s, 'IMMO' ) !== false ) {
            return 'avi_kc_vert_real_estate';
        }
        // Macro-verticales KC (codes HEALTH, AUTO, RETAIL, PRO / PRO_SERVICES, LIFESTYLE + synonymes).
        if ( $s === 'HEALTH' || strpos( $s, 'HEALTH' ) !== false || strpos( $s, 'MEDICAL' ) !== false || strpos( $s, 'MÉDICAL' ) !== false
            || strpos( $s, 'DENTAL' ) !== false || strpos( $s, 'DENTIST' ) !== false || strpos( $s, 'PHARMA' ) !== false
            || strpos( $s, 'CLINIC' ) !== false || strpos( $s, 'HOSPITAL' ) !== false ) {
            return 'avi_kc_vert_health';
        }
        if ( $s === 'AUTO' || strpos( $s, 'AUTO' ) !== false || strpos( $s, 'AUTOMOB' ) !== false || strpos( $s, 'GARAGE' ) !== false
            || strpos( $s, 'CONCESSION' ) !== false || strpos( $s, 'MOTO' ) !== false || strpos( $s, 'CAR_DEALER' ) !== false ) {
            return 'avi_kc_vert_auto';
        }
        if ( $s === 'RETAIL' || strpos( $s, 'RETAIL' ) !== false || strpos( $s, 'BOUTIQUE' ) !== false || strpos( $s, 'STORE' ) !== false
            || preg_match( '/\bSHOP\b/', $s ) === 1 || strpos( $s, 'SHOPPING' ) !== false || strpos( $s, 'COMMERCE_DETAIL' ) !== false ) {
            return 'avi_kc_vert_retail';
        }
        if ( $s === 'PRO' || $s === 'PRO_SERVICES' || strpos( $s, 'PRO_SERVICES' ) !== false || strpos( $s, 'CONSULTING' ) !== false
            || strpos( $s, 'ACCOUNTING' ) !== false || strpos( $s, 'LEGAL' ) !== false || strpos( $s, 'LAW_FIRM' ) !== false ) {
            return 'avi_kc_vert_pro_services';
        }
        if ( $s === 'LIFESTYLE' || strpos( $s, 'LIFESTYLE' ) !== false || strpos( $s, 'BEAUTY' ) !== false || strpos( $s, 'SALON' ) !== false
            || strpos( $s, 'SPA' ) !== false || strpos( $s, 'FITNESS' ) !== false || strpos( $s, 'GYM' ) !== false
            || strpos( $s, 'ENTERTAINMENT' ) !== false || strpos( $s, 'BARBER' ) !== false ) {
            return 'avi_kc_vert_lifestyle';
        }
        if ( $s === 'SERVICE' || strpos( $s, 'SAB' ) !== false ) {
            return 'avi_prompt_b2c_sab';
        }
        return null;
    }

    /**
     * Texte d’une option KC trilingue (base FR + _en / _es) selon la langue UI du chat.
     *
     * @param string $base_option Nom sans suffixe (ex. avi_kc_gbp_foundations).
     * @param string $lang        fr|en|es.
     * @return string
     */
    public static function kc_pick_locale_text( $base_option, $lang ) {
        if ( ! function_exists( 'get_option' ) ) {
            return '';
        }
        $base_option = is_string( $base_option ) ? $base_option : '';
        if ( $base_option === '' ) {
            return '';
        }
        $lang = strtolower( (string) $lang );
        if ( ! in_array( $lang, array( 'fr', 'en', 'es' ), true ) ) {
            $lang = 'fr';
        }
        if ( $lang === 'fr' ) {
            return trim( (string) get_option( $base_option, '' ) );
        }
        $suffix = ( 'en' === $lang ) ? '_en' : '_es';
        $localized = trim( (string) get_option( $base_option . $suffix, '' ) );
        if ( $localized !== '' ) {
            return $localized;
        }
        return trim( (string) get_option( $base_option, '' ) );
    }

    /**
     * Lit les 6 options KC « GBP Master » et les concatène avec des titres === N. … === (payload API : gbp_master_knowledge).
     *
     * @param string $lang Langue fr|en|es (défaut fr).
     * @return string Chaîne vide si aucun pilier rempli.
     */
    public static function get_gbp_master_knowledge_block( $lang = 'fr' ) {
        if ( ! function_exists( 'get_option' ) ) {
            return '';
        }
        $lang = strtolower( (string) $lang );
        if ( ! in_array( $lang, array( 'fr', 'en', 'es' ), true ) ) {
            $lang = 'fr';
        }
        $pillars = array(
            array( 'option' => 'avi_kc_gbp_foundations', 'heading' => '=== 1. FONDATIONS & VÉRIFICATIONS ===' ),
            array( 'option' => 'avi_kc_gbp_algorithm', 'heading' => '=== 2. ALGORITHME & RANKING SEO ===' ),
            array( 'option' => 'avi_kc_gbp_guidelines', 'heading' => '=== 3. CONFORMITÉ & RÈGLES STRICTES ===' ),
            array( 'option' => 'avi_kc_gbp_emergencies', 'heading' => '=== 4. URGENCES, DÉMÉNAGEMENTS & SUSPENSIONS ===' ),
            array( 'option' => 'avi_kc_gbp_reputation', 'heading' => '=== 5. RÉPUTATION & ANTI-SPAM ===' ),
            array( 'option' => 'avi_kc_gbp_advanced', 'heading' => '=== 6. ÉCOSYSTÈME AVANCÉ & APPLE MAPS ===' ),
        );
        $blocks = array();
        foreach ( $pillars as $row ) {
            $body = self::kc_pick_locale_text( $row['option'], $lang );
            if ( $body === '' ) {
                continue;
            }
            $blocks[] = $row['heading'] . "\n\n" . $body;
        }
        return $blocks !== array() ? implode( "\n\n", $blocks ) : '';
    }

    /**
     * Lit les 3 options KC « Master Knowledge Agence » (Fleet Assistant B2B) — payload API : b2b_master_knowledge.
     *
     * @param string $lang Langue fr|en|es (défaut fr).
     * @return string Chaîne vide si aucun bloc rempli.
     */
    public static function get_b2b_master_knowledge_block( $lang = 'fr' ) {
        if ( ! function_exists( 'get_option' ) ) {
            return '';
        }
        $lang = strtolower( (string) $lang );
        if ( ! in_array( $lang, array( 'fr', 'en', 'es' ), true ) ) {
            $lang = 'fr';
        }
        $sections = array(
            array( 'option' => 'avi_kc_b2b_fleet_management', 'heading' => '=== 1. GESTION DE FLOTTE ===' ),
            array( 'option' => 'avi_kc_b2b_roi_churn', 'heading' => '=== 2. ROI, RÉTENTION & ANTI-CHURN ===' ),
            array( 'option' => 'avi_kc_b2b_senior_strategy', 'heading' => '=== 3. STRATÉGIE SENIOR & VENTES B2B ===' ),
        );
        $blocks = array();
        foreach ( $sections as $row ) {
            $body = self::kc_pick_locale_text( $row['option'], $lang );
            if ( $body === '' ) {
                continue;
            }
            $blocks[] = $row['heading'] . "\n\n" . $body;
        }
        return $blocks !== array() ? implode( "\n\n", $blocks ) : '';
    }

    /**
     * Assemble le system prompt chat : d’abord le systemMessage JS (guardrails, personnalité), puis « --- Contexte Session --- »,
     * puis le bloc Knowledge Center (PHP) — encyclopédie, contexte global B2C/B2B, métier, niveau, correctifs (effet de récence côté proxy Python : <OVERRIDE_AI_TRAINING>).
     * Les « Règles SEO & mots tabous par métier » (option WP avi_kc_restricted_words) sont ajoutées au corps JSON de la requête FastAPI sous la clé avi_kc_restricted_words par class-avision-api.php ; le backend les injecte dans <OVERRIDE_AI_TRAINING> après le dictionnaire fichier.
     * Le bloc « GBP Master Knowledge » (6 piliers) est dupliqué dans le payload JSON sous la clé gbp_master_knowledge (voir {@see get_gbp_master_knowledge_block()}).
     * Le bloc « Master Knowledge Agence » (3 piliers B2B) est dupliqué sous la clé b2b_master_knowledge (voir {@see get_b2b_master_knowledge_block()}) pour le Fleet Assistant.
     *
     * SÉCURITÉ : n’injecter AUCUNE option `avi_ui_*` (Textes Interface / App UI) — réservées au frontend, pas à OpenRouter.
     *
     * @param array $args {
     *     @type string     $user_type      'b2c' ou 'b2b'.
     *     @type string     $sector         Secteur détecté (FOOD, SERVICE…).
     *     @type int        $level          Niveau 1–21.
     *     @type string     $client_context Prompt guardrails + JS / encyclopédie (peut être vide).
     *     @type string     $language       Langue UI du chat : fr|en|es (défaut en). EN/ES → pont de traduction sur le bloc niveau KC.
     * }
     * @return string
     */
    public static function build_chat_system_prompt( array $args ) {
        if ( ! function_exists( 'get_option' ) ) {
            return isset( $args['client_context'] ) ? trim( (string) $args['client_context'] ) : '';
        }

        // B2B (agence / Fleet) : pas de parcours L1–L21 dans le KC injecté — exclus : avi_kc_b2c_level_*, radar, sprints, encyclopédie B2C. Inclus : contexte global B2B, encyclopédie B2B, GBP Master (6 piliers), mémoires générales, verticale, gating (transverse).
        $user_type = isset( $args['user_type'] ) && strtolower( (string) $args['user_type'] ) === 'b2b' ? 'b2b' : 'b2c';
        $sector    = isset( $args['sector'] ) ? (string) $args['sector'] : '';
        $level     = isset( $args['level'] ) ? max( 1, min( 21, (int) $args['level'] ) ) : 1;
        $client    = isset( $args['client_context'] ) ? trim( (string) $args['client_context'] ) : '';
        $language  = isset( $args['language'] ) ? strtolower( (string) $args['language'] ) : 'en';
        if ( ! in_array( $language, array( 'fr', 'en', 'es' ), true ) ) {
            $language = 'en';
        }

        $parts      = array();
        $ency_wp_block = '';

        // B2C : encyclopédie globale KC — clé historique avi_prompt_b2c_encyclopedia (+ _en / _es).
        if ( $user_type === 'b2c' ) {
            $ency_first = trim( self::kc_pick_locale_text( 'avi_prompt_b2c_encyclopedia', $language ) );
            if ( $ency_first !== '' ) {
                $ency_wp_block = "--- PRIORITÉ ABSOLUE — LEXIQUE OFFICIEL AVI (Knowledge Center / WordPress) ---\n"
                    . "Tout ce qui suit dans ce bloc ENCYCLOPÉDIE est CANONIQUE : tu t’y conformes strictement pour les définitions, acronymes (ex. NAP), règles ou formulations pédagogiques, même si elles contredisent le sens « habituel » du marketing local, Wikipédia ou tes connaissances d’entraînement. En cas de conflit, SEUL ce texte fait foi.\n\n"
                    . "--- ENCYCLOPÉDIE DASHBOARD (dictionnaire modales / savoir général) — Knowledge Center ---\n"
                    . $ency_first;
            }
        }

        if ( $user_type === 'b2b' ) {
            $welcome = trim( self::kc_pick_locale_text( 'avi_prompt_b2b_welcome', $language ) );
            $global  = trim( self::kc_pick_locale_text( 'avi_prompt_b2b_global', $language ) );
        } else {
            $welcome = trim( self::kc_pick_locale_text( 'avi_prompt_b2c_welcome', $language ) );
            $global  = trim( self::kc_pick_locale_text( 'avi_prompt_b2c_global', $language ) );
        }
        $block_global = trim( $welcome . ( $welcome !== '' && $global !== '' ? "\n\n" : '' ) . $global );
        if ( $block_global !== '' ) {
            $parts[] = '--- CONTEXTE GLOBAL (' . strtoupper( $user_type ) . ") — Knowledge Center ---\n" . $block_global;
        }

        if ( $user_type === 'b2b' ) {
            $b2b_ency = trim( self::kc_pick_locale_text( 'avi_kc_b2b_encyclopedia', $language ) );
            if ( $b2b_ency !== '' ) {
                $parts[] = "--- ENCYCLOPÉDIE & RÔLE FLEET ASSISTANT (B2B — Knowledge Center) ---\n" . $b2b_ency;
            }
        }

        $sys_kc = trim( self::kc_pick_locale_text( 'avi_prompt_system', $language ) );
        if ( $sys_kc !== '' ) {
            $parts[] = "--- MÉMOIRE SYSTÈME (AVI KC — prompt système) ---\n" . $sys_kc;
        }

        $mem_gbp = trim( self::kc_pick_locale_text( 'avi_kc_general_gbp_rules', $language ) );
        if ( $mem_gbp !== '' ) {
            $parts[] = "--- MÉMOIRE GÉNÉRALE & DOCTRINE — Règles Google Maps (GBP) — Knowledge Center ---\n" . $mem_gbp;
        }
        $mem_susp = trim( self::kc_pick_locale_text( 'avi_kc_general_suspensions', $language ) );
        if ( $mem_susp !== '' ) {
            $parts[] = "--- MÉMOIRE GÉNÉRALE & DOCTRINE — Suspensions & Appels — Knowledge Center ---\n" . $mem_susp;
        }
        $mem_premium_teasing = trim( self::kc_pick_locale_text( 'avi_kc_general_premium_teasing', $language ) );
        if ( $mem_premium_teasing !== '' ) {
            $parts[] = "--- MÉMOIRE GÉNÉRALE & DOCTRINE — Fonctionnalités Premium (Teasing) — Knowledge Center ---\n" . $mem_premium_teasing;
        }

        $mem_design_chat = trim( self::kc_pick_locale_text( 'avi_kc_general_design', $language ) );
        if ( $mem_design_chat !== '' ) {
            $parts[] = "--- RÈGLES DE MISE EN FORME VISUELLE ---\n" . $mem_design_chat;
        }

        // B2C : garde-fou ton — le Design Hub ne doit pas écraser la personnalité Coach VIP (réponses trop sèches).
        if ( $user_type === 'b2c' ) {
            $parts[] = "--- GARDE-FOU TON COACH VIP (Premium B2C) ---\n"
                . 'Même si tu dois être concis et respecter le formatage visuel (couleurs, gras, liens d’aide), tu ne dois jamais perdre ton rôle de Coach VIP. '
                . 'Tes réponses doivent rester motivantes, structurées et donner des instructions claires sur l’endroit où cliquer '
                . '(ex. : volet latéral d’action, bouton noir « Vérifier l’action », carte du niveau concerné sur le parcours).';
        }

        // B2C + B2B (agence / Fleet) : corpus GBP 6 piliers — expertise dashboard agence comme pour le chat client.
        $gbp_master_knowledge = self::get_gbp_master_knowledge_block( $language );
        if ( $gbp_master_knowledge !== '' ) {
            $parts[] = "--- GBP MASTER KNOWLEDGE (Google Business Profile — 6 piliers — Knowledge Center) ---\n"
                . "Tu t'appuies sur ce corpus comme référence d'expertise mondiale GBP / Google Maps (priorité sur les généralités). "
                . "Une copie structurée identique est transmise dans le payload JSON sous la clé gbp_master_knowledge.\n\n"
                . "Reste aligné Coach AVI et domaine strict Local SEO.\n\n"
                . $gbp_master_knowledge;
        }

        if ( $user_type === 'b2b' ) {
            $b2b_master_knowledge = self::get_b2b_master_knowledge_block( $language );
            if ( $b2b_master_knowledge !== '' ) {
                $parts[] = "--- B2B AGENCY MASTER KNOWLEDGE (Stratégie, Flotte, ROI — Knowledge Center) ---\n"
                    . "Pour les questions d’agence (scalabilité, ROI, rétention, anti-churn, ventes B2B, posture senior), tu t’appuies sur ce corpus. "
                    . "Une copie structurée identique est transmise dans le payload JSON sous la clé b2b_master_knowledge.\n\n"
                    . $b2b_master_knowledge;
            }
        }

        $vkey = self::resolve_vertical_option_key( $sector );
        if ( $vkey ) {
            $vertical = trim( self::kc_pick_locale_text( $vkey, $language ) );
            if ( $vertical !== '' ) {
                $parts[] = "--- RÈGLES MÉTIER (secteur détecté : {$sector}) — Knowledge Center ---\n" . $vertical;
            }
        }

		if ( $user_type === 'b2c' ) {
			$sprints_kc = trim( self::kc_pick_locale_text( 'avi_kc_b2c_sprints', $language ) );
			if ( $sprints_kc !== '' ) {
				$parts[] = "--- SPRINTS & NIVEAUX (cartographie) — Knowledge Center ---\n" . $sprints_kc;
			}
			// Niveau courant, radar, gardien : strictement B2C (pas de parcours L1–L21 côté agence / Fleet).
			$level_key     = 'avi_kc_b2c_level_' . $level;
			$level_fr      = trim( (string) get_option( $level_key, '' ) );
			$level_native  = '';
			if ( $language === 'en' ) {
				$level_native = trim( (string) get_option( $level_key . '_en', '' ) );
			} elseif ( $language === 'es' ) {
				$level_native = trim( (string) get_option( $level_key . '_es', '' ) );
			}
			$level_txt = ( 'fr' === $language ) ? $level_fr : ( $level_native !== '' ? $level_native : $level_fr );
			if ( $level_txt !== '' ) {
				$level_block = '--- CONTEXTE NIVEAU ACTUEL (Level ' . $level . ") — Knowledge Center ---\n";
				// Pont de traduction uniquement si la variante EN/ES dédiée est vide mais le corps FR est utilisé.
				if ( ( $language === 'en' || $language === 'es' ) && $level_native === '' && $level_fr !== '' ) {
					$lang_courante = ( $language === 'es' ) ? 'espagnol' : 'anglais';
					$level_block  .= 'Le contenu suivant (Mission de l’Ombre, descriptions et consignes du niveau) est stocké en français dans le Knowledge Center. '
						. 'Tu dois le traduire fidèlement en ' . $lang_courante . " avant de l'intégrer dans ta réponse.\n\n";
				}
				$level_block .= $level_txt;
				$parts[]      = $level_block;
			}
		}

		if ( $user_type === 'b2c' ) {
			$radar_kc = trim( self::kc_pick_locale_text( 'avi_kc_b2c_radar', $language ) );
			if ( $radar_kc !== '' ) {
				$parts[] = "--- RADAR DE CROISSANCE — Knowledge Center ---\n" . $radar_kc;
			}
			$guardian_kc = trim( self::kc_pick_locale_text( 'avi_kc_b2c_guardian', $language ) );
			if ( $guardian_kc !== '' ) {
				$parts[] = "--- MODE GARDIEN (dashboard, teasing, alertes) — Knowledge Center ---\n" . $guardian_kc;
			}
		}

		if ( $user_type === 'b2c' ) {
			$gating_kc = trim( self::kc_pick_locale_text( 'avi_kc_b2c_gating', $language ) );
			if ( $gating_kc !== '' ) {
				$parts[] = "--- GATING & NIVEAU VERROUILLÉ (coach / UI / premium) — Knowledge Center ---\n" . $gating_kc;
			}
		}

		if ( class_exists( '\Avision\Avision_AI_Interactions' ) ) {
            $corr = \Avision\Avision_AI_Interactions::get_prompt_addon();
            if ( $corr !== '' ) {
                $parts[] = $corr;
            }
        }

        $kc_block = implode( "\n\n", $parts );
        $sep      = "\n\n--- Session Context ---\n\n";

        // Encyclopédie d’abord : le modèle lit le KC WordPress avant les garde-fous session (évite NAP / acronymes « généralistes »).
        if ( $ency_wp_block !== '' ) {
            if ( $client !== '' && $kc_block !== '' ) {
                return trim( $ency_wp_block . $sep . $client . $sep . $kc_block );
            }
            if ( $client !== '' ) {
                return trim( $ency_wp_block . $sep . $client );
            }
            if ( $kc_block !== '' ) {
                return trim( $ency_wp_block . $sep . $kc_block );
            }
            return trim( $ency_wp_block );
        }

        // Session / guardrails d’abord, puis KC (récence pour le modèle ; le proxy Python enveloppe le KC dans <OVERRIDE_AI_TRAINING>).
        if ( $kc_block !== '' && $client !== '' ) {
            return trim( $client . $sep . $kc_block );
        }
        if ( $client !== '' ) {
            return trim( $client );
        }

        return trim( $kc_block );
    }

    /**
     * Indique si un extrait ressemble au bloc KC WordPress (encyclopédie, contexte global, etc.).
     *
     * @param string $chunk
     * @return bool
     */
    private static function looks_like_wordpress_kc_block( $chunk ) {
        $l = strtolower( (string) $chunk );
        return (
            strpos( $l, 'encyclopédie dashboard' ) !== false
            || strpos( $l, 'encyclopedie dashboard' ) !== false
            || strpos( $l, 'enciclopedia dashboard' ) !== false
            || strpos( $l, 'lexique officiel avi' ) !== false
            || strpos( $l, 'priorité absolue' ) !== false
            || strpos( $l, 'priorite absolue' ) !== false
            || strpos( $l, '--- contexte global' ) !== false
            || strpos( $l, 'knowledge center' ) !== false
        );
    }

    /**
     * Extrait la partie « session » (systemMessage JS / guardrails) pour réinjection dans build_chat_system_prompt.
     * Gère l’ancien ordre (KC puis marqueur puis session) et le nouvel ordre (session puis marqueur puis KC).
     *
     * @param string $text Texte system complet éventuellement empilé.
     * @return string
     */
    private static function extract_session_context_from_full_system_text( $text ) {
        $text = trim( (string) $text );
        if ( $text === '' ) {
            return '';
        }
        $markers = array( "\n\n--- Session Context ---\n\n", "\n--- Session Context ---\n", '--- Session Context ---' );
        foreach ( $markers as $m ) {
            $p = strpos( $text, $m );
            if ( $p !== false ) {
                $before = trim( substr( $text, 0, $p ) );
                $after  = trim( substr( $text, $p + strlen( $m ) ) );
                // Ancien assemblage : KC + marqueur + session JS.
                if ( self::looks_like_wordpress_kc_block( $before ) && ! self::looks_like_wordpress_kc_block( $after ) ) {
                    return $after;
                }
                return $before;
            }
        }
        return $text;
    }

    /**
     * OpenRouter chat : impose un unique message system = build_chat_system_prompt (session JS + KC WordPress).
     * Retire tout rôle system envoyé par le client ; insère le bloc officiel en index 0.
     *
     * @param array<int, array<string, mixed>> $messages Modifié par référence (system, user, assistant uniquement).
     * @param array{
     *     user_type?: string,
     *     sector?: string,
     *     level?: int,
     *     client_context?: string,
     *     language?: string
     * } $args client_context = partie JS / session seule si déjà isolée.
     */
    public static function enforce_chat_messages_system_prompt( array &$messages, array $args ) {
        $user_type = isset( $args['user_type'] ) && strtolower( (string) $args['user_type'] ) === 'b2b' ? 'b2b' : 'b2c';
        $sector    = isset( $args['sector'] ) ? (string) $args['sector'] : '';
        $level     = isset( $args['level'] ) ? max( 1, min( 21, (int) $args['level'] ) ) : 1;
        $lang_enf  = isset( $args['language'] ) ? strtolower( (string) $args['language'] ) : 'en';
        if ( ! in_array( $lang_enf, array( 'fr', 'en', 'es' ), true ) ) {
            $lang_enf = 'en';
        }

        $fallback_system = '';
        foreach ( $messages as $m ) {
            if ( is_array( $m ) && isset( $m['role'] ) && strtolower( (string) $m['role'] ) === 'system' && isset( $m['content'] ) ) {
                $fallback_system = trim( (string) $m['content'] );
                break;
            }
        }

        $client = isset( $args['client_context'] ) ? trim( (string) $args['client_context'] ) : '';
        if ( $client === '' && $fallback_system !== '' ) {
            $client = self::extract_session_context_from_full_system_text( $fallback_system );
            if ( $client === '' ) {
                $client = $fallback_system;
            }
        }

        $final = self::build_chat_system_prompt(
            array(
                'user_type'      => $user_type,
                'sector'         => $sector,
                'level'          => $level,
                'client_context' => $client,
                'language'       => $lang_enf,
            )
        );
        if ( $final === '' && $fallback_system !== '' ) {
            $final = $fallback_system;
        }

        $rest = array();
        foreach ( $messages as $m ) {
            if ( ! is_array( $m ) || ! isset( $m['role'] ) ) {
                continue;
            }
            $role = strtolower( (string) $m['role'] );
            if ( $role === 'system' ) {
                continue;
            }
            if ( $role !== 'user' && $role !== 'assistant' ) {
                continue;
            }
            $content = isset( $m['content'] ) && is_string( $m['content'] ) ? $m['content'] : '';
            if ( $content === '' ) {
                continue;
            }
            $rest[] = array(
                'role'    => $role === 'assistant' ? 'assistant' : 'user',
                'content' => $content,
            );
        }

        $messages = array_merge(
            array( array( 'role' => 'system', 'content' => $final ) ),
            $rest
        );
    }

    /**
     * Moteur Deep-Niche : génère des mots-clés via OpenRouter (expert SEO local).
     * Distribution stricte par suffixe : 40% quartier, 40% ville, 20% near me.
     * Clés JSON alignées sur la distribution : quartier (8), ville (8), near_me (4).
     *
     * @param string $metier   Métier / catégorie (ex: plombier, restaurant).
     * @param string $ville    Ville.
     * @param string $quartier Quartier ou zone (peut être vide).
     * @param string $lang     Code langue (fr, en, es).
     * @param int    $count    Nombre de mots-clés à générer (défaut 20).
     * @return array Liste de strings (mots-clés), ou tableau vide en cas d'erreur.
     */
    public static function generate_niche_keywords( $metier, $ville, $quartier = '', $lang = 'fr', $count = 20, $exclude = [] ) {
        $metier   = trim( (string) $metier );
        $ville    = trim( (string) $ville );
        $quartier = trim( (string) $quartier );
        $lang     = in_array( $lang, [ 'fr', 'en', 'es', 'de', 'it', 'pt', 'nl' ], true ) ? $lang : 'fr';
        $count    = max( 5, min( 50, (int) $count ) );
        if ( ! is_array( $exclude ) ) $exclude = [];

        if ( $metier === '' || $ville === '' ) {
            return [];
        }

        $quartier_label = $quartier !== '' ? $quartier : $ville;
        $same_as_city   = ( mb_strtolower( trim( $quartier_label ) ) === mb_strtolower( trim( $ville ) ) );

        $lang_names = [ 'fr' => 'français', 'en' => 'English', 'es' => 'español', 'de' => 'Deutsch', 'it' => 'italiano', 'pt' => 'português', 'nl' => 'Nederlands' ];
        $lang_name  = isset( $lang_names[ $lang ] ) ? $lang_names[ $lang ] : $lang;

        $near_me_label = ( $lang === 'fr' ) ? 'proche de moi' : 'near me';

        if ( $same_as_city ) {
            $system_prompt = sprintf(
                "Tu es un expert SEO LOCAL. Métier : %s. Ville : %s.\n"
                . "LANGUE OBLIGATOIRE : %s. Tous les mots-clés DOIVENT être en %s.\n\n"
                . "RÈGLE CRITIQUE : Les 20 mots-clés doivent être des services RÉELS du métier \"%s\". NE GÉNÈRE JAMAIS de mots-clés d'un autre métier.\n\n"
                . "Aucun quartier spécifique n'est fourni. Tu DOIS inventer 8 mots-clés avec des VRAIS quartiers/zones de %s.\n\n"
                . "Génère exactement 20 mots-clés de services commerciaux spécifiques au métier \"%s\" (prestations, spécialités, problématiques clients, devis, meilleur, avis).\n\n"
                . "DISTRIBUTION OBLIGATOIRE :\n"
                . "- Clé \"quartier\" : exactement 8 mots-clés. Chacun DOIT se terminer par un NOM DE QUARTIER RÉEL de %s (ex: Soho, Camden, Shoreditch, Westminster, Mayfair, Brixton, Chelsea, Notting Hill). NE PAS utiliser \"%s\" comme suffixe pour cette clé.\n"
                . "- Clé \"ville\" : exactement 8 mots-clés. Chacun DOIT se terminer par \" %s\".\n"
                . "- Clé \"near_me\" : exactement 4 mots-clés. Chacun DOIT se terminer par \" %s\".\n\n"
                . "INTERDICTIONS : Pas de mots génériques (quotes, tips, history, ideas). Pas de mots-clés d'un AUTRE métier. Uniquement des services vendables du métier \"%s\".\n\n"
                . "FORMAT : Réponds UNIQUEMENT avec un objet JSON à 3 clés : quartier, ville, near_me. Chaque clé contient un tableau de strings. Pas de texte avant ni après.\n"
                . "Exemple pour le métier \"%s\" : {\"quartier\":[\"%s agency Soho\",\"%s consultant Camden\"],\"ville\":[\"%s services %s\",\"best %s agency %s\"],\"near_me\":[\"%s agency %s\",\"best %s %s\"]}",
                $metier,
                $ville,
                strtoupper( $lang ),
                $lang_name,
                $metier,
                $ville,
                $metier,
                $ville,
                $ville,
                $ville,
                $near_me_label,
                $metier,
                $metier,
                $metier,
                $metier,
                $metier,
                $ville,
                $metier,
                $ville,
                $metier,
                $near_me_label,
                $metier,
                $near_me_label
            );
        } else {
            $system_prompt = sprintf(
                "Tu es un expert SEO LOCAL. Métier : %s. Ville : %s. Quartier : %s.\n"
                . "LANGUE OBLIGATOIRE : %s. Tous les mots-clés DOIVENT être en %s.\n\n"
                . "RÈGLE CRITIQUE : Les 20 mots-clés doivent être des services RÉELS du métier \"%s\". NE GÉNÈRE JAMAIS de mots-clés d'un autre métier.\n\n"
                . "Génère exactement 20 mots-clés de services commerciaux spécifiques au métier \"%s\" (prestations, spécialités, problématiques clients, devis, meilleur, avis).\n\n"
                . "DISTRIBUTION OBLIGATOIRE :\n"
                . "- Clé \"quartier\" : exactement 8 mots-clés. Chacun DOIT se terminer par \" %s\".\n"
                . "- Clé \"ville\" : exactement 8 mots-clés. Chacun DOIT se terminer par \" %s\".\n"
                . "- Clé \"near_me\" : exactement 4 mots-clés. Chacun DOIT se terminer par \" %s\".\n\n"
                . "INTERDICTIONS : Pas de mots génériques (quotes, tips, history, ideas). Pas de mots-clés d'un AUTRE métier. Uniquement des services vendables du métier \"%s\".\n\n"
                . "FORMAT : Réponds UNIQUEMENT avec un objet JSON à 3 clés : quartier, ville, near_me. Chaque clé contient un tableau de strings. Pas de texte avant ni après.\n"
                . "Exemple pour le métier \"%s\" : {\"quartier\":[\"%s agency %s\",\"%s consultant %s\"],\"ville\":[\"%s services %s\",\"best %s agency %s\"],\"near_me\":[\"%s agency %s\",\"best %s %s\"]}",
                $metier,
                $ville,
                $quartier_label,
                strtoupper( $lang ),
                $lang_name,
                $metier,
                $metier,
                $quartier_label,
                $ville,
                $near_me_label,
                $metier,
                $metier,
                $metier,
                $quartier_label,
                $metier,
                $quartier_label,
                $metier,
                $ville,
                $metier,
                $ville,
                $metier,
                $near_me_label,
                $metier,
                $near_me_label
            );
        }

        if ( ! empty( $exclude ) ) {
            $exclude_list = implode( ', ', array_map( function( $e ) { return '"' . addslashes( $e ) . '"'; }, array_slice( $exclude, 0, 100 ) ) );
            $system_prompt .= "\n\nMOTS-CLÉS INTERDITS (déjà en base) — ne JAMAIS les régénérer ni proposer de variantes identiques :\n[" . $exclude_list . "]\n";
        }

        $api_key = apply_filters( 'avision_openrouter_api_key', get_option( 'avision_openrouter_api_key', '' ) );
        if ( empty( $api_key ) || ! is_string( $api_key ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] generate_niche_keywords: OpenRouter API key missing.' );
            }
            return [];
        }

        $api_url = 'https://openrouter.ai/api/v1/chat/completions';
        $body    = [
            'model'       => self::MODEL_STANDARD,
            'temperature' => 0.3,
            'messages'    => [
                [ 'role' => 'system', 'content' => $system_prompt ],
                [ 'role' => 'user', 'content' => 'Génère les 20 mots-clés maintenant. JSON uniquement.' ],
            ],
        ];

        $site_url  = function_exists( 'home_url' ) ? home_url( '', 'https' ) : '';
        $site_name = function_exists( 'get_bloginfo' ) ? get_bloginfo( 'name' ) : 'AVISION';

        $response = wp_remote_post(
            $api_url,
            [
                'headers' => [
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type'  => 'application/json',
                    'HTTP-Referer'  => $site_url,
                    'X-Title'       => $site_name,
                ],
                'body'    => wp_json_encode( $body ),
                'timeout' => 30,
            ]
        );

        if ( is_wp_error( $response ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] generate_niche_keywords OpenRouter error: ' . $response->get_error_message() );
            }
            return [];
        }

        $code = wp_remote_retrieve_response_code( $response );
        $raw  = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );

        if ( $code !== 200 || empty( $data['choices'][0]['message']['content'] ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] generate_niche_keywords OpenRouter HTTP ' . $code . '. Raw (500): ' . substr( $raw, 0, 500 ) );
            }
            return [];
        }

        $content = trim( $data['choices'][0]['message']['content'] );
        if ( preg_match( '/```(?:json)?\s*([\s\S]*?)```/', $content, $m ) ) {
            $content = trim( $m[1] );
        }
        $decoded = json_decode( $content, true );

        if ( ! is_array( $decoded ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] generate_niche_keywords: invalid JSON. Content (300): ' . substr( $content, 0, 300 ) );
            }
            return [];
        }

        $keywords = [];
        if ( isset( $decoded['quartier'] ) && isset( $decoded['ville'] ) && isset( $decoded['near_me'] ) ) {
            $keywords = array_merge(
                is_array( $decoded['quartier'] ) ? $decoded['quartier'] : [],
                is_array( $decoded['ville'] ) ? $decoded['ville'] : [],
                is_array( $decoded['near_me'] ) ? $decoded['near_me'] : []
            );
        } elseif ( isset( $decoded['local_core'] ) && isset( $decoded['services'] ) && isset( $decoded['intentions'] ) ) {
            $keywords = array_merge(
                is_array( $decoded['local_core'] ) ? $decoded['local_core'] : [],
                is_array( $decoded['services'] ) ? $decoded['services'] : [],
                is_array( $decoded['intentions'] ) ? $decoded['intentions'] : []
            );
        } elseif ( isset( $decoded['keywords'] ) && is_array( $decoded['keywords'] ) ) {
            $keywords = $decoded['keywords'];
        } else {
            $flat = [];
            array_walk_recursive( $decoded, function( $v ) use ( &$flat ) { if ( is_string( $v ) ) $flat[] = $v; } );
            $keywords = $flat;
        }

        $out = [];
        foreach ( $keywords as $kw ) {
            $kw = is_string( $kw ) ? trim( $kw ) : '';
            if ( $kw !== '' ) {
                $out[] = $kw;
            }
        }
        $result = array_slice( array_unique( $out ), 0, $count );

        // Validation de la distribution : log d'avertissement si hors tolérance
        if ( ! empty( $result ) ) {
            $nm_lower = mb_strtolower( $near_me_label );
            $v_lower  = mb_strtolower( $ville );
            $cnt_q = 0; $cnt_v = 0; $cnt_nm = 0; $cnt_localized = 0;
            foreach ( $result as $kw ) {
                $kw_l = mb_strtolower( $kw );
                if ( mb_substr( $kw_l, -mb_strlen( $nm_lower ) ) === $nm_lower ) {
                    $cnt_nm++;
                } elseif ( $same_as_city ) {
                    if ( mb_substr( $kw_l, -mb_strlen( $v_lower ) ) === $v_lower ) { $cnt_v++; $cnt_localized++; }
                    else { $cnt_q++; $cnt_localized++; }
                } else {
                    $q_lower = mb_strtolower( $quartier_label );
                    if ( mb_substr( $kw_l, -mb_strlen( $q_lower ) ) === $q_lower ) { $cnt_q++; }
                    elseif ( mb_substr( $kw_l, -mb_strlen( $v_lower ) ) === $v_lower ) { $cnt_v++; }
                }
            }
            $total = count( $result );
            if ( $same_as_city ) {
                error_log( '[AVI] generate_niche_keywords: ' . $total . ' keywords for ' . $metier . ' @ ' . $ville . ' (same city mode) — distribution: ville=' . $cnt_v . ', quartiers_locaux=' . $cnt_q . ', near_me=' . $cnt_nm );
                if ( ( $cnt_v + $cnt_q ) < 12 || $cnt_nm < 2 ) {
                    error_log( '[AVI] WARNING: distribution hors tolérance (attendu ~16 localisés + ~4 near_me). localisés=' . ( $cnt_v + $cnt_q ) . ', near_me=' . $cnt_nm );
                }
            } else {
                error_log( '[AVI] generate_niche_keywords: ' . $total . ' keywords for ' . $metier . ' @ ' . $ville . ' — distribution: quartier=' . $cnt_q . ', ville=' . $cnt_v . ', near_me=' . $cnt_nm );
                if ( $cnt_q < 6 || $cnt_v < 6 || $cnt_nm < 2 ) {
                    error_log( '[AVI] WARNING: distribution hors tolérance (attendu ~8/8/4). quartier=' . $cnt_q . ', ville=' . $cnt_v . ', near_me=' . $cnt_nm );
                }
            }
        }

        return $result;
    }

    /**
     * Texte d'un Google Post (Bulk Publisher agence) via OpenRouter.
     *
     * @param string $subject Consigne / sujet (texte brut).
     * @param string $business_sector Catégories Google / secteur (optionnel, pour adapter le ton).
     * @param string[] $google_categories Liste des catégories Google distinctes des fiches sélectionnées (prioritaire pour le contexte métier).
     * @return string Corps du post (max 1500 caractères) ou chaîne vide si échec.
     */
    public static function generate_bulk_google_post_text( $subject, $business_sector = '', $google_categories = array(), $lang = 'fr' ) {
        $subject = is_string( $subject ) ? trim( wp_strip_all_tags( $subject ) ) : '';
        $subject = preg_replace( '/\s+/u', ' ', $subject );
        if ( $subject === '' || ( function_exists( 'mb_strlen' ) ? mb_strlen( $subject ) : strlen( $subject ) ) < 2 ) {
            return '';
        }
        $max_in = 600;
        if ( function_exists( 'mb_strlen' ) && mb_strlen( $subject ) > $max_in ) {
            $subject = mb_substr( $subject, 0, $max_in );
        } elseif ( strlen( $subject ) > $max_in ) {
            $subject = substr( $subject, 0, $max_in );
        }

        $business_sector = is_string( $business_sector ) ? trim( wp_strip_all_tags( $business_sector ) ) : '';
        $business_sector = preg_replace( '/\s+/u', ' ', $business_sector );
        $max_sec        = 400;
        if ( $business_sector !== '' ) {
            if ( function_exists( 'mb_strlen' ) && mb_strlen( $business_sector ) > $max_sec ) {
                $business_sector = mb_substr( $business_sector, 0, $max_sec );
            } elseif ( strlen( $business_sector ) > $max_sec ) {
                $business_sector = substr( $business_sector, 0, $max_sec );
            }
        }

        $api_key = apply_filters( 'avision_openrouter_api_key', get_option( 'avision_openrouter_api_key', '' ) );
        if ( empty( $api_key ) || ! is_string( $api_key ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] generate_bulk_google_post_text: OpenRouter API key missing.' );
            }
            return '';
        }

        $cats_clean = array();
        if ( is_array( $google_categories ) ) {
            foreach ( $google_categories as $c ) {
                $c = is_string( $c ) ? trim( wp_strip_all_tags( $c ) ) : '';
                $c = preg_replace( '/\s+/u', ' ', $c );
                if ( $c !== '' && ! in_array( $c, $cats_clean, true ) ) {
                    $cats_clean[] = $c;
                }
            }
        }
        $cats_join = implode( ' · ', $cats_clean );
        $max_cat_blob = 500;
        if ( function_exists( 'mb_strlen' ) && mb_strlen( $cats_join ) > $max_cat_blob ) {
            $cats_join = mb_substr( $cats_join, 0, $max_cat_blob );
        } elseif ( strlen( $cats_join ) > $max_cat_blob ) {
            $cats_join = substr( $cats_join, 0, $max_cat_blob );
        }

        $sector_ctx = '';
        if ( $cats_join !== '' ) {
            $sector_ctx .= ' PRIORITÉ ABSOLUE — Catégories Google réellement sélectionnées dans le tableau de bord agence (liste ci-dessous). '
                . 'Tu dois calibrer le vocabulaire, les bénéfices et l’accroche sur ces catégories ; n’invente pas un autre métier. '
                . 'Liste : « '
                . $cats_join
                . ' ». Si plusieurs catégories sont listées, reste pertinent pour une diffusion multi-établissements sans citer chaque libellé de façon mécanique.';
        }
        if ( $business_sector !== '' ) {
            $sector_ctx .= ' Contexte complémentaire (secteur / consigne agence) : « '
                . $business_sector
                . ' ».';
        }
        if ( $sector_ctx !== '' ) {
            $sector_ctx .= ' Adapte le ton et les arguments à ce périmètre métier local (Google Business Profile).';
        }
        if ( $cats_join === '' && $business_sector !== '' ) {
            $sector_ctx .= ' Aucune catégorie Google distincte n’a été transmise : déduis le métier uniquement à partir du contexte complémentaire ci-dessus et du sujet.';
        }

        // ANTI-RÉGRESSION [06 juin 2026] — Prompt trilingue selon langue UI agence
        $lang = in_array( $lang, array( 'fr', 'en', 'es' ), true ) ? $lang : 'fr';
        $system_prompts = array(
            'fr' => 'Tu es AVI, coach VIP Local SEO (Google Business Profile). Rédige un Google Post (max 1500 caractères) sur le sujet suivant : '
                . $subject . $sector_ctx
                . ' Le ton doit être professionnel, engageant, et optimisé pour inciter au clic. Ne mets pas de hashtags, ni d\'emojis superflus. Rédige uniquement le texte du post.',
            'en' => 'You are AVI, a VIP Local SEO coach (Google Business Profile). Write a Google Post (max 1500 characters) on the following topic: '
                . $subject . $sector_ctx
                . ' The tone must be professional, engaging, and optimized to drive clicks. No hashtags, no unnecessary emojis. Write only the post body text.',
            'es' => 'Eres AVI, coach VIP de SEO Local (Google Business Profile). Escribe un Google Post (máx. 1500 caracteres) sobre el siguiente tema: '
                . $subject . $sector_ctx
                . ' El tono debe ser profesional, atractivo y optimizado para generar clics. Sin hashtags ni emojis innecesarios. Escribe únicamente el cuerpo del post.',
        );
        $system_prompt = $system_prompts[ $lang ];

        $user_messages = array(
            'fr' => 'Rédige le post maintenant, en respectant la limite de 1500 caractères. Aucun titre, aucun préambule : uniquement le corps du post.',
            'en' => 'Write the post now, within the 1500 character limit. No title, no preamble: only the post body.',
            'es' => 'Escribe el post ahora, respetando el límite de 1500 caracteres. Sin título, sin preámbulo: solo el cuerpo del post.',
        );

        $route   = self::get_model_and_temperature( $subject, 'bulk_google_post' );
        $api_url = 'https://openrouter.ai/api/v1/chat/completions';
        $body    = [
            'model'       => $route['model'],
            'temperature' => min( 0.7, (float) $route['temperature'] ),
            'messages'    => [
                [ 'role' => 'system', 'content' => $system_prompt ],
                [ 'role' => 'user', 'content' => $user_messages[ $lang ] ],
            ],
        ];

        $site_url  = function_exists( 'home_url' ) ? home_url( '', 'https' ) : '';
        $site_name = function_exists( 'get_bloginfo' ) ? get_bloginfo( 'name' ) : 'AVISION';

        $response = wp_remote_post(
            $api_url,
            [
                'headers' => [
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type'  => 'application/json',
                    'HTTP-Referer'  => $site_url,
                    'X-Title'       => $site_name,
                ],
                'body'    => wp_json_encode( $body ),
                'timeout' => 45,
            ]
        );

        if ( is_wp_error( $response ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] generate_bulk_google_post_text OpenRouter error: ' . $response->get_error_message() );
            }
            return '';
        }

        $code = wp_remote_retrieve_response_code( $response );
        $raw  = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );

        if ( $code !== 200 || empty( $data['choices'][0]['message']['content'] ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] generate_bulk_google_post_text OpenRouter HTTP ' . $code . '. Raw (400): ' . substr( $raw, 0, 400 ) );
            }
            return '';
        }

        $content = trim( (string) $data['choices'][0]['message']['content'] );
        if ( preg_match( '/```[\w]*\s*([\s\S]*?)```/', $content, $m ) ) {
            $content = trim( $m[1] );
        }

        $max_out = 1500;
        if ( function_exists( 'mb_strlen' ) && mb_strlen( $content ) > $max_out ) {
            $content = mb_substr( $content, 0, $max_out );
        } elseif ( strlen( $content ) > $max_out ) {
            $content = substr( $content, 0, $max_out );
        }

        return $content;
    }

    /**
     * Traduction entre deux langues (FR/EN/ES) pour champs HTML Knowledge Center.
     *
     * @param string $text        Texte source.
     * @param string $source_lang fr|en|es.
     * @param string $target_lang fr|en|es.
     * @return string Chaîne vide si échec.
     */
    public static function translate_knowledge_center_between_langs( $text, $source_lang, $target_lang ) {
        $text = is_string( $text ) ? str_replace( "\0", '', $text ) : '';
        $source_lang = strtolower( trim( (string) $source_lang ) );
        $target_lang = strtolower( trim( (string) $target_lang ) );
        if ( ! in_array( $source_lang, array( 'fr', 'en', 'es' ), true ) || ! in_array( $target_lang, array( 'fr', 'en', 'es' ), true ) ) {
            return '';
        }
        if ( $source_lang === $target_lang ) {
            return trim( $text );
        }
        if ( trim( $text ) === '' ) {
            return '';
        }
        $max_in = 50000;
        if ( function_exists( 'mb_strlen' ) && mb_strlen( $text ) > $max_in ) {
            $text = mb_substr( $text, 0, $max_in );
        } elseif ( strlen( $text ) > $max_in ) {
            $text = substr( $text, 0, $max_in );
        }

        $lang_names = array(
            'fr' => 'French',
            'en' => 'English',
            'es' => 'Spanish',
        );
        $src_n = isset( $lang_names[ $source_lang ] ) ? $lang_names[ $source_lang ] : 'French';
        $tgt_n = isset( $lang_names[ $target_lang ] ) ? $lang_names[ $target_lang ] : 'English';
        $user_msg  = 'Translate the following ' . $src_n . ' text into ' . $tgt_n . '. Keep the exact same HTML formatting, emojis, and variables like ${items}. Only output the translation, nothing else. Text: ' . $text;

        $api_key = apply_filters( 'avision_openrouter_api_key', get_option( 'avision_openrouter_api_key', '' ) );
        if ( empty( $api_key ) || ! is_string( $api_key ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] translate_knowledge_center_fragment: OpenRouter API key missing.' );
            }
            return '';
        }

        $api_url = 'https://openrouter.ai/api/v1/chat/completions';
        $body    = array(
            'model'       => self::MODEL_STANDARD,
            'temperature' => 0.2,
            'messages'    => array(
                array(
                    'role'    => 'system',
                    'content' => 'You are a professional translator. Follow the user instructions exactly. Never add explanations or preambles.',
                ),
                array(
                    'role'    => 'user',
                    'content' => $user_msg,
                ),
            ),
        );

        $site_url  = function_exists( 'home_url' ) ? home_url( '', 'https' ) : '';
        $site_name = function_exists( 'get_bloginfo' ) ? get_bloginfo( 'name' ) : 'AVISION';

        $response = wp_remote_post(
            $api_url,
            array(
                'headers' => array(
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type'  => 'application/json',
                    'HTTP-Referer'  => $site_url,
                    'X-Title'       => $site_name,
                ),
                'body'    => wp_json_encode( $body ),
                'timeout' => 120,
            )
        );

        if ( is_wp_error( $response ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] translate_knowledge_center_fragment: ' . $response->get_error_message() );
            }
            return '';
        }

        $code = wp_remote_retrieve_response_code( $response );
        $raw  = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );

        if ( $code !== 200 || empty( $data['choices'][0]['message']['content'] ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( '[AVI] translate_knowledge_center_fragment HTTP ' . $code . ' body: ' . substr( (string) $raw, 0, 400 ) );
            }
            return '';
        }

        $content = trim( (string) $data['choices'][0]['message']['content'] );
        if ( preg_match( '/```[\w]*\s*([\s\S]*?)```/', $content, $m ) ) {
            $content = trim( $m[1] );
        }

        return $content;
    }

    /**
     * Traduction FR → EN ou ES (alias de {@see translate_knowledge_center_between_langs()}).
     *
     * @param string $french_text Texte source FR (HTML conservé).
     * @param string $target_lang en|es.
     * @return string Traduction ou chaîne vide si échec / clé absente.
     */
    public static function translate_knowledge_center_fragment( $french_text, $target_lang ) {
        return self::translate_knowledge_center_between_langs( $french_text, 'fr', $target_lang );
    }
}
