<?php
/**
 * Classe API Avision - Proxy vers FastAPI (engine.avision.org)
 */

namespace Avision;

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

class Avision_API {

    public function init() {
        // Compatibilité avec les appels existants (ex. class-ajax-router)
    }

    /**
     * En-têtes multi-tenant pour les requêtes vers le backend Python : X-Profile-UUID (pid) et X-Agency-UUID (aid).
     * SÉCURITÉ : n'envoie JAMAIS l'ID numérique au backend, uniquement l'UUID (format 32 ou 36 caractères).
     * Permet au moteur de vérifier que l'agence a le droit d'accéder aux données du client (prévention IDOR).
     *
     * @param \WP_REST_Request|array|null $request Requête REST (get_param) ou tableau de params (profile_uuid, agency_uuid / pid, aid).
     * @return array Headers à merger avec Content-Type, Host, etc.
     */
    private static function get_backend_uuid_headers($request = null) {
        $profile_uuid = '';
        $agency_uuid  = '';
        if ($request instanceof \WP_REST_Request) {
            $profile_uuid = $request->get_param('pid') ?: $request->get_param('profile_uuid');
            $agency_uuid  = $request->get_param('aid') ?: $request->get_param('agency_uuid');
            if (is_array($profile_uuid)) $profile_uuid = '';
            if (is_array($agency_uuid)) $agency_uuid = '';
            $profile_uuid = is_string($profile_uuid) ? trim($profile_uuid) : '';
            $agency_uuid  = is_string($agency_uuid) ? trim($agency_uuid) : '';
        } elseif (is_array($request)) {
            $profile_uuid = isset($request['profile_uuid']) ? trim((string) $request['profile_uuid']) : (isset($request['pid']) ? trim((string) $request['pid']) : '');
            $agency_uuid  = isset($request['agency_uuid']) ? trim((string) $request['agency_uuid']) : (isset($request['aid']) ? trim((string) $request['aid']) : '');
        }
        // Ne jamais envoyer d'ID numérique dans les headers : uniquement des UUID (32 hex ou 36 avec tirets).
        if ($profile_uuid !== '' && (ctype_digit((string) $profile_uuid) || ! self::is_valid_uuid($profile_uuid))) {
            $profile_uuid = '';
        }
        if ($agency_uuid !== '' && (ctype_digit((string) $agency_uuid) || ! self::is_valid_uuid($agency_uuid))) {
            $agency_uuid = '';
        }
        if ($profile_uuid === '' && function_exists('avi_get_active_profile') && function_exists('avi_get_or_create_profile_uuid')) {
            $pid = avi_get_active_profile();
            if ($pid) $profile_uuid = avi_get_or_create_profile_uuid($pid);
        }
        if ($agency_uuid === '' && function_exists('avi_get_agency_uuid_for_user')) {
            $agency_uuid = avi_get_agency_uuid_for_user();
        }
        $h = array('Host' => 'engine.avision.org');
        if (self::is_valid_uuid($profile_uuid)) $h['X-Profile-UUID'] = $profile_uuid;
        if (self::is_valid_uuid($agency_uuid)) $h['X-Agency-UUID'] = $agency_uuid;
        return $h;
    }

    /**
     * Vérifie qu'une chaîne est un UUID valide (36 car. avec tirets ou 32 car. hex).
     * Utilisé pour n'envoyer que des UUID au backend, jamais d'ID numériques.
     *
     * @param string $value Valeur à tester.
     * @return bool
     */
    private static function is_valid_uuid($value) {
        if (!is_string($value) || $value === '') return false;
        $v = trim($value);
        return (bool) (preg_match('/^[a-f0-9\-]{36}$/i', $v) || (strlen($v) === 32 && preg_match('/^[a-f0-9]+$/i', $v)));
    }

    /**
     * Log diagnostic WordPress → FastAPI /chat (mute / 502).
     *
     * @param string $chat_url URL complète (ex. http://127.0.0.1:8001/chat).
     * @param string $json_body Corps JSON déjà encodé.
     */
    private static function log_fastapi_chat_outbound($chat_url, $json_body) {
        if (!function_exists('error_log')) {
            return;
        }
        $raw = is_string($json_body) ? $json_body : '';
        $len = strlen($raw);
        $max = 8000;
        $snippet = ($len > $max) ? substr($raw, 0, $max) . '…[tronqué, total=' . $len . ' octets]' : $raw;
        error_log('[AVI-FASTAPI-CHAT-OUT] url=' . $chat_url . ' body_bytes=' . $len . ' body_json=' . $snippet);
    }

    /**
     * Log réponse FastAPI → WordPress (corps vide, HTTP non-2xx, erreur transport).
     *
     * @param string      $chat_url  URL appelée.
     * @param string      $phase     primary|retry.
     * @param int|string  $http_code Code HTTP ou 0 si inconnu.
     * @param string      $body      Corps brut.
     * @param string      $wp_error  Message WP_Error transport, ou chaîne vide.
     */
    private static function log_fastapi_chat_inbound($chat_url, $phase, $http_code, $body, $wp_error = '') {
        if (!function_exists('error_log')) {
            return;
        }
        if ($wp_error !== '') {
            error_log('[AVI-FASTAPI-CHAT-IN][' . $phase . '] transport_wp_error url=' . $chat_url . ' detail=' . $wp_error . ' — Cause fréquente: backend arrêté, port 8001 fermé, ou hôte PHP incapable de joindre 127.0.0.1 (conteneur / réseau).');
            return;
        }
        $blen = is_string($body) ? strlen($body) : 0;
        $excerpt = is_string($body) ? substr(trim($body), 0, 600) : '';
        error_log('[AVI-FASTAPI-CHAT-IN][' . $phase . '] http=' . (string) $http_code . ' body_bytes=' . $blen . ' excerpt=' . $excerpt);
        if ($blen === 0) {
            if (is_numeric($http_code) && (int) $http_code >= 200 && (int) $http_code < 300) {
                error_log('[AVI-FASTAPI-CHAT-IN][' . $phase . '] Raison (logs WP): HTTP succès mais corps vide — vérifier que FastAPI /chat renvoie bien du JSON (reply) et qu’aucun proxy ne tronque la réponse.');
            } else {
                error_log('[AVI-FASTAPI-CHAT-IN][' . $phase . '] Raison (logs WP): corps vide avec HTTP ' . (string) $http_code . ' — erreur application ou gateway devant le backend.');
            }
        }
        if (!is_numeric($http_code) || (int) $http_code < 200 || (int) $http_code >= 300) {
            error_log('[AVI-FASTAPI-CHAT-IN][' . $phase . '] Réponse HTTP anormale — le dashboard peut afficher AVI muet ou une erreur JSON côté client.');
        }
    }

    /**
     * Bloc trilingue (FR / EN / ES) pour une option KC fallback de base (sans suffixe _en/_es).
     *
     * @param string $base_option ex. avi_kc_fallback_greetings
     * @return array{fr:string,en:string,es:string}
     */
    private static function kc_fallback_locale_triple($base_option) {
        $fr = get_option($base_option, '');
        $en = get_option($base_option . '_en', '');
        $es = get_option($base_option . '_es', '');
        $fr_s = is_string($fr) ? $fr : '';
        $en_s = is_string($en) ? $en : '';
        $es_s = is_string($es) ? $es : '';
        return array(
            'fr' => $fr_s,
            'en' => $en_s,
            'es' => $es_s,
        );
    }

    /**
     * Même logique que {@see kc_fallback_locale_triple()} — nom générique pour tout champ KC base + _en/_es.
     *
     * @param string $base_option ex. avi_kc_gbp_foundations
     * @return array{fr:string,en:string,es:string}
     */
    private static function kc_option_locale_triple($base_option) {
        return self::kc_fallback_locale_triple($base_option);
    }

    /**
     * Réponse LLM / FastAPI : ne pas appliquer wp_kses ni strip_tags (balises &lt;span class="avi-…"&gt;, markdown, etc.).
     * Normalisation UTF-8 uniquement pour éviter une réponse JSON invalide.
     *
     * @param mixed $value
     * @return mixed
     */
    private static function passthrough_llm_string($value) {
        if (! is_string($value)) {
            return $value;
        }
        return wp_check_invalid_utf8($value, true);
    }

    /**
     * @param array $data Charge utile décodée depuis FastAPI (reply, meta_level_title, …).
     * @return array
     */
    private static function passthrough_fastapi_chat_response_array(array $data) {
        foreach (array('reply', 'meta_level_title') as $key) {
            if (isset($data[ $key ]) && is_string($data[ $key ])) {
                $data[ $key ] = self::passthrough_llm_string($data[ $key ]);
            }
        }
        /**
         * Filtre : ne pas y appliquer wp_kses sur reply (préserver le HTML / markdown côté client).
         */
        return apply_filters('avision_fastapi_chat_response_data', $data);
    }

    /**
     * Détecte HELLO_INIT (seul ou après « Question utilisateur : » dans le message chat).
     *
     * @param string $message Message brut (idéalement avec retours ligne conservés).
     * @return bool
     */
    private static function message_is_hello_init($message) {
        if (!is_string($message) || trim($message) === '') {
            return false;
        }
        $t = trim($message);
        if ($t === 'HELLO_INIT') {
            return true;
        }
        return (bool) preg_match('/Question\s+utilisateur\s*:\s*HELLO_INIT\b/u', $message);
    }

    /**
     * Langue pour le JSON /chat → FastAPI : uniquement les champs explicites `language` / `lang` de la requête.
     * Ne pas utiliser get_locale(), WPLANG ni la langue d’administration WordPress.
     *
     * @param array $params Corps JSON ou tableau AJAX (clés language, lang).
     * @return string fr|en|es
     */
    public static function normalize_chat_language_for_fastapi(array $params) {
        $raw = '';
        if (isset($params['language']) && $params['language'] !== '' && $params['language'] !== null) {
            $raw = strtolower(trim((string) $params['language']));
        } elseif (isset($params['lang']) && $params['lang'] !== '' && $params['lang'] !== null) {
            $raw = strtolower(trim((string) $params['lang']));
        }
        if ($raw !== '') {
            $parts = preg_split('/[-_]/', $raw, 2);
            $raw = isset($parts[0]) ? strtolower(trim((string) $parts[0])) : $raw;
            $raw = substr($raw, 0, 2);
        }
        // Repli : ?lang= / ?language= sur l’URL de la page (Referer). Jamais get_locale() ni locale d’admin WP.
        if ($raw === '' && !empty($_SERVER['HTTP_REFERER']) && is_string($_SERVER['HTTP_REFERER'])) {
            $q = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY);
            if (is_string($q) && $q !== '') {
                $rq = array();
                parse_str($q, $rq);
                $ref_raw = '';
                if (isset($rq['lang']) && $rq['lang'] !== '' && $rq['lang'] !== null) {
                    $ref_raw = strtolower(trim((string) $rq['lang']));
                } elseif (isset($rq['language']) && $rq['language'] !== '' && $rq['language'] !== null) {
                    $ref_raw = strtolower(trim((string) $rq['language']));
                }
                if ($ref_raw !== '') {
                    $rp = preg_split('/[-_]/', $ref_raw, 2);
                    $raw = isset($rp[0]) ? strtolower(trim((string) $rp[0])) : $ref_raw;
                    $raw = substr($raw, 0, 2);
                }
            }
        }
        if (in_array($raw, array('fr', 'en', 'es'), true)) {
            return $raw;
        }
        return 'en';
    }

    /**
     * Accueil chat Fleet (HELLO_INIT) selon la langue : KC FR + variantes EN/ES optionnelles, sinon pont translate_reply_from_french.
     *
     * @param string $language fr|en|es
     * @return string Vide si aucun texte FR configuré dans le KC.
     */
    public static function get_b2b_hello_reply_for_language($language) {
        $language = strtolower((string) $language);
        if (!in_array($language, array('fr', 'en', 'es'), true)) {
            $language = 'en';
        }
        $welcome_b2b = trim((string) get_option('avi_prompt_b2b_welcome', ''));
        if ($welcome_b2b === '') {
            return '';
        }
        if ($language === 'en') {
            $welcome_en = trim((string) get_option('avi_prompt_b2b_welcome_en', ''));
            return $welcome_en !== '' ? $welcome_en : self::translate_reply_from_french($welcome_b2b, 'en');
        }
        if ($language === 'es') {
            $welcome_es = trim((string) get_option('avi_prompt_b2b_welcome_es', ''));
            return $welcome_es !== '' ? $welcome_es : self::translate_reply_from_french($welcome_b2b, 'es');
        }
        return $welcome_b2b;
    }

    /**
     * Niveau 1–21 pour le moteur ; B2B sans niveau explicite → null (pas de logique L1–L21).
     *
     * @param array  $params
     * @param string $user_type b2c|b2b
     * @return int|null
     */
    private static function resolve_chat_level_from_params(array $params, $user_type) {
        $user_type = ($user_type === 'b2b') ? 'b2b' : 'b2c';
        $raw = null;
        foreach (array('currentLevel', 'level') as $key) {
            if (!isset($params[$key]) || $params[$key] === '' || $params[$key] === null) {
                continue;
            }
            $v = (int) $params[$key];
            if ($v >= 1 && $v <= 21) {
                $raw = $v;
                break;
            }
        }
        if ($user_type === 'b2b') {
            return $raw;
        }
        return ($raw !== null) ? $raw : 1;
    }

    /**
     * Dernier kilomètre : impose systemMessage = KC (PHP) en tête + « --- Contexte Session --- » + JS brut requête.
     * Duplique sous clés snake_case et avi_system_prompt_wordpress_final pour que FastAPI prenne ce bloc en priorité.
     *
     * @param array  $payload    Réf. payload JSON vers /chat.
     * @param array  $params     Corps JSON d’origine (systemMessage = texte JS seul).
     * @param string $user_type  b2c|b2b.
     * @param string $sector
     * @param int    $level      1–21.
     */
    private static function apply_absolute_final_chat_system_prompt(array &$payload, array $params, $user_type, $sector, $level) {
        $session_js = isset($params['systemMessage']) && is_string($params['systemMessage']) ? trim($params['systemMessage']) : '';
        // Uniquement la langue transmise par le client (pas la locale WordPress).
        $lang_kc = self::normalize_chat_language_for_fastapi(is_array($params) ? $params : array());
        $final_system_prompt = '';
        if (class_exists('\Avision\Avision_OpenRouter_Router')) {
            $final_system_prompt = \Avision\Avision_OpenRouter_Router::build_chat_system_prompt(array(
                'user_type'      => $user_type,
                'sector'         => $sector,
                'level'          => $level,
                'client_context' => $session_js,
                'language'       => $lang_kc,
            ));
        }
        if ($final_system_prompt === '' && $session_js !== '') {
            $final_system_prompt = $session_js;
        }
        if ( ! class_exists( '\Avision\Avision_OpenRouter_Router' ) && class_exists( '\Avision\Avision_AI_Interactions' ) ) {
            $addon = \Avision\Avision_AI_Interactions::get_prompt_addon();
            if ( $addon !== '' ) {
                $final_system_prompt = trim( ( $final_system_prompt !== '' ? $final_system_prompt : $session_js ) . "\n\n" . $addon );
            }
        }
        if ($final_system_prompt !== '') {
            $payload['systemMessage'] = $final_system_prompt;
            $payload['system_message'] = $final_system_prompt;
            $payload['avi_system_prompt_wordpress_final'] = $final_system_prompt;
        } else {
            unset($payload['systemMessage'], $payload['system_message'], $payload['avi_system_prompt_wordpress_final']);
        }
        // Chat Fleet / Dashboard Agence : payload JSON vers FastAPI (clé gbp_master_knowledge + user_type b2b).
        $is_b2b_agency_chat = ($user_type === 'b2b');
        if (!$is_b2b_agency_chat && isset($params['dashboard_type']) && is_string($params['dashboard_type'])) {
            $dt_ag = strtolower(trim($params['dashboard_type']));
            if (in_array($dt_ag, array('b2b', 'agency', 'fleet', 'b2b_agency'), true)) {
                $is_b2b_agency_chat = true;
            }
        }
        // B2C : encyclopédie KC (clé historique avi_prompt_b2c_encyclopedia + _en / _es) remplie → le backend Python ne doit pas réinjecter avi-encyclopedia.json par-dessus.
        if ($user_type === 'b2c' && function_exists('get_option')) {
            $enc_fr = trim((string) get_option('avi_prompt_b2c_encyclopedia', ''));
            $enc_en = trim((string) get_option('avi_prompt_b2c_encyclopedia_en', ''));
            $enc_es = trim((string) get_option('avi_prompt_b2c_encyclopedia_es', ''));
            if ($enc_fr !== '' || $enc_en !== '' || $enc_es !== '') {
                $payload['avi_skip_static_encyclopedia'] = true;
            }
            $gbp_opts = array(
                'avi_kc_gbp_foundations',
                'avi_kc_gbp_algorithm',
                'avi_kc_gbp_guidelines',
                'avi_kc_gbp_emergencies',
                'avi_kc_gbp_reputation',
                'avi_kc_gbp_advanced',
            );
            $gbp_any = false;
            foreach ($gbp_opts as $go) {
                if (trim((string) get_option($go, '')) !== ''
                    || trim((string) get_option($go . '_en', '')) !== ''
                    || trim((string) get_option($go . '_es', '')) !== '') {
                    $gbp_any = true;
                    break;
                }
            }
            if ($gbp_any) {
                $payload['avi_academy_from_wordpress'] = true;
            }
            if (class_exists('\Avision\Avision_OpenRouter_Router')) {
                $rw_resolved = trim((string) \Avision\Avision_OpenRouter_Router::kc_pick_locale_text('avi_kc_restricted_words', $lang_kc));
                if ($rw_resolved !== '') {
                    $payload['avi_kc_restricted_words'] = $rw_resolved;
                }
            }
        }
        // Blocs trilingues structurés (FR / EN / ES) pour le moteur Python — complément des chaînes déjà résolues dans le prompt.
        if (function_exists('get_option')) {
            $kc_tri = array(
                'avi_prompt_system'             => self::kc_option_locale_triple('avi_prompt_system'),
                'avi_kc_restricted_words'       => self::kc_option_locale_triple('avi_kc_restricted_words'),
                'avi_prompt_b2c_encyclopedia'   => self::kc_option_locale_triple('avi_prompt_b2c_encyclopedia'),
                'gbp_foundations'               => self::kc_option_locale_triple('avi_kc_gbp_foundations'),
                'gbp_algorithm'                 => self::kc_option_locale_triple('avi_kc_gbp_algorithm'),
                'gbp_guidelines'                => self::kc_option_locale_triple('avi_kc_gbp_guidelines'),
                'gbp_emergencies'               => self::kc_option_locale_triple('avi_kc_gbp_emergencies'),
                'gbp_reputation'                => self::kc_option_locale_triple('avi_kc_gbp_reputation'),
                'gbp_advanced'                  => self::kc_option_locale_triple('avi_kc_gbp_advanced'),
                'b2b_fleet_management'          => self::kc_option_locale_triple('avi_kc_b2b_fleet_management'),
                'b2b_roi_churn'                 => self::kc_option_locale_triple('avi_kc_b2b_roi_churn'),
                'b2b_senior_strategy'           => self::kc_option_locale_triple('avi_kc_b2b_senior_strategy'),
                'avi_kc_b2b_encyclopedia'       => self::kc_option_locale_triple('avi_kc_b2b_encyclopedia'),
                'avi_prompt_b2b_global'         => self::kc_option_locale_triple('avi_prompt_b2b_global'),
                'avi_prompt_b2b_welcome'        => self::kc_option_locale_triple('avi_prompt_b2b_welcome'),
            );
            for ($ln = 1; $ln <= 21; $ln++) {
                $b = 'avi_kc_b2c_level_' . $ln;
                $kc_tri['level_' . $ln] = self::kc_option_locale_triple($b);
                $kc_tri['level_' . $ln . '_infusion'] = self::kc_option_locale_triple('avi_kc_b2c_level_' . $ln . '_infusion');
            }
            $payload['kc_trilingual'] = $kc_tri;
        }
        // B2C + Fleet Assistant (B2B) : corpus GBP 6 piliers dans le body JSON (directive d’injection côté FastAPI), langue alignée sur le chat.
        if (($user_type === 'b2c' || $is_b2b_agency_chat) && class_exists('\Avision\Avision_OpenRouter_Router')) {
            $gbp_master_knowledge = \Avision\Avision_OpenRouter_Router::get_gbp_master_knowledge_block($lang_kc);
            if ($gbp_master_knowledge !== '') {
                $payload['gbp_master_knowledge'] = $gbp_master_knowledge;
            }
        }
        if ($is_b2b_agency_chat && class_exists('\Avision\Avision_OpenRouter_Router')) {
            $b2b_master_knowledge = \Avision\Avision_OpenRouter_Router::get_b2b_master_knowledge_block($lang_kc);
            if ($b2b_master_knowledge !== '') {
                $payload['b2b_master_knowledge'] = $b2b_master_knowledge;
            }
        }
        $log_chunk = $final_system_prompt;
        if (is_string($log_chunk) && strlen($log_chunk) > 20000) {
            $log_chunk = substr($log_chunk, 0, 20000) . "\n... [AVI PROMPT FINAL tronqué log]\n";
        }
        error_log('[AVI PROMPT FINAL] ' . print_r($log_chunk, true));
    }

    /**
     * Nettoie l’objet KPI flotte envoyé au moteur (JSON shallow, pas de dump client complet).
     *
     * @param array $fk
     * @return array
     */
    /**
     * Accepte camelCase accidentel depuis le client et le mappe en snake_case attendu par le moteur.
     *
     * @param array $fk
     * @return array
     */
    private static function normalize_fleet_kpis_incoming(array $fk) {
        $aliases = array(
            'managedListingsCount'   => 'managed_listings_count',
            'slotsUsed'              => 'slots_used',
            'slotsQuota'             => 'slots_quota',
            'agencyTier'             => 'agency_tier',
            'visibilityTier'         => 'visibility_tier',
            'criticalCount'          => 'critical_count',
            'opportunitiesCount'     => 'opportunities_count',
            'underProtectionCount'   => 'under_protection_count',
            'unmonitoredCount'       => 'unmonitored_count',
            'googleConnected'        => 'google_connected',
            'snapshotAt'             => 'snapshot_at',
        );
        foreach ($aliases as $from => $to) {
            if (!array_key_exists($to, $fk) && array_key_exists($from, $fk)) {
                $fk[$to] = $fk[$from];
            }
        }
        return $fk;
    }

    /**
     * Rehausse les compteurs flotte depuis la BDD si le payload JS est vide ou sous-estimé.
     *
     * @param int   $user_id ID WP agence.
     * @param array $kpis    KPIs déjà sanitizés.
     * @return array
     */
    private static function merge_agency_fleet_kpis_with_db($user_id, array $kpis) {
        $user_id = (int) $user_id;
        $ds = isset($kpis['fleet_kpis_dom_sourced']) ? $kpis['fleet_kpis_dom_sourced'] : null;
        if ($ds === true || $ds === 1 || $ds === '1' || $ds === 'true' || $ds === 'yes') {
            return $kpis;
        }
        if ($user_id <= 0 || !function_exists('avi_chat_merge_agency_fleet_kpis_from_db')) {
            return $kpis;
        }
        return avi_chat_merge_agency_fleet_kpis_from_db($user_id, $kpis);
    }

    private static function sanitize_fleet_kpis_for_chat(array $fk) {
        $fk = self::normalize_fleet_kpis_incoming($fk);
        $out = array();
        $int_keys = array(
            'managed_listings_count',
            'slots_used',
            'slots_quota',
            'critical_count',
            'opportunities_count',
            'under_protection_count',
            'unmonitored_count',
            'agency_tier',
            'avg_visibility_score',
            'fleet_avg_visibility',
            'listings_in_danger',
            'listings_critical',
        );
        foreach ($int_keys as $k) {
            if (!array_key_exists($k, $fk)) {
                continue;
            }
            $out[$k] = max(0, (int) $fk[$k]);
        }
        if (array_key_exists('google_connected', $fk)) {
            $gc = $fk['google_connected'];
            $out['google_connected'] = ($gc === true || $gc === 1 || $gc === '1' || $gc === 'true' || $gc === 'yes');
        }
        if (array_key_exists('fleet_kpis_dom_sourced', $fk)) {
            $ds = $fk['fleet_kpis_dom_sourced'];
            $out['fleet_kpis_dom_sourced'] = ($ds === true || $ds === 1 || $ds === '1' || $ds === 'true' || $ds === 'yes');
        }
        foreach (array('visibility_tier', 'agency_radar_plan', 'snapshot_at', 'fleet_summary_plain') as $sk) {
            if (!empty($fk[$sk]) && is_string($fk[$sk])) {
                $out[$sk] = sanitize_text_field($sk === 'fleet_summary_plain' ? substr($fk[$sk], 0, 600) : $fk[$sk]);
            }
        }
        return $out;
    }

    /**
     * Détermine le type d'utilisateur pour le payload FastAPI : b2b (agence) ou b2c.
     */
    private static function get_user_type_for_payload() {
        $user_id = get_current_user_id();
        if (!$user_id) return 'b2c';
        if (user_can($user_id, 'administrator')) return 'b2b';
        $is_agency = get_user_meta($user_id, 'is_agency', true);
        if ($is_agency === true || $is_agency === '1' || $is_agency === 'true' || $is_agency === 'yes') return 'b2b';
        $is_agency_master = get_user_meta($user_id, 'is_agency_master', true);
        if ($is_agency_master === true || $is_agency_master === '1' || $is_agency_master === 'true' || $is_agency_master === 'yes') return 'b2b';
        $u = get_userdata($user_id);
        if ($u && !empty($u->roles) && is_array($u->roles) && in_array('avi_agency', $u->roles, true)) return 'b2b';
        return 'b2c';
    }

    /**
     * Proxy chat vers FastAPI (appel REST).
     * Pour l'AJAX (tableau $params), utiliser proxy_chat_to_fastapi_params().
     */
    public static function proxy_chat_to_fastapi($request) {
        $params = $request->get_json_params();
        if (empty($params) && $request->get_body()) {
            $params = json_decode($request->get_body(), true) ?: array();
        }

        $user_id = get_current_user_id();
        $business_name = (isset($params['business_name']) && $params['business_name'] !== '') ? sanitize_text_field($params['business_name']) : (($user_id && get_user_meta($user_id, 'avision_business_name', true)) ? get_user_meta($user_id, 'avision_business_name', true) : get_option('avision_business_name', get_option('avision_company_name', '')));
        $city = ($user_id && get_user_meta($user_id, 'avision_business_city', true)) ? get_user_meta($user_id, 'avision_business_city', true) : get_option('avision_business_city', get_option('avision_city', ''));
        $sector = isset($params['sector']) ? sanitize_text_field($params['sector']) : (($user_id && get_user_meta($user_id, 'avision_sector', true)) ? get_user_meta($user_id, 'avision_sector', true) : get_option('avision_sector', ''));
        $user_email = ($user_id && get_userdata($user_id)) ? get_userdata($user_id)->user_email : '';
        $user_type = self::get_user_type_for_payload();
        if (isset($params['dashboard_type']) && is_string($params['dashboard_type'])) {
            $dt = strtolower(trim($params['dashboard_type']));
            if (in_array($dt, array('b2b', 'agency', 'fleet', 'b2b_agency'), true)) {
                $user_type = 'b2b';
            } elseif (in_array($dt, array('b2c', 'client', 'premium', 'demo'), true)) {
                $user_type = 'b2c';
            }
        }

        $language = self::normalize_chat_language_for_fastapi(is_array($params) ? $params : array());

        $raw_message_full = isset($params['message']) && is_string($params['message']) ? wp_unslash($params['message']) : '';
        // HELLO_INIT B2B : bulle = KC (FR + variantes EN/ES optionnelles, sinon pont translate_reply_from_french).
        if ($user_type === 'b2b' && self::message_is_hello_init($raw_message_full)) {
            $hello_reply = self::get_b2b_hello_reply_for_language($language);
            if ($hello_reply !== '') {
                return new \WP_REST_Response(array('reply' => self::passthrough_llm_string($hello_reply)), 200);
            }
        }

        $level = self::resolve_chat_level_from_params($params, $user_type);
        $score = isset($params['score']) ? (int) $params['score'] : 0;
        // Agence : le score B2C du formulaire / localStorage est hors-sujet — évite score chirurgical erroné côté Python.
        if ($user_type === 'b2b') {
            $score = 0;
        }

        $user_status = isset($params['user_status']) && $params['user_status'] !== '' ? sanitize_text_field($params['user_status']) : null;
        $chat_history = isset($params['chatHistory']) ? $params['chatHistory'] : array();
        if (is_string($chat_history)) {
            $chat_history = json_decode($chat_history, true);
            if (!is_array($chat_history)) {
                $chat_history = array();
            }
        }
        $context_extra = array(
            'business_name' => $business_name !== '' ? $business_name : ($language === 'fr' ? 'votre établissement' : ($language === 'es' ? 'su establecimiento' : 'your business')),
            'city'          => $city !== '' ? $city : ($language === 'fr' ? 'ta ville' : ($language === 'es' ? 'tu ciudad' : 'your city')),
            'sector'        => $sector !== '' ? $sector : 'SERVICE',
            'email'         => $user_email !== '' ? $user_email : '',
        );
        $fleet_kpis_raw = isset($params['fleet_kpis']) ? $params['fleet_kpis'] : null;
        if (is_string($fleet_kpis_raw) && $fleet_kpis_raw !== '') {
            $decoded_fk = json_decode($fleet_kpis_raw, true);
            $fleet_kpis_raw = is_array($decoded_fk) ? $decoded_fk : null;
        }
        if (is_array($fleet_kpis_raw) && $fleet_kpis_raw !== array()) {
            $context_extra['fleet_kpis'] = self::sanitize_fleet_kpis_for_chat($fleet_kpis_raw);
        }
        if ($user_type === 'b2b' && $user_id > 0) {
            $fk_base = (isset($context_extra['fleet_kpis']) && is_array($context_extra['fleet_kpis'])) ? $context_extra['fleet_kpis'] : array();
            $context_extra['fleet_kpis'] = self::sanitize_fleet_kpis_for_chat(
                self::merge_agency_fleet_kpis_with_db($user_id, $fk_base)
            );
        }
        if (isset($params['profile_id']) && $params['profile_id'] !== '' && $params['profile_id'] !== null) {
            $context_extra['profile_id'] = sanitize_text_field($params['profile_id']);
        }
        if (isset($params['user_id']) && $params['user_id'] !== '' && $params['user_id'] !== null) {
            $context_extra['user_id'] = sanitize_text_field($params['user_id']);
        }
        $msg_sanitized = isset($params['message']) ? sanitize_textarea_field(wp_unslash($params['message'])) : (isset($params['query']) ? sanitize_textarea_field(wp_unslash($params['query'])) : '');
        $payload = array(
            'message'     => $msg_sanitized,
            'score'       => $score,
            'user_type'   => $user_type,
            'user_status' => $user_status,
            'profile_id'  => isset($params['profile_id']) && $params['profile_id'] !== '' && $params['profile_id'] !== null ? sanitize_text_field($params['profile_id']) : null,
            'user_id'     => isset($params['user_id']) && $params['user_id'] !== '' && $params['user_id'] !== null ? sanitize_text_field($params['user_id']) : null,
            'language'    => $language,
            'context'     => array_merge(
                $context_extra,
                array_filter(array(
                    'lang'                  => $language,
                    'l21_validation_count'  => isset($params['l21_validation_count']) && $params['l21_validation_count'] !== null ? max(0, (int) $params['l21_validation_count']) : null,
                    'l21_validated_names'  => isset($params['l21_validated_names']) && is_array($params['l21_validated_names']) ? array_map('sanitize_text_field', $params['l21_validated_names']) : null,
                ), function ($v) { return $v !== null; })
            ),
            'history'     => $chat_history,
            'kc_fallbacks' => array(
                'greetings' => self::kc_fallback_locale_triple('avi_kc_fallback_greetings'),
                'alerts'    => self::kc_fallback_locale_triple('avi_kc_fallback_alerts'),
                'levels'    => self::kc_fallback_locale_triple('avi_kc_fallback_levels'),
                'system'    => self::kc_fallback_locale_triple('avi_kc_fallback_system'),
            ),
        );
        if ($level !== null) {
            $payload['level'] = $level;
        }
        if (isset($params['dashboard_type']) && is_string($params['dashboard_type']) && $params['dashboard_type'] !== '') {
            $payload['dashboard_type'] = sanitize_text_field($params['dashboard_type']);
        }
        if (isset($params['review_rating']) && $params['review_rating'] >= 1 && $params['review_rating'] <= 5) {
            $payload['review_rating'] = (int) $params['review_rating'];
        }
        if (isset($params['review_context']) && is_string($params['review_context']) && $params['review_context'] !== '') {
            $payload['review_context'] = sanitize_text_field($params['review_context']);
        }

        // BINGO : écrasement absolu juste avant encodage JSON (KC PHP d’abord, puis contexte session JS).
        $level_for_prompt = ($level !== null) ? $level : 1;
        self::apply_absolute_final_chat_system_prompt($payload, $params, $user_type, $sector, $level_for_prompt);

        // URL canonique locale : http://127.0.0.1:8001/chat — surcharger via wp-config : define( 'AVISION_BACKEND_INTERNAL_URL', 'http://127.0.0.1:8001' );
        // Route Guard : class-avision-dashboard-route-guard retourne immédiatement si REST_REQUEST — /wp-json/avision/v1/* n’est pas redirigé vers /login.
        $base_internal = defined('AVISION_BACKEND_INTERNAL_URL') ? rtrim((string) AVISION_BACKEND_INTERNAL_URL, '/') : 'http://127.0.0.1:8001';
        $chat_url = $base_internal . '/chat';
        $chat_url = apply_filters('avision_fastapi_chat_url', $chat_url);

        error_log('[AVI-LANG-DEBUG] Payload → engine: language=' . ($payload['language'] ?? '(absent)') . ', context.lang=' . ($payload['context']['lang'] ?? '(absent)') . ', url=' . $chat_url);
        if (isset($payload['review_rating'])) {
            error_log('[AVI-L15-DEBUG] review_rating=' . $payload['review_rating'] . ', review_context=' . ($payload['review_context'] ?? '(absent)') . ' → envoi à ' . $chat_url);
        }

        $json_body = wp_json_encode($payload);
        if ($json_body === false) {
            $json_body = '{}';
            error_log('[AVI-FASTAPI-CHAT-OUT] wp_json_encode(payload) a échoué — corps vide {} envoyé.');
        }
        self::log_fastapi_chat_outbound($chat_url, $json_body);

        $backend_headers = array_merge(
            array('Content-Type' => 'application/json'),
            self::get_backend_uuid_headers($request)
        );
        $response = wp_remote_post($chat_url, array(
            'timeout'            => 30,
            'reject_unsafe_urls'  => false,
            'headers'            => $backend_headers,
            'body'               => $json_body,
            'sslverify'          => false,
        ));

        if (is_wp_error($response)) {
            self::log_fastapi_chat_inbound($chat_url, 'primary', 0, '', $response->get_error_message());
            return new \WP_Error('fastapi_error', $response->get_error_message(), array('status' => 500));
        }

        $code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        self::log_fastapi_chat_inbound($chat_url, 'primary', $code, is_string($body) ? $body : '');

        if ($code !== 200) {
            $excerpt = is_string($body) ? substr(trim($body), 0, 300) : '';
            error_log('Avision API: chat HTTP ' . $code . '. Body (300 chars max): ' . $excerpt);
        }

        $data = json_decode($body, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            error_log('[AVI-FASTAPI-CHAT-IN][primary] json_decode_error=' . json_last_error_msg() . ' http=' . (string) $code . ' — Le backend n’a probablement pas renvoyé du JSON valide (HTML d’erreur, timeout partiel, etc.).');
            if ($code === 200) {
                $excerpt = is_string($body) ? substr(trim($body), 0, 300) : '';
                error_log('Avision API: chat réponse non-JSON (HTTP ' . $code . '). Body: ' . $excerpt);
            }
            return new \WP_Error('json_error', 'Erreur serveur (HTTP ' . $code . ').', array('status' => 500));
        }

        if (is_array($data) && empty($data['reply']) && $code >= 200 && $code < 300) {
            error_log('[AVI-FASTAPI-CHAT-IN][primary] JSON valide mais reply vide — AVI muet côté UI : vérifier la logique main.py /chat (clé reply manquante ou chaîne vide).');
        }

        // FILET L15 : si review_rating envoyé mais backend renvoie le message générique → retry avec message simplifié
        if (
            isset($payload['review_rating']) && $payload['review_rating'] >= 1 && $payload['review_rating'] <= 5
            && is_array($data) && !empty($data['reply'])
            && (
                stripos($data['reply'], 'validación técnica') !== false
                || stripos($data['reply'], 'validation technique') !== false
                || stripos($data['reply'], 'priorité immédiate') !== false
                || stripos($data['reply'], 'prioridad inmediata') !== false
            )
        ) {
            error_log('[AVI-L15-FILET] Backend a renvoyé message générique pour review_rating=' . $payload['review_rating'] . '. Retry avec message simplifié.');
            $retry_payload = $payload;
            $retry_payload['message'] = 'HELP_REQUEST | LevelID: level15 | RATING: ' . (int) $payload['review_rating']
                . ' | CONTEXT: ' . (isset($payload['review_context']) && $payload['review_context'] !== '' ? $payload['review_context'] : 'Réponse standard');
            $retry_payload['level'] = 15;
            self::apply_absolute_final_chat_system_prompt($retry_payload, $params, $user_type, $sector, 15);
            $retry_json = wp_json_encode($retry_payload);
            if ($retry_json === false) {
                $retry_json = '{}';
            }
            self::log_fastapi_chat_outbound($chat_url, $retry_json);
            $retry_response = wp_remote_post($chat_url, array(
                'timeout'            => 30,
                'reject_unsafe_urls'  => false,
                'headers'            => array_merge(array('Content-Type' => 'application/json'), self::get_backend_uuid_headers($request)),
                'body'               => $retry_json,
                'sslverify'          => false,
            ));
            if (is_wp_error($retry_response)) {
                self::log_fastapi_chat_inbound($chat_url, 'retry', 0, '', $retry_response->get_error_message());
            } else {
                $retry_code = wp_remote_retrieve_response_code($retry_response);
                $retry_body = wp_remote_retrieve_body($retry_response);
                self::log_fastapi_chat_inbound($chat_url, 'retry', $retry_code, is_string($retry_body) ? $retry_body : '');
                $retry_data = json_decode(is_string($retry_body) ? $retry_body : '', true);
                if (json_last_error() === JSON_ERROR_NONE && is_array($retry_data) && !empty($retry_data['reply'])) {
                    $is_still_generic = (
                        stripos($retry_data['reply'], 'validación técnica') !== false
                        || stripos($retry_data['reply'], 'validation technique') !== false
                        || stripos($retry_data['reply'], 'prioridad inmediata') !== false
                    );
                    if (!$is_still_generic) {
                        error_log('[AVI-L15-FILET] Retry réussi → réponse L15 obtenue.');
                        $data = $retry_data;
                    } else {
                        error_log('[AVI-L15-FILET] Retry ÉCHOUÉ → backend ne supporte pas L15. Déployer main.py avec coupe-file L15.');
                    }
                }
            }
        }

        if ($language !== 'fr' && is_array($data) && !empty($data['reply'])) {
            $data['reply'] = self::translate_reply_from_french($data['reply'], $language);
        }

        if ($user_type === 'b2b' && self::message_is_hello_init($raw_message_full) && is_array($data)) {
            unset($data['meta_level_title']);
        }

        if (
            $code >= 200 && $code < 300
            && is_array($data) && !empty($data['reply'])
            && class_exists('\Avision\Avision_AI_Interactions')
        ) {
            $log_msg = isset($payload['message']) ? (string) $payload['message'] : '';
            $log_pid = isset($payload['profile_id']) && $payload['profile_id'] !== null && $payload['profile_id'] !== ''
                ? (string) $payload['profile_id'] : null;
            $log_lvl = array_key_exists('level', $payload) && $payload['level'] !== null ? (string) $payload['level'] : null;
            \Avision\Avision_AI_Interactions::log_exchange(
                get_current_user_id(),
                $log_pid,
                $log_msg,
                (string) $data['reply'],
                $log_lvl
            );
        }

        if (is_array($data)) {
            $data = self::passthrough_fastapi_chat_response_array($data);
        }

        return new \WP_REST_Response($data, $code >= 200 && $code < 300 ? $code : 502);
    }

    private static $level_titles_fr = array(
        1 => 'Catégorie Principale',
        2 => 'Description & Mots-clés',
        3 => 'Photos (Pack 1)',
        4 => 'Mode Gardien',
        5 => 'Intégration Web',
        6 => 'Catalogue Produits / Menu',
        7 => 'Contact & Réseaux sociaux',
        8 => 'Horaires',
        9 => 'Attributs & Options',
        10 => 'Couverture GPS',
        11 => 'Besoins & Expansion Sémantique',
        12 => 'Nettoyage Concurrence',
        13 => 'Fiches de Services',
        14 => 'Bouton Réservation',
        15 => 'Gestion des Avis',
        16 => 'Exactitude Données Web',
        17 => 'Story & Storytelling',
        18 => 'Publications Hebdomadaires',
        19 => 'Statistiques & Rapports',
        20 => 'Photos (Pack 2)',
        21 => 'Annuaires Élites',
    );

    private static $level_titles_en = array(
        1 => 'Primary Category',
        2 => 'Description & Keywords',
        3 => 'Photos (Pack 1)',
        4 => 'Guardian Mode',
        5 => 'Website Integration',
        6 => 'Products & Menu Catalog',
        7 => 'Contact & Social Links',
        8 => 'Business Hours',
        9 => 'Attributes & Options',
        10 => 'GPS Coverage',
        11 => 'Needs & Semantic Expansion',
        12 => 'Competitor Cleanup',
        13 => 'Service Listings',
        14 => 'Booking Button',
        15 => 'Review Management',
        16 => 'Web Data Accuracy',
        17 => 'Story & Storytelling',
        18 => 'Weekly Posts',
        19 => 'Stats & Reports',
        20 => 'Photos (Pack 2)',
        21 => 'Elite Directories',
    );

    private static $level_titles_es = array(
        1 => 'Categoría principal',
        2 => 'Descripción y palabras clave',
        3 => 'Fotos (Pack 1)',
        4 => 'Modo Guardián',
        5 => 'Integración web',
        6 => 'Catálogo y menú',
        7 => 'Contacto y redes sociales',
        8 => 'Horarios',
        9 => 'Atributos y opciones',
        10 => 'Cobertura GPS',
        11 => 'Necesidades y expansión semántica',
        12 => 'Limpieza competencia',
        13 => 'Fichas de servicios',
        14 => 'Botón reservas',
        15 => 'Gestión de reseñas',
        16 => 'Exactitud datos web',
        17 => 'Historia y storytelling',
        18 => 'Publicaciones semanales',
        19 => 'Estadísticas e informes',
        20 => 'Fotos (Pack 2)',
        21 => 'Directorios élite',
    );

    /**
     * Templates chips (Top 3 et Idée de Post) en EN/ES pour remplacement côté proxy si le backend renvoie encore le FR.
     */
    private static function get_chip_top3_template($language) {
        $templates = array(
            'en' => "1. **Neighborhood Circle** — Be the go-to within 500 m: precise category, description with local words, photos of your premises.\n\n"
                . "2. **City Circle** — Strengthen your presence in your city + trade: consistent keywords, recent reviews, regular posts.\n\n"
                . "3. **Trade Circle** — Become the « trade + city » reference: complete your info, hours, services.\n\n"
                . "Foundations to focus on first: **primary category** (Level 1), **description and keywords** (Level 2), **photos** (Level 3). "
                . "Check the active level side panel for concrete actions.",
            'es' => "1. **Círculo Barrio** — Sé la referencia a 500 m: categoría precisa, descripción con palabras del barrio, fotos de tu local.\n\n"
                . "2. **Círculo Ciudad** — Refuerza tu presencia en tu ciudad + sector: palabras clave coherentes, reseñas recientes, publicaciones regulares.\n\n"
                . "3. **Círculo Sector** — Conviértete en la referencia « sector + ciudad »: completa tu información, horarios, servicios.\n\n"
                . "Fundamentos a cuidar primero: **categoría principal** (Level 1), **descripción y palabras clave** (Level 2), **fotos** (Level 3). "
                . "Consulta el panel lateral del nivel activo para las acciones concretas.",
        );
        return isset($templates[$language]) ? $templates[$language] : $templates['en'];
    }

    private static function get_chip_post_template($language) {
        $templates = array(
            'en' => "**Hook** — A line that catches attention (offer, news, useful tip).\n\n"
                . "**Proof / detail** — One concrete fact: today's special, customer testimonial, trade tip in 2 lines.\n\n"
                . "**Call to action** — Invite to visit, book or leave a review.\n\n"
                . "Use your Radar keywords and your sector tone. Keep emojis subtle (1 or 2 max). "
                . "You can adapt this template in the active level panel.",
            'es' => "**Gancho** — Una frase que llame la atención (oferta, noticia, consejo útil).\n\n"
                . "**Prueba / detalle** — Un hecho concreto: especialidad del día, testimonio de cliente, truco del sector en 2 líneas.\n\n"
                . "**Llamada a la acción** — Invita a visitar, reservar o dejar una reseña.\n\n"
                . "Usa las palabras clave de tu Radar y el tono de tu sector. Emojis sobrios (1 o 2 máx). "
                . "Puedes adaptar este esquema en el panel del nivel activo.",
        );
        return isset($templates[$language]) ? $templates[$language] : $templates['en'];
    }

    /**
     * Translate known French patterns in a reply to the target language.
     * Inclut le remplacement des réponses chips (Top 3, Idée de Post) si le backend renvoie encore le FR.
     */
    private static function translate_reply_from_french($reply, $language) {
        if ($language === 'fr' || !$reply || !is_string($reply)) return $reply;

        // Remplacement complet des réponses chips FR si le backend a renvoyé les templates français
        if (strpos($reply, 'Cercle Quartier') !== false && strpos($reply, 'Sois incontournable') !== false && strpos($reply, 'fondations à soigner') !== false) {
            return self::get_chip_top3_template($language);
        }
        if (strpos($reply, 'Accroche') !== false && strpos($reply, 'Preuve / détail') !== false && strpos($reply, 'Utilise les mots-clés de ton Radar') !== false) {
            return self::get_chip_post_template($language);
        }

        $titles_target = ($language === 'es') ? self::$level_titles_es : self::$level_titles_en;
        $titles_fr_to_target = array();
        foreach (self::$level_titles_fr as $num => $fr) {
            $titles_fr_to_target[$fr] = isset($titles_target[$num]) ? $titles_target[$num] : $fr;
        }

        $phrases = ($language === 'es') ? array(
            'Tu es au niveau' => 'Estás en el nivel',
            'et ton score de visibilité est de' => 'y tu puntuación de visibilidad es de',
            'La validation technique du niveau' => 'La validación técnica del nivel',
            'est la priorité immédiate pour ta visibilité' => 'es la prioridad inmediata para tu visibilidad',
            'Clique sur le bloc' => 'Haz clic en el bloque',
            'et suis les instructions' => 'y sigue las instrucciones',
            'Une fois modifié, clique sur le bouton noir' => 'Una vez modificado, haz clic en el botón negro',
            'Vérifier l\'action' => 'Verificar la acción',
            "Vérifier l\xe2\x80\x99action" => 'Verificar la acción',
            'pour synchroniser les données' => 'para sincronizar los datos',
            'pour synchroniser les éléments' => 'para sincronizar',
            'Je vérifierai ton travail' => 'Verificaré tu trabajo',
            'J\'ai analysé ta fiche' => 'He analizado tu ficha',
            "J\xe2\x80\x99ai analysé ta fiche" => 'He analizado tu ficha',
            'On commence fort :' => 'Empecemos fuerte:',
            'On commence fort:' => 'Empecemos fuerte:',
            'Corrigeons ça ensemble dans le' => 'Corrijámoslo juntos en el',
            'Une fois la catégorie corrigée sur ta fiche Google, clique sur le bouton noir' => 'Una vez corregida la categoría en tu ficha de Google, haz clic en el botón negro',
            '**Ton travail préparatoire (Mission de l\'Ombre) :**' => '**Tu trabajo preparatorio (Misión en la Sombra):**',
            "**Ton travail préparatoire (Mission de l\xe2\x80\x99Ombre) :**" => '**Tu trabajo preparatorio (Misión en la Sombra):**',
            'Ton action prioritaire maintenant est :' => 'Tu acción prioritaria ahora es:',
            'Ton action prioritaire maintenant est:' => 'Tu acción prioritaria ahora es:',
            'Ton empire local se construit' => 'Tu imperio local se construye',
            'domination du Top 3 local' => 'dominación del Top 3 local',
            'Prochaine étape' => 'Próximo paso',
            'Niveau Verrouillé' => 'Nivel bloqueado',
            'Utilise le bouton noir pour valider ton action' => 'Usa el botón negro para validar tu acción',
            'On ne construit pas le toit avant les murs' => 'No se construye el techo antes que las paredes',
            'Termine d\'abord ta Phase actuelle pour débloquer cette étape' => 'Termina primero tu Fase actual para desbloquear este paso',
            "Termine d\xe2\x80\x99abord ta Phase actuelle pour débloquer cette étape" => 'Termina primero tu Fase actual para desbloquear este paso',
            'pour synchroniser les éléments. Je vérifierai ton travail' => 'para sincronizar. Verificaré tu trabajo',
            'pour synchroniser les données. Je vérifierai ton travail' => 'para sincronizar los datos. Verificaré tu trabajo',
            'Une fois corrigée sur ta fiche Google' => 'Una vez corregida en tu ficha de Google',
        ) : array(
            'Tu es au niveau' => 'You are at level',
            'et ton score de visibilité est de' => 'and your visibility score is',
            'La validation technique du niveau' => 'The technical validation of',
            'est la priorité immédiate pour ta visibilité' => 'is the immediate priority for your visibility',
            'Clique sur le bloc' => 'Click on the',
            'et suis les instructions' => 'and follow the instructions',
            'Une fois modifié, clique sur le bouton noir' => 'Once modified, click the black button',
            'Vérifier l\'action' => 'Verify action',
            "Vérifier l\xe2\x80\x99action" => 'Verify action',
            'pour synchroniser les données' => 'to sync the data',
            'pour synchroniser les éléments' => 'to sync',
            'Je vérifierai ton travail' => 'I\'ll check your work',
            'J\'ai analysé ta fiche' => 'I\'ve analyzed your listing',
            "J\xe2\x80\x99ai analysé ta fiche" => 'I\'ve analyzed your listing',
            'On commence fort :' => 'Let\'s start strong:',
            'On commence fort:' => 'Let\'s start strong:',
            'Corrigeons ça ensemble dans le' => 'Let\'s fix that together in',
            'Une fois la catégorie corrigée sur ta fiche Google, clique sur le bouton noir' => 'Once the category is corrected on your Google listing, click the black',
            '**Ton travail préparatoire (Mission de l\'Ombre) :**' => '**Your preparatory work (Shadow Mission):**',
            "**Ton travail préparatoire (Mission de l\xe2\x80\x99Ombre) :**" => '**Your preparatory work (Shadow Mission):**',
            'Ton action prioritaire maintenant est :' => 'Your immediate priority is:',
            'Ton action prioritaire maintenant est:' => 'Your immediate priority is:',
            'Ton empire local se construit' => 'Your local empire is being built',
            'domination du Top 3 local' => 'Top 3 local domination',
            'Prochaine étape' => 'Next step',
            'Niveau Verrouillé' => 'Locked Level',
            'Utilise le bouton noir pour valider ton action' => 'Use the black button to validate your action',
            'On ne construit pas le toit avant les murs' => 'You can\'t build the roof before the walls',
            'Termine d\'abord ta Phase actuelle pour débloquer cette étape' => 'Finish your current Phase first to unlock this step',
            "Termine d\xe2\x80\x99abord ta Phase actuelle pour débloquer cette étape" => 'Finish your current Phase first to unlock this step',
            'pour synchroniser les éléments. Je vérifierai ton travail' => 'to sync. I\'ll check your work',
            'pour synchroniser les données. Je vérifierai ton travail' => 'to sync the data. I\'ll check your work',
            'Une fois corrigée sur ta fiche Google' => 'Once corrected on your Google listing',
        );

        $out = $reply;

        // 1. Replace French level titles with translated versions (longest first to avoid partial matches)
        $fr_titles_sorted = array_keys($titles_fr_to_target);
        usort($fr_titles_sorted, function($a, $b) { return mb_strlen($b) - mb_strlen($a); });
        foreach ($fr_titles_sorted as $fr_title) {
            $target_title = $titles_fr_to_target[$fr_title];
            if ($fr_title !== $target_title) {
                $out = str_replace($fr_title, $target_title, $out);
            }
        }

        // 2. Replace French phrase patterns (longest first)
        $fr_phrases_sorted = array_keys($phrases);
        usort($fr_phrases_sorted, function($a, $b) { return mb_strlen($b) - mb_strlen($a); });
        foreach ($fr_phrases_sorted as $fr_phrase) {
            $out = str_replace($fr_phrase, $phrases[$fr_phrase], $out);
        }

        return $out;
    }

    /**
     * Catégorie principale Google (table avi_saas_profiles) pour ancrer le chat FastAPI (ex. hôtellerie vs BTP).
     *
     * @param array $params Paramètres AJAX (profile_id, user_id).
     * @return string Libellé sanitizé ou chaîne vide.
     */
    private static function resolve_google_primary_category_for_chat_proxy( $params ) {
        $params = is_array( $params ) ? $params : array();
        $pid = isset( $params['profile_id'] ) ? (int) $params['profile_id'] : 0;
        $uid = isset( $params['user_id'] ) ? (int) $params['user_id'] : 0;
        if ( $pid <= 0 && $uid <= 0 ) {
            return '';
        }
        if ( ! class_exists( __NAMESPACE__ . '\\Avision_Database' ) ) {
            return '';
        }
        try {
            $db = new Avision_Database();
        } catch ( \Throwable $e ) {
            return '';
        }
        $row = null;
        if ( $pid > 0 ) {
            $row = $db->get_saas_profile_by_id( $pid );
        } elseif ( $uid > 0 ) {
            $row = $db->get_first_saas_profile_by_user_id( $uid );
        }
        if ( ! is_array( $row ) ) {
            return '';
        }
        $gp = isset( $row['google_primary_category'] ) ? trim( (string) $row['google_primary_category'] ) : '';
        if ( $gp !== '' ) {
            return sanitize_text_field( $gp );
        }
        $cat = isset( $row['category'] ) ? trim( (string) $row['category'] ) : '';
        return $cat !== '' ? sanitize_text_field( $cat ) : '';
    }

    /**
     * Proxy chat appelé par l'AJAX avec un tableau (message, chatHistory, currentLevel, score, sector).
     */
    public function proxy_chat_to_fastapi_params($params) {
        $params = is_array($params) ? $params : array();
        $message = isset($params['message']) ? $params['message'] : (isset($params['query']) ? $params['query'] : '');
        $message = is_string($message) ? sanitize_textarea_field(wp_unslash($message)) : '';

        if ($message === '') {
            return array('success' => false, 'error' => 'Message required');
        }

        $request = new \WP_REST_Request('POST', '/avision/v1/chat');
        $language = self::normalize_chat_language_for_fastapi($params);
        $user_type_resolve = 'b2c';
        if (isset($params['dashboard_type']) && is_string($params['dashboard_type'])) {
            $dt = strtolower(trim($params['dashboard_type']));
            if (in_array($dt, array('b2b', 'agency', 'fleet', 'b2b_agency'), true)) {
                $user_type_resolve = 'b2b';
            }
        }
        $level_resolved = self::resolve_chat_level_from_params($params, $user_type_resolve);
        $score_body = isset($params['score']) ? (int) $params['score'] : 0;
        if ($user_type_resolve === 'b2b') {
            $score_body = 0;
        }
        $body = array(
            'message'       => $message,
            'query'         => $message,
            'score'         => $score_body,
            'chatHistory'   => isset($params['chatHistory']) ? $params['chatHistory'] : array(),
            'sector'        => isset($params['sector']) ? sanitize_text_field($params['sector']) : '',
            'business_name' => isset($params['business_name']) && $params['business_name'] !== '' ? sanitize_text_field($params['business_name']) : '',
            'profile_id'    => isset($params['profile_id']) && $params['profile_id'] !== '' ? sanitize_text_field($params['profile_id']) : null,
            'user_id'       => isset($params['user_id']) && $params['user_id'] !== '' ? sanitize_text_field($params['user_id']) : null,
            'language'      => $language,
            'context'       => array('lang' => $language),
        );
        if ($level_resolved !== null) {
            $body['level'] = $level_resolved;
        }
        if (isset($params['user_status']) && $params['user_status'] !== '' && $params['user_status'] !== null) {
            $body['user_status'] = sanitize_text_field($params['user_status']);
        }
        if (isset($params['l21_validation_count']) && $params['l21_validation_count'] !== null && $params['l21_validation_count'] !== '') {
            $body['l21_validation_count'] = max(0, (int) $params['l21_validation_count']);
        }
        if (isset($params['l21_validated_names']) && is_array($params['l21_validated_names'])) {
            $body['l21_validated_names'] = array_map('sanitize_text_field', $params['l21_validated_names']);
        }
        if (isset($params['review_rating']) && $params['review_rating'] >= 1 && $params['review_rating'] <= 5) {
            $body['review_rating'] = (int) $params['review_rating'];
        }
        if (isset($params['review_context']) && $params['review_context'] !== '') {
            $body['review_context'] = sanitize_text_field($params['review_context']);
        }
        if (isset($params['systemMessage']) && is_string($params['systemMessage']) && $params['systemMessage'] !== '') {
            $body['systemMessage'] = $params['systemMessage'];
        }
        if (isset($params['pid']) && is_string($params['pid']) && preg_match('/^[a-f0-9\-]{36}$/i', trim($params['pid']))) {
            $body['pid'] = trim($params['pid']);
        }
        if (isset($params['aid']) && is_string($params['aid']) && preg_match('/^[a-f0-9\-]{36}$/i', trim($params['aid']))) {
            $body['aid'] = trim($params['aid']);
        }
        if (isset($params['dashboard_type']) && is_string($params['dashboard_type']) && $params['dashboard_type'] !== '') {
            $body['dashboard_type'] = sanitize_text_field($params['dashboard_type']);
        }
        $fk_in_params = isset($params['fleet_kpis']) ? $params['fleet_kpis'] : null;
        if (is_string($fk_in_params) && $fk_in_params !== '') {
            $fk_in_params = json_decode($fk_in_params, true);
        }
        if (is_array($fk_in_params) && $fk_in_params !== array()) {
            $body['fleet_kpis'] = self::sanitize_fleet_kpis_for_chat($fk_in_params);
        }
        $gp_post = '';
        if (isset($params['google_primary_category']) && is_string($params['google_primary_category'])) {
            $gp_post = trim($params['google_primary_category']);
        }
        if ($gp_post !== '') {
            $body['google_primary_category'] = sanitize_text_field($gp_post);
        } else {
            $gp_chat = self::resolve_google_primary_category_for_chat_proxy($params);
            if ($gp_chat !== '') {
                $body['google_primary_category'] = $gp_chat;
            }
        }
        $request->set_body(json_encode($body));
        $request->set_header('Content-Type', 'application/json');

        $response = self::proxy_chat_to_fastapi($request);

        if (is_wp_error($response)) {
            return array('success' => false, 'error' => $response->get_error_message());
        }

        $http_status = $response->get_status();
        $data = is_array($response->get_data()) ? $response->get_data() : array();
        // Ne jamais renvoyer success: true si le backend a répondu 4xx/5xx (évite bulle vide avec reply: "")
        if ($http_status < 200 || $http_status >= 300) {
            $err = isset($data['detail']) ? $data['detail'] : ('Erreur serveur (HTTP ' . $http_status . ')');
            return array('success' => false, 'error' => is_string($err) ? $err : 'Erreur serveur', 'reply' => isset($data['reply']) ? $data['reply'] : '');
        }
        if (is_array($data) && isset($data['reply'])) {
            return array_merge(array('success' => true), $data);
        }
        if (is_array($data)) {
            return array_merge(array('success' => true), $data);
        }
        $err = is_array($data) && isset($data['detail']) ? $data['detail'] : 'Réponse FastAPI invalide';
        return array('success' => false, 'error' => is_string($err) ? $err : 'Réponse FastAPI invalide');
    }

    /**
     * Proxy Level 21 Panel vers FastAPI (contenu dynamique Side Panel Annuaires Élites).
     *
     * @param array $body { city, lang, sector, l21_validation_count, l21_validated_names }
     * @return array|WP_Error
     */
    public static function proxy_level21_panel_to_fastapi($body) {
        $body = is_array($body) ? $body : array();
        // Loopback direct : éviter 502 (pas d'HTTPS via internet)
        $base_internal = defined('AVISION_BACKEND_INTERNAL_URL') ? rtrim(AVISION_BACKEND_INTERNAL_URL, '/') : 'http://127.0.0.1:8001';
        $panel_url = $base_internal . '/level-21-panel';
        $panel_url = apply_filters('avision_fastapi_level21_panel_url', $panel_url);

        $response = wp_remote_post($panel_url, array(
            'headers'            => array_merge(array('Content-Type' => 'application/json'), self::get_backend_uuid_headers(null)),
            'body'               => json_encode($body),
            'timeout'            => 30,
            'reject_unsafe_urls' => false,
            'sslverify'          => false,
        ));

        if (is_wp_error($response)) {
            return $response;
        }

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

        if ($code !== 200 || json_last_error() !== JSON_ERROR_NONE) {
            return new \WP_Error('level21_panel_error', 'Erreur lors du chargement du panneau L21.', array('status' => $code >= 400 ? $code : 502));
        }

        return $data;
    }

    /**
     * Proxy catégories Google vers FastAPI (pour Guardian REST API).
     */
    public static function proxy_categories_to_fastapi($request) {
        $search = $request instanceof \WP_REST_Request ? $request->get_param('search') : (isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '');
        $regionCode = $request instanceof \WP_REST_Request ? $request->get_param('regionCode') : (isset($_GET['regionCode']) ? sanitize_text_field($_GET['regionCode']) : 'US');
        $languageCode = $request instanceof \WP_REST_Request ? $request->get_param('languageCode') : (isset($_GET['languageCode']) ? sanitize_text_field($_GET['languageCode']) : 'en');
        // Loopback direct + Host pour éviter 502 (CyberPanel)
        $base_internal = defined('AVISION_BACKEND_INTERNAL_URL') ? rtrim(AVISION_BACKEND_INTERNAL_URL, '/') : 'http://127.0.0.1:8001';
        $url = add_query_arg(array(
            'search'       => $search,
            'regionCode'   => $regionCode,
            'languageCode' => $languageCode
        ), $base_internal . '/google-categories');
        $response = wp_remote_get($url, array(
            'timeout'            => 30,
            'reject_unsafe_urls' => false,
            'headers'            => self::get_backend_uuid_headers($request),
            'sslverify'          => false,
        ));
        if (is_wp_error($response)) {
            return new \WP_REST_Response(array('categories' => array()), 200);
        }
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);
        $code = wp_remote_retrieve_response_code($response);
        if (json_last_error() !== JSON_ERROR_NONE || $code < 200 || $code >= 300) {
            return new \WP_REST_Response(array('categories' => array()), 200);
        }
        if (!is_array($data)) {
            $data = array('categories' => array());
        }
        if (!isset($data['categories'])) {
            $data['categories'] = array();
        }
        return new \WP_REST_Response($data, 200);
    }
}
