<?php
/**
 * Routeur AJAX pour les appels vers FastAPI
 *
 * Chat AVI : accessible en connecté ET en déconnecté (nopriv).
 * Aucune vérification is_user_logged_in() ni current_user_can() : le nonce suffit.
 */

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

// Dépendance critique du proxy chat / L21 : chargée en amont pour éviter des fatals si l’ordre d’inclusion du plugin change sur le serveur.
if (defined('AVISION_PATH')) {
    $_avi_ajax_router_api_file = AVISION_PATH . 'includes/class-avision-api.php';
    if (is_readable($_avi_ajax_router_api_file)) {
        require_once $_avi_ajax_router_api_file;
    } elseif (function_exists('error_log')) {
        error_log('❌ CRITICAL: Avision_API — class-avision-api.php introuvable ou illisible: ' . $_avi_ajax_router_api_file);
    }
}

class Avision_Ajax_Router {

    public static function init() {
        add_action('wp_ajax_avision_api', array(__CLASS__, 'handle_ajax_request'));
        add_action('wp_ajax_nopriv_avision_api', array(__CLASS__, 'handle_ajax_request'));
        add_action('wp_ajax_avision_guardian_stats', array(__CLASS__, 'handle_guardian_stats'));
        add_action('wp_ajax_nopriv_avision_guardian_stats', array(__CLASS__, 'handle_guardian_stats'));
        add_action('wp_ajax_avision_get_guardian_logs', array(__CLASS__, 'handle_get_guardian_logs'));
        add_action('wp_ajax_nopriv_avision_get_guardian_logs', array(__CLASS__, 'handle_get_guardian_logs'));
        // S9-B — Centre d'alertes flotte (Agency Dashboard) — READ ONLY (connecté uniquement, pas de nopriv)
        add_action('wp_ajax_avision_get_agency_alerts', array(__CLASS__, 'handle_get_agency_alerts'));
        // S9-C — Upgrade plan Radar (quota mots-clés) — READ ONLY paiement (connecté uniquement, pas de nopriv)
        add_action('wp_ajax_avision_upgrade_radar_plan', array(__CLASS__, 'handle_upgrade_radar_plan'));
        // S10 prereq — Validation niveau côté serveur (connecté uniquement, pas de nopriv)
        add_action('wp_ajax_avision_validate_level', array(__CLASS__, 'handle_validate_level'));
        // Vérification GBP réelle avant validation manuelle (premium dashboard)
        add_action('wp_ajax_avision_verify_level_gbp', array(__CLASS__, 'handle_verify_level_gbp'));
        // S10-B — Checkout Stripe (plans + bundles), connecté uniquement.
        add_action('wp_ajax_avision_stripe_s10b_checkout', array(__CLASS__, 'handle_stripe_s10b_checkout'));
        add_action('wp_ajax_nopriv_avision_stripe_s10b_checkout', array(__CLASS__, 'handle_stripe_s10b_checkout'));
        // S11 — Upgrade abonnement (proration Stripe) ou session Checkout de secours.
        add_action('wp_ajax_avision_upgrade_subscription', array(__CLASS__, 'handle_upgrade_subscription'));
        // S11 — AppSumo redeem (visiteur ou connecté) + essai Guardian + import CSV admin.
        add_action('wp_ajax_nopriv_avision_appsumo_redeem', array(__CLASS__, 'handle_appsumo_redeem'));
        add_action('wp_ajax_avision_appsumo_redeem', array(__CLASS__, 'handle_appsumo_redeem'));
        add_action('wp_ajax_avision_appsumo_start_guardian_trial', array(__CLASS__, 'handle_appsumo_start_guardian_trial'));
        add_action('wp_ajax_nopriv_avision_appsumo_create_setup_intent', array(__CLASS__, 'handle_appsumo_create_setup_intent'));
        add_action('wp_ajax_avision_appsumo_create_setup_intent', array(__CLASS__, 'handle_appsumo_create_setup_intent'));
        add_action('wp_ajax_avision_appsumo_import_csv', array(__CLASS__, 'handle_appsumo_import_csv'));
        // S11 — Désinscription marketing + confirmation Brevo.
        add_action('wp_ajax_avision_unsubscribe_with_reason', array(__CLASS__, 'handle_avision_unsubscribe_with_reason'));
        // Visibilité rapport client (Agency admin → profil).
        add_action('wp_ajax_avision_save_report_visibility', array(__CLASS__, 'handle_save_report_visibility'));
        add_action('wp_ajax_avision_get_report_visibility', array(__CLASS__, 'handle_get_report_visibility'));
        add_action('wp_ajax_avision_save_email_prefs', array(__CLASS__, 'handle_save_email_prefs'));
        add_action('wp_ajax_avision_get_email_prefs', array(__CLASS__, 'handle_get_email_prefs'));
        add_action('wp_ajax_avision_delete_account', array(__CLASS__, 'handle_delete_account'));
        // Sprint 32 — Smart Gallery warning email.
        add_action('wp_ajax_avision_gallery_warning_email', array(__CLASS__, 'handle_gallery_warning_email'));
        // ANTI-RÉGRESSION [06 juin 2026] — Stats galerie depuis DB (in_stock / published)
        add_action('wp_ajax_avision_get_media_stats', array(__CLASS__, 'handle_get_media_stats'));
        // ANTI-RÉGRESSION [06 juin 2026] — Liste posts bulk schedulés
        add_action('wp_ajax_avision_get_scheduled_bulk_posts', array(__CLASS__, 'handle_get_scheduled_bulk_posts'));
        // ANTI-RÉGRESSION [06 juin 2026] — Annulation post bulk planifié
        add_action('wp_ajax_avision_cancel_scheduled_bulk_post', array(__CLASS__, 'handle_cancel_scheduled_bulk_post'));
        add_action('wp_ajax_avision_btob_save_agency', array(__CLASS__, 'handle_btob_save_agency'));
        add_action('wp_ajax_nopriv_avision_btob_save_agency', array(__CLASS__, 'handle_btob_save_agency'));
        add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_nonprofit_admin_scripts'));
        add_action( 'wp_ajax_avision_nonprofit_apply', array( __CLASS__, 'handle_nonprofit_apply' ) );
        add_action( 'wp_ajax_nopriv_avision_nonprofit_apply', array( __CLASS__, 'handle_nonprofit_apply' ) );
        add_action( 'wp_ajax_nopriv_avision_np_submit_share', array( __CLASS__, 'handle_np_submit_share' ) );
        add_action( 'wp_ajax_avision_np_submit_share', array( __CLASS__, 'handle_np_submit_share' ) );
    }

    /**
     * Nonce + AJAX URL pour la page admin Nonprofits.
     *
     * @param string $hook Page hook suffix.
     * @return void
     */
    public static function enqueue_nonprofit_admin_scripts( $hook ) {
        if ( strpos( (string) $hook, 'avi-nonprofits' ) === false ) {
            return;
        }
        wp_localize_script(
            'jquery',
            'aviNonprofitAdmin',
            array(
                'ajax_url' => admin_url( 'admin-ajax.php' ),
                'nonce'    => wp_create_nonce( 'avision_nonprofit_nonce' ),
            )
        );
    }

    /**
     * Réponse JSON d’erreur avec code HTTP lorsque WordPress ≥ 5.6 le permet.
     * Sur WordPress ancien (< 5.6) avec PHP 8+, wp_send_json_error( $data, $status ) peut provoquer ArgumentCountError (HTTP 500).
     *
     * @param array<string, mixed> $data
     * @param int|null             $status_code
     */
    private static function avision_wp_send_json_error_http( $data, $status_code = null ) {
        global $wp_version;
        if ( $status_code !== null && is_numeric( $status_code ) && function_exists( 'wp_send_json_error' ) ) {
            if ( isset( $wp_version ) && version_compare( (string) $wp_version, '5.6', '>=' ) ) {
                wp_send_json_error( $data, (int) $status_code );
                return;
            }
            if ( function_exists( 'status_header' ) ) {
                status_header( (int) $status_code );
            }
        }
        wp_send_json_error( $data );
    }

    /**
     * Colonne avi_saas_profiles.user_id (SaaS), alignée sur le REST / les CRON Gardien — pas l’ID WordPress.
     */
    private static function avision_ajax_profile_saas_user_id($profile_id) {
        global $wpdb;
        $profile_id = (int) $profile_id;
        if ($profile_id <= 0 || !isset($wpdb) || !$wpdb) {
            return 0;
        }
        $t = $wpdb->prefix . 'avi_saas_profiles';
        if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $t)) !== $t) {
            return 0;
        }
        return (int) $wpdb->get_var($wpdb->prepare("SELECT user_id FROM `{$t}` WHERE id = %d LIMIT 1", $profile_id));
    }

    /**
     * Résout l’ID utilisateur pour avi_security_logs : SaaS user_id du profil si profile_id autorisé, sinon (connecté) repli legacy WP.
     *
     * @return int -1 = interdit, 0 = non authentifié / incomplet, >0 = user_id pour les requêtes Guardian
     */
    private static function resolve_guardian_logs_user_id_for_ajax() {
        $profile_id = isset($_POST['profile_id']) ? (int) wp_unslash($_POST['profile_id']) : 0;

        if ($profile_id > 0) {
            if (is_user_logged_in()) {
                if (!function_exists('avi_current_user_can_access_profile') || !avi_current_user_can_access_profile($profile_id)) {
                    return -1;
                }
            } else {
                $report_uri_ok = function_exists('avi_premium_report_request_uri_or_referer_authorizes_profile')
                    && avi_premium_report_request_uri_or_referer_authorizes_profile($profile_id);
                $ref_ok = !$report_uri_ok && function_exists('avi_premium_referer_allows_shared_profile_readonly')
                    && avi_premium_referer_allows_shared_profile_readonly($profile_id);
                $share_ok = !$report_uri_ok && !$ref_ok && function_exists('avi_premium_shared_report_access_allowed_for_profile')
                    && avi_premium_shared_report_access_allowed_for_profile($profile_id);
                if (!$report_uri_ok && !$ref_ok && !$share_ok) {
                    return -1;
                }
            }
            $saas_uid = self::avision_ajax_profile_saas_user_id($profile_id);
            if ($saas_uid > 0) {
                return $saas_uid;
            }
            // Fiche grey-label / test sans user_id SaaS : accès lecture seule OK → stats/logs vides (user_id 0).
            return 0;
        }

        if (!is_user_logged_in()) {
            return 0;
        }

        return (int) get_current_user_id();
    }

    /**
     * Nonce « avision_nonce » : filtre skip, validation WP, ou passe-droit debug pour administrateur (ex. SALT rotés, page en cache).
     *
     * @param string $nonce Valeur brute (déjà extraite de $_POST).
     * @return bool
     */
    private static function avision_verify_nonce_or_admin_bypass($nonce) {
        if (apply_filters('avision_skip_nonce_verification', false)) {
            return true;
        }
        if ($nonce !== '' && wp_verify_nonce($nonce, 'avision_nonce') !== false) {
            return true;
        }
        if (is_user_logged_in() && current_user_can('manage_options')) {
            error_log('[AVI-SECURITY] Nonce bypass for admin');
            return true;
        }
        return false;
    }

    /**
     * Vide le tampon de sortie actif (évite du HTML parasite avant la réponse JSON), trace l’action, envoie le JSON puis wp_die().
     *
     * @param string               $avi_router_action
     * @param array<string, mixed> $payload
     * @param bool                 $success
     */
    private static function avision_router_send_json_response($avi_router_action, $payload, $success) {
        if (function_exists('ob_get_level') && ob_get_level() > 0) {
            ob_clean();
        }
        if (function_exists('error_log')) {
            error_log('[AVI-ROUTER] Response cleaned and sent for action: ' . (string) $avi_router_action);
        }
        if ($success) {
            wp_send_json_success($payload);
        } else {
            wp_send_json_error($payload);
        }
        wp_die();
    }

    /**
     * SPRINT 9 — S9-B
     * Endpoint : avision_get_agency_alerts (Agency Dashboard)
     * READ-ONLY : ne modifie rien en base.
     *
     * POST params:
     * - nonce (check_ajax_referer: avision_agency_nonce)
     * - days (int, max 30, défaut 7)
     * - limit (int, max 100, défaut 50)
     * - filter (all|critical|warning, défaut all)
     */
    public static function handle_get_agency_alerts() {
        if (!function_exists('check_ajax_referer') || !function_exists('is_user_logged_in')) {
            wp_send_json_error(array('code' => 'missing_wp_functions'), 500);
            wp_die();
        }

        check_ajax_referer('avision_agency_nonce', 'nonce');

        if (!is_user_logged_in()) {
            wp_send_json_error(array('code' => 'not_logged_in'), 401);
            wp_die();
        }

        $admin_user_id = (int) get_current_user_id();
        $days = isset($_POST['days']) ? (int) wp_unslash($_POST['days']) : 7;
        $limit = isset($_POST['limit']) ? (int) wp_unslash($_POST['limit']) : 50;
        $filter = isset($_POST['filter']) ? sanitize_text_field(wp_unslash($_POST['filter'])) : 'all';

        $days = max(1, min(30, $days));
        $limit = max(1, min(100, $limit));
        if (!in_array($filter, array('all', 'critical', 'warning'), true)) {
            $filter = 'all';
        }

        global $wpdb;
        if (!isset($wpdb) || !$wpdb) {
            wp_send_json_error(array('code' => 'no_db'), 500);
            wp_die();
        }

        $t_logs = $wpdb->prefix . 'avi_security_logs';
        $t_profiles = $wpdb->prefix . 'avi_saas_profiles';
        $t_agencies = $wpdb->prefix . 'avi_saas_agencies';

        $where_priority = '';
        if ($filter === 'critical') {
            $where_priority = " AND sl.event_type = 'real' AND sl.status = 'blocked' ";
        } elseif ($filter === 'warning') {
            $where_priority = " AND sl.event_type = 'real' AND sl.status = 'alert' ";
        }

        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $sql = "
            SELECT
                sl.id, sl.user_id, sl.event_type, sl.signal_id,
                sl.status, sl.message_json, sl.created_at,
                sp.id AS profile_id, sp.business_name,
                sp.score, sp.guardian_active
            FROM {$t_logs} sl
            INNER JOIN {$t_profiles} sp ON sp.user_id = sl.user_id
            INNER JOIN {$t_agencies} ag ON ag.id = sp.agency_id
            WHERE ag.admin_user_id = %d
              AND sl.created_at > DATE_SUB(NOW(), INTERVAL %d DAY)
              {$where_priority}
            ORDER BY
              CASE sl.status WHEN 'blocked' THEN 0 ELSE 1 END ASC,
              sl.created_at DESC
            LIMIT %d
        ";

        $rows = $wpdb->get_results(
            $wpdb->prepare($sql, $admin_user_id, $days, $limit),
            ARRAY_A
        );
        if (!is_array($rows)) {
            $rows = array();
        }

        $signal_labels = array(
            1  => "Nom de l'établissement",
            2  => "Catégorie d'activité",
            3  => "Numéro de téléphone",
            4  => "Adresse physique",
            5  => "Lien du site web",
            6  => "Horaires d'ouverture",
            7  => "Zone de chalandise",
            8  => "Services et prestations",
            9  => "Attributs (Accessibilité / Options)",
            10 => "Lien de réservation",
            11 => "Statut d'ouverture",
            12 => "Galerie photos & vidéos",
        );

        $shield_by_signal = array(
            1  => '🛡️',
            2  => '🕵️',
            3  => '🛡️',
            4  => '🛡️',
            5  => '🛡️',
            6  => '📡',
            7  => '📡',
            8  => '📋',
            9  => '📋',
            10 => '🛡️',
            11 => '📡',
            12 => '🔒',
        );

        // Compteurs badges (fenêtre temporelle identique, sans filtre de priorité).
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $sql_counts = "
            SELECT
                SUM(CASE WHEN sl.event_type = 'real' AND sl.status = 'blocked' THEN 1 ELSE 0 END) AS n_crit,
                SUM(CASE WHEN sl.event_type = 'real' AND sl.status = 'alert' THEN 1 ELSE 0 END) AS n_warn,
                COUNT(*) AS n_all
            FROM {$t_logs} sl
            INNER JOIN {$t_profiles} sp ON sp.user_id = sl.user_id
            INNER JOIN {$t_agencies} ag ON ag.id = sp.agency_id
            WHERE ag.admin_user_id = %d
              AND sl.created_at > DATE_SUB(NOW(), INTERVAL %d DAY)
        ";
        $count_row = $wpdb->get_row(
            $wpdb->prepare($sql_counts, $admin_user_id, $days),
            ARRAY_A
        );
        if (!is_array($count_row)) {
            $count_row = array();
        }
        $alert_counts = array(
            'critical' => isset($count_row['n_crit']) ? (int) $count_row['n_crit'] : 0,
            'warning'  => isset($count_row['n_warn']) ? (int) $count_row['n_warn'] : 0,
            'all'      => isset($count_row['n_all']) ? (int) $count_row['n_all'] : 0,
        );

        $alerts = array();
        foreach ($rows as $r) {
            $event_type = isset($r['event_type']) ? (string) $r['event_type'] : '';
            $status = isset($r['status']) ? (string) $r['status'] : '';
            $priority = 'info';
            if ($event_type === 'real' && $status === 'blocked') {
                $priority = 'critical';
            } elseif ($event_type === 'real' && $status === 'alert') {
                $priority = 'warning';
            }

            $sid = isset($r['signal_id']) ? (int) $r['signal_id'] : 0;
            $msg_raw = isset($r['message_json']) ? (string) $r['message_json'] : '';
            $msg = $msg_raw;
            $decoded = json_decode($msg_raw, true);
            if (is_array($decoded)) {
                if (isset($decoded['fr']) && is_string($decoded['fr']) && $decoded['fr'] !== '') {
                    $msg = $decoded['fr'];
                } elseif (isset($decoded['en']) && is_string($decoded['en']) && $decoded['en'] !== '') {
                    $msg = $decoded['en'];
                } else {
                    $msg = wp_json_encode($decoded);
                }
            }

            $alerts[] = array(
                'id'            => isset($r['id']) ? (int) $r['id'] : 0,
                'user_id'       => isset($r['user_id']) ? (int) $r['user_id'] : 0,
                'profile_id'    => isset($r['profile_id']) ? (int) $r['profile_id'] : 0,
                'business_name' => isset($r['business_name']) ? (string) $r['business_name'] : '',
                'score'         => isset($r['score']) ? (int) $r['score'] : 0,
                'guardian_active' => !empty($r['guardian_active']) ? 1 : 0,
                'event_type'    => $event_type,
                'signal_id'     => $sid,
                'signal_label'  => isset($signal_labels[$sid]) ? $signal_labels[$sid] : '—',
                'status'        => $status,
                'priority'      => $priority,
                'shield_icon'   => isset($shield_by_signal[$sid]) ? $shield_by_signal[$sid] : '📋',
                'message'       => is_string($msg) ? $msg : '',
                'created_at'    => isset($r['created_at']) ? (string) $r['created_at'] : '',
            );
        }

        $fleet_kpis = array();
        $cache_key = 'avi_fleet_kpis_cache_' . (string) $admin_user_id;
        $fleet_raw = function_exists('get_option') ? get_option($cache_key) : '';
        if (is_string($fleet_raw) && $fleet_raw !== '') {
            $fleet_dec = json_decode($fleet_raw, true);
            if (is_array($fleet_dec)) {
                $fleet_kpis = $fleet_dec;
            }
        } elseif (is_array($fleet_raw)) {
            $fleet_kpis = $fleet_raw;
        }

        wp_send_json_success(array(
            'alerts'     => $alerts,
            'fleet_kpis' => $fleet_kpis,
            'meta'       => array(
                'days'          => $days,
                'total'         => count($alerts),
                'generated_at'  => current_time('mysql', true),
                'version'       => '1.0',
                'alert_counts'  => $alert_counts,
            ),
        ));
        wp_die();
    }

    /**
     * SPRINT 9 — S9-C
     * Endpoint : avision_upgrade_radar_plan
     * READ-ONLY paiement : suppose que Stripe a déjà confirmé l'upgrade.
     *
     * POST params:
     * - nonce : check_ajax_referer('avision_premium_nonce', 'nonce')
     * - plan  : 'offert'|'hebdo'|'elite'
     */
    public static function handle_upgrade_radar_plan() {
        if (!function_exists('check_ajax_referer') || !function_exists('is_user_logged_in')) {
            wp_send_json_error(array('code' => 'missing_wp_functions'), 500);
            wp_die();
        }

        check_ajax_referer('avision_premium_nonce', 'nonce');

        if (!is_user_logged_in()) {
            wp_send_json_error(array('code' => 'not_logged_in'), 401);
            wp_die();
        }

        $plan = isset($_POST['plan']) ? sanitize_text_field(wp_unslash($_POST['plan'])) : '';
        if (!in_array($plan, array('offert', 'hebdo', 'elite'), true)) {
            wp_send_json_error(array('code' => 'invalid_plan'), 400);
            wp_die();
        }

        $plans = array(
            'offert' => array('max_keywords' => 20, 'label' => 'Mensuel', 'price' => 0),
            'hebdo'  => array('max_keywords' => 75, 'label' => 'Hebdo', 'price' => 7),
            'elite'  => array('max_keywords' => 100, 'label' => 'Élite', 'price' => 14),
        );
        $conf = isset($plans[$plan]) ? $plans[$plan] : null;
        if (!is_array($conf)) {
            wp_send_json_error(array('code' => 'invalid_plan'), 400);
            wp_die();
        }

        global $wpdb;
        if (!isset($wpdb) || !$wpdb) {
            wp_send_json_error(array('code' => 'no_db'), 500);
            wp_die();
        }

        $uid = (int) get_current_user_id();
        if ($uid <= 0) {
            wp_send_json_error(array('code' => 'not_logged_in'), 401);
            wp_die();
        }

        $t_profiles = $wpdb->prefix . 'avi_saas_profiles';
        $max_kw = (int) $conf['max_keywords'];
        $updated = $wpdb->update(
            $t_profiles,
            array(
                'radar_plan'   => $plan,
                'max_keywords' => $max_kw,
            ),
            array('user_id' => $uid),
            array('%s', '%d'),
            array('%d')
        );
        // Force un rescan immédiat au prochain CRON (quota mis à jour).
        $wpdb->update(
            $t_profiles,
            array('last_radar_scan' => null),
            array('user_id' => $uid),
            array('%s'),
            array('%d')
        );

        $label = (string) $conf['label'];
        $msg = 'Plan ' . $label . ' activé — ' . $max_kw . ' mots-clés disponibles.';
        wp_send_json_success(array(
            'plan'         => $plan,
            'max_keywords' => $max_kw,
            'label'        => $label,
            'message'      => $msg,
            'version'      => '1.0',
            'rows_updated' => (int) ($updated === false ? 0 : $updated),
        ));
        wp_die();
    }

    /**
     * Endpoint de validation de niveau — côté serveur.
     * Déclenche do_action('avi_level_validated', $user_id, $new_level).
     *
     * @sprint S10-B prereq v1.0
     */
    public static function handle_validate_level() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }

        check_ajax_referer( 'avision_premium_nonce', 'nonce' );

        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }

        $user_id = function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0;
        $raw_level = isset( $_POST['level'] ) ? wp_unslash( $_POST['level'] ) : '';
        $raw_level = is_scalar( $raw_level ) ? (string) $raw_level : '';
        $raw_level = sanitize_text_field( $raw_level );

        $new_level = 0;
        if ( preg_match( '/^level\s*(\d{1,2})$/i', $raw_level, $m ) ) {
            $new_level = (int) $m[1];
        } elseif ( preg_match( '/^\d{1,2}$/', $raw_level ) ) {
            $new_level = (int) $raw_level;
        }
        if ( $new_level < 1 || $new_level > 21 ) {
            wp_send_json_error( array( 'code' => 'invalid_level' ), 400 );
            wp_die();
        }

        global $wpdb;
        if ( ! isset( $wpdb ) || ! $wpdb ) {
            wp_send_json_error( array( 'code' => 'no_db' ), 500 );
            wp_die();
        }

        $table = $wpdb->prefix . 'avi_saas_profiles';
        $current = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT current_level FROM `{$table}` WHERE user_id = %d ORDER BY id DESC LIMIT 1",
                $user_id
            )
        );

        if ( $new_level > $current ) {
            $wpdb->update(
                $table,
                array( 'current_level' => $new_level ),
                array( 'user_id' => $user_id ),
                array( '%d' ),
                array( '%d' )
            );
            if ( function_exists( 'do_action' ) ) {
                do_action( 'avi_level_validated', $user_id, $new_level );
            }
        }

        wp_send_json_success(
            array(
                'level'   => $new_level,
                'version' => '1.0',
            )
        );
        wp_die();
    }

    /**
     * Vérifie sur la fiche Google (cache NAP / Places API) qu'une action niveau est réellement faite.
     * Réponse : wp_send_json_success( verified true ) ou wp_send_json_error( verified false + message ).
     */
    public static function handle_verify_level_gbp() {
        $nonce = sanitize_text_field( isset( $_POST['nonce'] ) ? wp_unslash( $_POST['nonce'] ) : '' );
        if ( ! wp_verify_nonce( $nonce, 'avision_premium_nonce' )
            && ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => 'Unauthorized' ) );
            return;
        }

        $user_id    = get_current_user_id();
        $level_id   = sanitize_text_field( isset( $_POST['level_id'] ) ? wp_unslash( $_POST['level_id'] ) : '' );
        $gbp_signal = sanitize_text_field( isset( $_POST['gbp_signal'] ) ? wp_unslash( $_POST['gbp_signal'] ) : '' );

        if ( ! $user_id || ! $level_id || ! $gbp_signal ) {
            wp_send_json_error( array( 'message' => 'Missing parameters' ) );
            return;
        }

        global $wpdb;
        if ( ! isset( $wpdb ) || ! $wpdb ) {
            wp_send_json_success( array(
                'verified' => true,
                'message'  => 'Database unavailable — action assumed done.',
            ) );
            return;
        }

        $table   = $wpdb->prefix . 'avi_saas_profiles';
        $profile = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT id, google_place_id, google_location_name,
                        google_access_token, google_data_cache,
                        review_count, average_rating,
                        business_name, city, category
                 FROM `{$table}`
                 WHERE user_id = %d LIMIT 1",
                $user_id
            ),
            ARRAY_A
        );

        if ( ! $profile ) {
            wp_send_json_success( array(
                'verified' => true,
                'message'  => 'Profile not found — action assumed done.',
            ) );
            return;
        }

        $verified = false;
        $message  = '';
        $cache    = ! empty( $profile['google_data_cache'] )
            ? json_decode( $profile['google_data_cache'], true )
            : array();
        if ( ! is_array( $cache ) ) {
            $cache = array();
        }
        $nap = isset( $cache['nap'] ) && is_array( $cache['nap'] ) ? $cache['nap'] : array();

        switch ( $gbp_signal ) {
            case 'nap_category':
                $cat = $nap['categories']['primaryCategory']['displayName']
                    ?? ( $nap['categories']['primaryCategory']['name'] ?? '' );
                if ( empty( $cat ) ) {
                    $cat = $profile['category'] ?? '';
                }
                $verified = ! empty( $cat );
                $message  = $verified ? '' :
                    'No primary category found on your Google Business Profile yet. Go to "About" → "Category" to add it.';
                break;

            case 'nap_description':
                $desc     = $nap['profile']['description'] ?? '';
                $verified = mb_strlen( trim( $desc ) ) >= 100;
                $message  = $verified ? '' :
                    'Your Google Business Profile description is missing or too short (min. 100 characters). Go to "About" → "Description".';
                break;

            case 'nap_website':
                $verified = ! empty( $nap['websiteUri'] );
                $message  = $verified ? '' :
                    'No website URL found on your Google Business Profile yet. Add it in "Contact information".';
                break;

            case 'nap_phone':
                $phone     = $nap['phoneNumbers'] ?? array();
                $has_phone = false;
                if ( ! empty( $phone['primaryPhone'] ) ) {
                    $has_phone = true;
                } elseif ( ! empty( $phone ) && is_array( $phone ) && count( $phone ) > 0 ) {
                    $has_phone = true;
                }
                $verified = $has_phone;
                $message  = $verified ? '' :
                    'No phone number found on your Google Business Profile yet. Add it in "Contact information".';
                break;

            case 'nap_hours':
                $hours    = $nap['regularHours'] ?? array();
                $verified = ! empty( $hours['periods'] ) && is_array( $hours['periods'] )
                    && count( $hours['periods'] ) > 0;
                $message  = $verified ? '' :
                    'No business hours found on your Google Business Profile yet. Go to "Hours" to add your opening times.';
                break;

            case 'photos':
                $place_id   = isset( $profile['google_place_id'] ) ? (string) $profile['google_place_id'] : '';
                $places_key = defined( 'AVI_GOOGLE_PLACES_API_KEY' )
                    ? AVI_GOOGLE_PLACES_API_KEY
                    : '';
                if ( $place_id !== '' && $places_key !== '' ) {
                    $place_resource = $place_id;
                    if ( strpos( $place_resource, 'places/' ) === 0 ) {
                        $place_resource = substr( $place_resource, 7 );
                    }
                    $url  = 'https://places.googleapis.com/v1/places/' . rawurlencode( $place_resource );
                    $resp = wp_remote_get(
                        $url,
                        array(
                            'headers' => array(
                                'X-Goog-Api-Key'   => $places_key,
                                'X-Goog-FieldMask' => 'photos',
                            ),
                            'timeout' => 10,
                        )
                    );
                    if ( ! is_wp_error( $resp )
                        && (int) wp_remote_retrieve_response_code( $resp ) === 200 ) {
                        $body = json_decode( wp_remote_retrieve_body( $resp ), true );
                        $verified = ! empty( $body['photos'] )
                            && count( $body['photos'] ) >= 1;
                    } else {
                        $verified = true;
                    }
                } else {
                    $verified = true;
                }
                $message = $verified ? '' :
                    'No photos found on your Google Business Profile yet. Add at least one photo.';
                break;

            case 'reviews':
                $verified = (int) ( $profile['review_count'] ?? 0 ) >= 1;
                $message  = $verified ? '' :
                    'No reviews yet on your Google profile. Ask your first customer to leave a review.';
                break;

            default:
                $verified = true;
                break;
        }

        if ( $verified ) {
            wp_send_json_success( array(
                'verified' => true,
                'message'  => 'Verified on Google ✅',
            ) );
        } else {
            wp_send_json_error( array(
                'verified' => false,
                'message'  => $message,
            ) );
        }
    }

    /**
     * Crée une session Checkout Stripe pour un produit S10-B (metadata subscription).
     *
     * @sprint S10-B v1.0
     */
    public static function handle_stripe_s10b_checkout() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            self::avision_wp_send_json_error_http( array( 'code' => 'missing_wp_functions' ), 500 );
        }
        check_ajax_referer( 'avision_premium_nonce', 'nonce' );
        $product_peek = isset( $_POST['avi_product'] ) ? sanitize_key( wp_unslash( $_POST['avi_product'] ) ) : '';
        $is_agency_product  = in_array( $product_peek, array( 'ltd_agency_t1', 'ltd_agency_t2', 'ltd_agency_t3' ), true );
        $is_guest_allowed   = in_array( $product_peek, array( 'ltd_solo' ), true );
        if ( ! is_user_logged_in() && ! $is_agency_product && ! $is_guest_allowed ) {
            self::avision_wp_send_json_error_http( array( 'code' => 'not_logged_in' ), 401 );
        }
        if ( ! function_exists( 'avi_create_stripe_checkout_session' ) || ! function_exists( 'avi_get_stripe_plan_config' ) ) {
            self::avision_wp_send_json_error_http(
                array(
                    'code'    => 'stripe_s10b_missing',
                    'message' => 'Stripe checkout module is not loaded.',
                ),
                500
            );
        }
        $uid = (int) get_current_user_id();
        $product = isset( $_POST['avi_product'] ) ? sanitize_key( wp_unslash( $_POST['avi_product'] ) ) : '';
        $currency = isset( $_POST['currency'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['currency'] ) ) ) : 'eur';
        if ( ! in_array( $currency, array( 'eur', 'usd' ), true ) ) {
            $currency = 'eur';
        }
        $plan_cfg = avi_get_stripe_plan_config();
        if ( $product === '' || ! isset( $plan_cfg[ $product ] ) ) {
            self::avision_wp_send_json_error_http( array( 'code' => 'invalid_product' ), 400 );
        }
        $checkout_url = avi_create_stripe_checkout_session( $uid, $product, $currency );
        if ( $checkout_url === '' || ( is_string( $checkout_url ) && strpos( $checkout_url, '#' ) === 0 ) ) {
            if ( function_exists( 'error_log' ) ) {
                error_log(
                    '[AVI-CHECKOUT-FAIL] product=' . $product
                    . ' url=' . (string) $checkout_url
                    . ' user=' . $uid
                );
            }
            $code = ( $checkout_url === '#price-not-configured' ) ? 'price_not_configured' : 'checkout_failed';
            self::avision_wp_send_json_error_http(
                array(
                    'message' => 'Payment unavailable: ' . (string) $checkout_url,
                    'code'    => $code,
                    'product' => $product,
                ),
                ( $code === 'price_not_configured' ) ? 400 : 500
            );
        }
        wp_send_json_success(
            array(
                'url'     => $checkout_url,
                'version' => '1.0',
            )
        );
        wp_die();
    }

    /**
     * Met à jour un abonnement Stripe (proration) ou renvoie une URL Checkout si impossible.
     */
    public static function handle_upgrade_subscription() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            self::avision_wp_send_json_error_http( array( 'message' => 'missing_wp_functions' ), 500 );
        }
        check_ajax_referer( 'avision_premium_nonce', 'nonce' );
        if ( ! is_user_logged_in() ) {
            self::avision_wp_send_json_error_http( array( 'message' => 'Not logged in' ), 401 );
        }
        if ( ! function_exists( 'avi_get_stripe_plan_config' ) || ! function_exists( 'avi_stripe_s10b_resolve_secret_key' ) ) {
            self::avision_wp_send_json_error_http(
                array(
                    'code'    => 'stripe_s10b_missing',
                    'message' => 'Stripe S10-B module is not loaded.',
                ),
                500
            );
        }
        $user_id     = (int) get_current_user_id();
        $new_product = isset( $_POST['new_product'] ) ? sanitize_key( wp_unslash( $_POST['new_product'] ) ) : '';
        $current_sub = isset( $_POST['current_sub'] ) ? sanitize_text_field( wp_unslash( $_POST['current_sub'] ) ) : '';
        $current_sub = preg_match( '/^sub_[A-Za-z0-9]+$/', $current_sub ) ? $current_sub : '';

        $config_all = avi_get_stripe_plan_config();
        $config     = isset( $config_all[ $new_product ] ) ? $config_all[ $new_product ] : null;
        if ( ! $config ) {
            self::avision_wp_send_json_error_http( array( 'message' => 'Invalid product' ), 400 );
        }
        $price_key = isset( $config['price_key'] ) ? (string) $config['price_key'] : '';
        $new_price_id = ( $price_key !== '' && function_exists( 'get_option' ) ) ? trim( (string) get_option( $price_key, '' ) ) : '';
        if ( $new_price_id === '' || strpos( $new_price_id, 'price_' ) !== 0 ) {
            self::avision_wp_send_json_error_http( array( 'message' => 'Price not configured' ), 400 );
        }

        $secret_key = avi_stripe_s10b_resolve_secret_key();
        if ( $secret_key === '' ) {
            self::avision_wp_send_json_error_http( array( 'message' => 'Stripe not configured' ), 500 );
        }

        global $wpdb;
        if ( $current_sub !== '' && isset( $wpdb ) && is_object( $wpdb ) ) {
            $sync = $wpdb->prefix . 'avi_stripe_sync';
            if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $sync ) ) === $sync ) {
                $owned = (int) $wpdb->get_var(
                    $wpdb->prepare(
                        "SELECT id FROM `{$sync}` WHERE user_id = %d AND stripe_subscription_id = %s AND action_type = %s ORDER BY processed_at DESC LIMIT 1",
                        $user_id,
                        $current_sub,
                        'activate'
                    )
                );
                if ( $owned > 0 ) {
                    $sub_resp = wp_remote_get(
                        'https://api.stripe.com/v1/subscriptions/' . rawurlencode( $current_sub ),
                        array(
                            'timeout' => 20,
                            'headers' => array(
                                'Authorization' => 'Bearer ' . $secret_key,
                            ),
                        )
                    );
                    $sub_body = is_wp_error( $sub_resp ) ? '' : (string) wp_remote_retrieve_body( $sub_resp );
                    $sub      = $sub_body !== '' ? json_decode( $sub_body ) : null;
                    $item_id  = '';
                    if ( is_object( $sub ) && isset( $sub->items->data ) && is_array( $sub->items->data ) && ! empty( $sub->items->data[0] ) && isset( $sub->items->data[0]->id ) ) {
                        $item_id = (string) $sub->items->data[0]->id;
                    }
                    if ( $item_id !== '' ) {
                        $upgrade = wp_remote_post(
                            'https://api.stripe.com/v1/subscriptions/' . rawurlencode( $current_sub ),
                            array(
                                'timeout' => 25,
                                'headers' => array(
                                    'Authorization' => 'Bearer ' . $secret_key,
                                ),
                                'body'    => array(
                                    'items[0][id]'           => $item_id,
                                    'items[0][price]'        => $new_price_id,
                                    'proration_behavior'     => 'create_prorations',
                                    'metadata[avi_product]'  => $new_product,
                                    'metadata[user_id]'    => (string) $user_id,
                                ),
                            )
                        );
                        $up_body = is_wp_error( $upgrade ) ? '' : (string) wp_remote_retrieve_body( $upgrade );
                        $result  = $up_body !== '' ? json_decode( $up_body ) : null;
                        if ( is_object( $result ) && ! empty( $result->id ) && empty( $result->error ) ) {
                            $portal = function_exists( 'get_option' ) ? trim( (string) get_option( 'avi_stripe_customer_portal_url', '' ) ) : '';
                            wp_send_json_success(
                                array(
                                    'message'    => 'Upgrade successful',
                                    'portal_url' => ( $portal !== '' && $portal !== '#' ) ? $portal : '',
                                )
                            );
                            wp_die();
                        }
                        $err_msg = ( is_object( $result ) && isset( $result->error->message ) ) ? (string) $result->error->message : 'Stripe upgrade failed';
                        if ( function_exists( 'error_log' ) ) {
                            error_log( '[AVI-S11] upgrade_subscription stripe_error ' . $err_msg );
                        }
                    }
                }
            }
        }

        if ( ! function_exists( 'avi_create_stripe_checkout_session' ) ) {
            self::avision_wp_send_json_error_http( array( 'message' => 'Checkout unavailable' ), 500 );
        }
        $currency = isset( $_POST['currency'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['currency'] ) ) ) : 'eur';
        if ( ! in_array( $currency, array( 'eur', 'usd' ), true ) ) {
            $currency = 'eur';
        }
        $checkout_url = avi_create_stripe_checkout_session( $user_id, $new_product, $currency );
        if ( $checkout_url === '#price-not-configured' || $checkout_url === '' ) {
            self::avision_wp_send_json_error_http( array( 'message' => 'Could not start checkout' ), 500 );
        }
        wp_send_json_success(
            array(
                'message'    => 'Redirecting to checkout',
                'portal_url' => $checkout_url,
            )
        );
        wp_die();
    }

    public static function handle_ajax_request() {
        /*
         * Bypass « béton armé » : rôle administrator lu brutement sur l’objet user (repli si current_user_can
         * se comporte mal en AJAX). Aucun wp_verify_nonce / check_ajax_referer pour ce profil.
         */
        $avi_router_action = 'unknown';
        if (isset($_REQUEST['avision_action'])) {
            $avi_router_action = is_string($_REQUEST['avision_action'])
                ? sanitize_text_field(wp_unslash($_REQUEST['avision_action']))
                : 'unknown';
        }
        $wp_user_router = function_exists('wp_get_current_user') ? wp_get_current_user() : null;
        $has_administrator_role = (
            $wp_user_router
            && isset($wp_user_router->ID)
            && (int) $wp_user_router->ID > 0
            && !empty($wp_user_router->roles)
            && is_array($wp_user_router->roles)
            && in_array('administrator', $wp_user_router->roles, true)
        );
        $has_manage_options = is_user_logged_in() && function_exists('current_user_can') && current_user_can('manage_options');
        $anthony_url_bypass = (
            isset($_REQUEST['admin_bypass'])
            && is_string($_REQUEST['admin_bypass'])
            && sanitize_text_field(wp_unslash($_REQUEST['admin_bypass'])) === 'anthony'
        );
        $avi_admin_concrete_bypass = (is_user_logged_in() && ($has_administrator_role || $has_manage_options)) || $anthony_url_bypass;

        $user_id = (int) get_current_user_id();
        if (function_exists('error_log')) {
            error_log('[AVI-AUTH-CHECK] User ID: ' . (string) $user_id);
        }
        if ($user_id === 0 && isset($_REQUEST['admin_bypass']) && $_REQUEST['admin_bypass'] === 'anthony') {
            $user_id = 1;
            if (function_exists('error_log')) {
                error_log('[AVI-AUTH-FORCE] Session invisible, forcage ID 1 via bypass.');
            }
            if (function_exists('wp_set_current_user')) {
                wp_set_current_user($user_id);
            }
        }

        if ($anthony_url_bypass && function_exists('error_log')) {
            error_log('[AVI-ROUTER] admin_bypass=anthony → nonce ignoré (action: ' . $avi_router_action . ')');
        }

        if ($avi_admin_concrete_bypass) {
            if (function_exists('error_log')) {
                error_log('[AVI-ROUTER] Admin Bypass Triggered for action: ' . $avi_router_action);
            }
        } else {
            $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
            if (!self::avision_verify_nonce_or_admin_bypass($nonce)) {
                self::avision_router_send_json_response(
                    $avi_router_action,
                    array(
                        'error'   => 'invalid_nonce',
                        'message' => __('Jeton de sécurité invalide ou manquant. Rechargez la page (F5 ou Ctrl+F5). Après un changement des clés SALT WordPress, un rechargement complet est obligatoire.', 'avision-dashboard'),
                    ),
                    false
                );
            }
        }

        $endpoint = isset($_POST['endpoint']) ? sanitize_text_field($_POST['endpoint']) : '';
        $avision_action = isset($_POST['avision_action']) ? sanitize_text_field($_POST['avision_action']) : '';
        if ($endpoint === '' && $avision_action === 'send_chat_message') {
            $endpoint = 'chat';
        }
        if ($endpoint === '' && $avision_action === 'level_21_panel') {
            $endpoint = 'level_21_panel';
        }

        if ($endpoint === 'level_21_panel') {
            $city = isset($_POST['city']) ? sanitize_text_field(wp_unslash($_POST['city'])) : '';
            $lang = isset($_POST['lang']) ? sanitize_text_field($_POST['lang']) : 'en';
            $sector = isset($_POST['sector']) ? sanitize_text_field($_POST['sector']) : '';
            $l21_count = isset($_POST['l21_validation_count']) ? max(0, (int) $_POST['l21_validation_count']) : 0;
            $l21_names_raw = isset($_POST['l21_validated_names']) ? wp_unslash($_POST['l21_validated_names']) : '[]';
            $l21_names = json_decode($l21_names_raw, true);
            if (!is_array($l21_names)) {
                $l21_names = array();
            }
            $body = array(
                'city' => $city,
                'lang' => $lang,
                'sector' => $sector,
                'l21_validation_count' => $l21_count,
                'l21_validated_names' => $l21_names,
            );
            if (!class_exists('Avision\Avision_API')) {
                if (defined('AVISION_PATH') && is_readable(AVISION_PATH . 'includes/class-avision-api.php')) {
                    require_once AVISION_PATH . 'includes/class-avision-api.php';
                }
            }
            if (!class_exists('Avision\Avision_API')) {
                error_log('❌ CRITICAL: Avision_API class not found!');
                self::avision_router_send_json_response($avi_router_action, array('error' => 'api_class_missing'), false);
            }
            $result = \Avision\Avision_API::proxy_level21_panel_to_fastapi($body);
            if (is_wp_error($result)) {
                self::avision_router_send_json_response($avi_router_action, array('error' => $result->get_error_message()), false);
            } else {
                self::avision_router_send_json_response($avi_router_action, $result, true);
            }
        }

        if ($endpoint === 'chat') {
            if (!class_exists('Avision\Avision_API')) {
                if (defined('AVISION_PATH') && is_readable(AVISION_PATH . 'includes/class-avision-api.php')) {
                    require_once AVISION_PATH . 'includes/class-avision-api.php';
                }
            }
            if (!class_exists('Avision\Avision_API')) {
                error_log('❌ CRITICAL: Avision_API class not found!');
                self::avision_router_send_json_response(
                    $avi_router_action,
                    array(
                        'error'   => 'api_class_missing',
                        'message' => __('Module API Avision indisponible. Rechargez la page ou contactez l’administrateur.', 'avision-dashboard'),
                    ),
                    false
                );
            }
            $message = isset($_POST['message']) ? wp_unslash($_POST['message']) : '';
            if (is_string($message)) {
                $message = sanitize_textarea_field($message);
            }
            $dashboard_type_early = isset($_POST['dashboard_type']) && is_string($_POST['dashboard_type']) ? strtolower(sanitize_text_field(wp_unslash($_POST['dashboard_type']))) : '';
            $is_b2b_chat_ajax = in_array($dashboard_type_early, array('b2b', 'b2b_fleet', 'agency', 'fleet', 'b2b_agency'), true);
            if ($is_b2b_chat_ajax) {
                // B2B fleet: force level=null to prevent B2C Level 1 routing in FastAPI
                $level = null;
            } else {
                $level = isset($_POST['level']) ? intval($_POST['level']) : (isset($_POST['currentLevel']) ? intval($_POST['currentLevel']) : 1);
                $level = max(1, min(21, $level));
            }
            $score = isset($_POST['score']) ? intval($_POST['score']) : 0;
            if ($is_b2b_chat_ajax) {
                $score = 0;
            }
            $fleet_kpis_param = null;
            if (!empty($_POST['fleet_kpis'])) {
                $raw_fk = wp_unslash($_POST['fleet_kpis']);
                if (is_string($raw_fk)) {
                    $fk_dec = json_decode($raw_fk, true);
                    if (is_array($fk_dec)) {
                        $fleet_kpis_param = $fk_dec;
                    }
                }
            }

            $user_status = isset($_POST['user_status']) ? sanitize_text_field($_POST['user_status']) : '';
            $l21_names = array();
            if (!empty($_POST['l21_validated_names'])) {
                $raw = wp_unslash($_POST['l21_validated_names']);
                if (is_string($raw)) {
                    $dec = json_decode($raw, true);
                    if (is_array($dec)) {
                        $l21_names = array_map('sanitize_text_field', $dec);
                    }
                }
            }
            // Langue : POST language/lang (reflète l’URL côté navigateur) + repli Referer ?lang= / ?language= — jamais get_locale().
            $language = \Avision\Avision_API::normalize_chat_language_for_fastapi(array(
                'language' => isset($_POST['language']) ? sanitize_text_field(wp_unslash($_POST['language'])) : '',
                'lang'     => isset($_POST['lang']) ? sanitize_text_field(wp_unslash($_POST['lang'])) : '',
            ));
            error_log('[AVI-LANG-DEBUG] POST language/lang → resolved=' . $language . ' (no WP locale)');
            $review_rating = isset($_POST['review_rating']) ? max(1, min(5, (int) $_POST['review_rating'])) : null;
            if ($review_rating === 0) $review_rating = null;
            $review_context = isset($_POST['review_context']) ? sanitize_text_field(wp_unslash($_POST['review_context'])) : '';
            $system_message = isset($_POST['systemMessage']) && is_string($_POST['systemMessage']) ? sanitize_textarea_field(wp_unslash($_POST['systemMessage'])) : '';
            $dashboard_type = isset($_POST['dashboard_type']) && is_string($_POST['dashboard_type']) ? sanitize_text_field(wp_unslash($_POST['dashboard_type'])) : '';
            $google_primary_category = '';
            if (isset($_POST['google_primary_category']) && is_string($_POST['google_primary_category'])) {
                $google_primary_category = sanitize_text_field(wp_unslash($_POST['google_primary_category']));
            }
            $params = array(
                'message'       => $message,
                'query'         => $message,
                'score'         => $score,
                'sector'        => isset($_POST['sector']) ? sanitize_text_field($_POST['sector']) : '',
                'chatHistory'   => isset($_POST['chatHistory']) ? $_POST['chatHistory'] : '[]',
                'user_status'   => $user_status !== '' ? $user_status : null,
                'business_name' => isset($_POST['business_name']) ? sanitize_text_field(wp_unslash($_POST['business_name'])) : '',
                'l21_validation_count' => isset($_POST['l21_validation_count']) ? max(0, (int) $_POST['l21_validation_count']) : null,
                'l21_validated_names'  => $l21_names,
                'profile_id'     => isset($_POST['profile_id']) ? sanitize_text_field(wp_unslash($_POST['profile_id'])) : '',
                'user_id'       => isset($_POST['user_id']) ? sanitize_text_field(wp_unslash($_POST['user_id'])) : '',
                'language'      => $language,
                'lang'          => $language,
                'review_rating' => $review_rating,
                'review_context' => $review_context !== '' ? $review_context : null,
                'pid'           => isset($_POST['pid']) ? sanitize_text_field(wp_unslash($_POST['pid'])) : '',
                'aid'           => isset($_POST['aid']) ? sanitize_text_field(wp_unslash($_POST['aid'])) : '',
            );
            if ($system_message !== '') {
                $params['systemMessage'] = $system_message;
            }
            if ($dashboard_type !== '') {
                $params['dashboard_type'] = $dashboard_type;
            }
            // B2B fleet: don't transmit primary category (triggers B2C Level 1 routing in FastAPI)
            if ($google_primary_category !== '' && !$is_b2b_chat_ajax) {
                $params['google_primary_category'] = $google_primary_category;
            }
            if ($level !== null) {
                $params['currentLevel'] = $level;
                $params['level'] = $level;
            }
            // Toujours enrichir fleet_kpis avec profils DB réels
            if (!is_array($fleet_kpis_param)) {
                $fleet_kpis_param = array();
            }
            if ($is_b2b_chat_ajax && class_exists('Avision\\Avision_API')) {
                $fleet_kpis_param = \Avision\Avision_API::enrich_fleet_kpis_with_profiles_public(
                    $fleet_kpis_param,
                    get_current_user_id()
                );
            }
            // ANTI-RÉGRESSION #209k [28 mai 2026] — agency_name dans params B2B
            // Raison : orchestrator.py utilise agency_name pour nommer l'agence dans les réponses
            // get_current_user_id() = user connecté → avi_get_agency_name() = nom affiché dashboard
            if ( $is_b2b_chat_ajax && function_exists( 'avi_get_agency_name' ) ) {
                // ANTI-RÉGRESSION #209k-fix [28 mai 2026] — get_current_user_id()=0 en AJAX
                // Utiliser $params['user_id'] (envoyé par JS depuis data-user-id)
                $agency_uid = isset( $params['user_id'] ) && (int) $params['user_id'] > 0
                    ? (int) $params['user_id']
                    : (int) get_current_user_id();
                if ( $agency_uid > 0 ) {
                    $agency_nm = avi_get_agency_name( $agency_uid );
                    if ( is_string( $agency_nm ) && trim( $agency_nm ) !== '' ) {
                        $params['agency_name'] = sanitize_text_field( $agency_nm );
                    }
                }
            }
            if (!empty($fleet_kpis_param)) {
                $params['fleet_kpis'] = $fleet_kpis_param;
            }
            if (!empty($_POST['technical_context']) && is_string($_POST['technical_context'])) {
                $params['technical_context'] = sanitize_textarea_field(wp_unslash($_POST['technical_context']));
            }

            $api = new \Avision\Avision_API();
            $api->init();
            $result = $api->proxy_chat_to_fastapi_params($params);

            if (!empty($result['success'])) {
                self::avision_router_send_json_response($avi_router_action, $result, true);
            } else {
                self::avision_router_send_json_response(
                    $avi_router_action,
                    isset($result['error']) ? array('error' => $result['error']) : $result,
                    false
                );
            }
        }

        self::avision_router_send_json_response($avi_router_action, array('error' => 'Endpoint non reconnu'), false);
    }

    /**
     * Endpoint AJAX : compteurs Mode Gardien (Aujourd'hui, 7j, 30j, Total).
     * Connecté : legacy sans profile_id = get_current_user_id() ; avec profile_id = user_id SaaS du profil si ACL OK.
     * Invité : profile_id + referer / lien partagé (avi_premium_referer_allows_shared_profile_readonly ou équivalent).
     */
    public static function handle_guardian_stats() {
        $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
        if (!self::avision_verify_nonce_or_admin_bypass($nonce)) {
            wp_send_json_error(array('error' => 'invalid_nonce'));
            return;
        }

        $user_id = self::resolve_guardian_logs_user_id_for_ajax();
        if ($user_id === -1) {
            wp_send_json_error(array('message' => 'unauthorized'), 401);
            return;
        }
        if ($user_id <= 0) {
            wp_send_json_error(array('error' => 'not_logged_in', 'message' => 'unauthorized'), 401);
            return;
        }

        if (class_exists('Avision\Avision_Guardian_Watchers')) {
            $stats = \Avision\Avision_Guardian_Watchers::get_guardian_stats($user_id);
        } else {
            $stats = array('today' => 0, 'week' => 0, 'month' => 0, 'total' => 0);
        }

        wp_send_json_success($stats);
    }

    /**
     * Handler AJAX — déclenche l'email galerie vide (une seule fois).
     * ANTI-RÉGRESSION #65 #90 [Sprint 32]
     *
     * @return void
     */
    public static function handle_gallery_warning_email() {
        $nonce = isset($_POST['nonce'])
            ? sanitize_text_field(wp_unslash($_POST['nonce']))
            : '';

        if (!self::avision_verify_nonce_or_admin_bypass($nonce)) {
            wp_send_json_error(array('error' => 'invalid_nonce'));
            return;
        }

        if (!is_user_logged_in()) {
            wp_send_json_error(array('error' => 'not_logged_in'));
            return;
        }

        $profile_id = isset($_POST['profile_id'])
            ? (int) $_POST['profile_id']
            : 0;

        if ($profile_id <= 0) {
            wp_send_json_error(array('error' => 'invalid_profile'));
            return;
        }

        $context = isset($_POST['context'])
            ? sanitize_text_field(wp_unslash($_POST['context']))
            : 'guardian';

        $allowed_contexts = array('guardian', 'level3', 'level20');
        if (!in_array($context, $allowed_contexts, true)) {
            $context = 'guardian';
        }

        $sent = \Avision\Avision_Notifications::send_gallery_warning_email($profile_id, $context);

        wp_send_json_success(array('sent' => (bool) $sent));
    }

    public static function handle_get_media_stats() {
        // ANTI-RÉGRESSION [06 juin 2026] — Lecture DB avi_saas_media
        // Ne jamais lire localStorage ici — source de vérité = DB uniquement
        check_ajax_referer('avision_premium_nonce', 'nonce');
        $profile_id = (int) ($_POST['profile_id'] ?? 0);
        if ($profile_id <= 0) {
            wp_send_json_error(array('message' => 'Invalid profile_id'));
            return;
        }
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_media';
        $in_stock = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE profile_id = %d AND status = 'in_stock'",
            $profile_id
        ));
        $published = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE profile_id = %d AND status = 'published'",
            $profile_id
        ));
        $last_published = $wpdb->get_var($wpdb->prepare(
            "SELECT published_at FROM {$table} WHERE profile_id = %d AND status = 'published' ORDER BY published_at DESC LIMIT 1",
            $profile_id
        ));
        wp_send_json_success(array(
            'in_stock'       => $in_stock,
            'published'      => $published,
            'total'          => $in_stock + $published,
            'low_stock'      => $in_stock <= 3 && $in_stock > 0,
            'empty'          => $in_stock === 0,
            'last_published' => $last_published,
        ));
    }

    public static function handle_get_scheduled_bulk_posts() {
        // ANTI-RÉGRESSION [06 juin 2026] — Lecture posts bulk planifiés depuis DB
        // ANTI-RÉGRESSION [06 juin 2026] — bulkPublishNonce disponible dans AvisionAgencyDashboard
        if ( ! check_ajax_referer( 'avi_bulk_publish_google_post', 'nonce', false ) &&
             ! check_ajax_referer( 'avision_agency_nonce', 'nonce', false ) ) {
            wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
            return;
        }
        $user_id = (int) get_current_user_id();
        if ( $user_id <= 0 ) {
            wp_send_json_error( array( 'message' => 'Unauthorized' ) );
            return;
        }
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_posts';
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT p.id, p.profile_id, p.status, p.scheduled_at,
                        p.published_at, LEFT(p.content, 80) as preview,
                        pr.business_name
                 FROM {$table} p
                 LEFT JOIN {$wpdb->prefix}avi_saas_profiles pr ON pr.id = p.profile_id
                 WHERE p.agency_user_id = %d
                   AND p.status IN ('pending','publishing','published','failed')
                   AND p.scheduled_at IS NOT NULL
                 ORDER BY p.scheduled_at DESC
                 LIMIT 30",
                $user_id
            ),
            ARRAY_A
        );
        wp_send_json_success( array(
            'posts'   => is_array( $rows ) ? $rows : array(),
            'pending' => count( array_filter( (array) $rows, function($r) {
                return in_array( $r['status'], array('pending','publishing'), true );
            })),
        ));
    }

    public static function handle_cancel_scheduled_bulk_post() {
        // ANTI-RÉGRESSION [06 juin 2026] — Annulation post pending uniquement
        if ( ! check_ajax_referer( 'avi_bulk_publish_google_post', 'nonce', false ) &&
             ! check_ajax_referer( 'avision_agency_nonce', 'nonce', false ) ) {
            wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
            return;
        }
        $user_id = (int) get_current_user_id();
        $post_id = (int) ( $_POST['post_id'] ?? 0 );
        if ( $user_id <= 0 || $post_id <= 0 ) {
            wp_send_json_error( array( 'message' => 'Invalid params' ) );
            return;
        }
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_posts';
        // Vérifier que le post appartient à cet utilisateur ET est encore pending
        $row = $wpdb->get_row( $wpdb->prepare(
            "SELECT id, agency_user_id, status FROM {$table} WHERE id = %d LIMIT 1",
            $post_id
        ), ARRAY_A );
        if ( ! $row ) {
            wp_send_json_error( array( 'message' => 'Post not found' ) );
            return;
        }
        if ( (int) $row['agency_user_id'] !== $user_id ) {
            wp_send_json_error( array( 'message' => 'Forbidden' ) );
            return;
        }
        if ( ! in_array( $row['status'], array( 'pending', 'publishing' ), true ) ) {
            wp_send_json_error( array( 'message' => 'Cannot cancel — post already ' . $row['status'] ) );
            return;
        }
        $ok = $wpdb->update(
            $table,
            array( 'status' => 'rejected' ),
            array( 'id' => $post_id ),
            array( '%s' ),
            array( '%d' )
        );
        if ( $ok !== false ) {
            wp_send_json_success( array( 'cancelled' => true ) );
        } else {
            wp_send_json_error( array( 'message' => 'DB error' ) );
        }
    }

    /**
     * Endpoint AJAX : logs du Centre d'Intervention (Timeline) — event_type real + system,
     * colonnes event_type, signal_id, status, message_json, created_at.
     */
    public static function handle_get_guardian_logs() {
        $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
        if (!self::avision_verify_nonce_or_admin_bypass($nonce)) {
            wp_send_json_error(array('error' => 'invalid_nonce'));
            return;
        }

        $user_id = self::resolve_guardian_logs_user_id_for_ajax();
        if ($user_id === -1) {
            wp_send_json_error(array('message' => 'unauthorized'), 401);
            return;
        }
        if ($user_id <= 0) {
            wp_send_json_error(array('error' => 'not_logged_in', 'message' => 'unauthorized'), 401);
            return;
        }

        $limit = isset($_POST['limit']) ? max(1, min(200, (int) $_POST['limit'])) : 80;

        if (class_exists('Avision\Avision_Guardian_Watchers')) {
            $logs = \Avision\Avision_Guardian_Watchers::get_guardian_logs($user_id, $limit);
        } else {
            $logs = array();
        }

        wp_send_json_success(array('logs' => $logs));
    }

    /**
     * AppSumo — activation LTD (email + code), connexion auto.
     */
    public static function handle_appsumo_redeem() {
        if ( ! function_exists( 'check_ajax_referer' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_appsumo_nonce', 'nonce' );
        $code  = isset( $_POST['code'] ) ? sanitize_text_field( wp_unslash( $_POST['code'] ) ) : '';
        $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
        $submitted_type = isset( $_POST['account_type'] ) ? sanitize_key( wp_unslash( $_POST['account_type'] ) ) : 'individual';
        if ( ! function_exists( 'avi_appsumo_process_redeem' ) ) {
            wp_send_json_error( array( 'code' => 'appsumo_module_missing' ), 500 );
            wp_die();
        }
        $out = avi_appsumo_process_redeem( $code, $email, $submitted_type );
        if ( is_wp_error( $out ) ) {
            wp_send_json_error(
                array(
                    'code'    => $out->get_error_code(),
                    'message' => $out->get_error_message(),
                ),
                400
            );
            wp_die();
        }
        $is_agency = ! empty( $out['is_agency'] );
        $acct      = isset( $out['account_type'] ) ? (string) $out['account_type'] : 'individual';
        wp_send_json_success(
            array(
                'user_id'          => isset( $out['user_id'] ) ? (int) $out['user_id'] : 0,
                'account_type'     => $acct,
                'is_agency'        => $is_agency,
                'plan_type'        => isset( $out['plan_type'] ) ? (string) $out['plan_type'] : '',
                'redirect'         => '',
                'redirect_premium' => isset( $out['redirect_premium'] ) ? (string) $out['redirect_premium'] : '',
                'redirect_agency'  => isset( $out['redirect_agency'] ) ? (string) $out['redirect_agency'] : '',
                'message'          => isset( $out['message'] ) ? (string) $out['message'] : __( 'Account activated!', 'avision-dashboard' ),
                'version'          => '1.2',
            )
        );
        wp_die();
    }

    /**
     * AppSumo — SetupIntent Stripe (Elements) pour essai Guardian.
     */
    public static function handle_appsumo_create_setup_intent() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_appsumo_nonce', 'nonce' );
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }
        $uid      = (int) get_current_user_id();
        $uid_post = isset( $_POST['user_id'] ) ? (int) wp_unslash( $_POST['user_id'] ) : 0;
        if ( $uid_post > 0 && $uid_post !== $uid ) {
            wp_send_json_error( array( 'code' => 'user_mismatch' ), 403 );
            wp_die();
        }
        if ( ! function_exists( 'avi_appsumo_create_setup_intent_for_user' ) ) {
            wp_send_json_error( array( 'code' => 'appsumo_module_missing' ), 500 );
            wp_die();
        }
        $out = avi_appsumo_create_setup_intent_for_user( $uid );
        if ( is_wp_error( $out ) ) {
            wp_send_json_error(
                array(
                    'code'    => $out->get_error_code(),
                    'message' => $out->get_error_message(),
                ),
                400
            );
            wp_die();
        }
        wp_send_json_success(
            array(
                'client_secret' => isset( $out['client_secret'] ) ? (string) $out['client_secret'] : '',
                'version'       => '1.1',
            )
        );
        wp_die();
    }

    /**
     * AppSumo — essai Guardian 15j via Stripe (utilisateur connecté).
     */
    public static function handle_appsumo_start_guardian_trial() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_appsumo_nonce', 'nonce' );
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }
        $pm       = isset( $_POST['payment_method_id'] ) ? sanitize_text_field( wp_unslash( $_POST['payment_method_id'] ) ) : '';
        $uid      = (int) get_current_user_id();
        $uid_post = isset( $_POST['user_id'] ) ? (int) wp_unslash( $_POST['user_id'] ) : 0;
        if ( $uid_post > 0 && $uid_post !== $uid ) {
            wp_send_json_error( array( 'code' => 'user_mismatch' ), 403 );
            wp_die();
        }
        if ( ! function_exists( 'avi_appsumo_create_guardian_trial_subscription' ) ) {
            wp_send_json_error( array( 'code' => 'appsumo_module_missing' ), 500 );
            wp_die();
        }
        $res = avi_appsumo_create_guardian_trial_subscription( $uid, $pm );
        if ( is_wp_error( $res ) ) {
            wp_send_json_error( array( 'code' => $res->get_error_code(), 'message' => $res->get_error_message() ), 400 );
            wp_die();
        }
        $redirect = ( is_array( $res ) && ! empty( $res['redirect'] ) )
            ? (string) $res['redirect']
            : ( function_exists( 'avi_appsumo_after_guardian_trial_redirect_url' ) ? avi_appsumo_after_guardian_trial_redirect_url() : '' );
        wp_send_json_success(
            array(
                'ok'        => true,
                'redirect'  => $redirect,
                'version'   => '1.1',
            )
        );
        wp_die();
    }

    /**
     * AppSumo — import CSV codes (admin).
     */
    public static function handle_appsumo_import_csv() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'current_user_can' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_appsumo_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'code' => 'forbidden' ), 403 );
            wp_die();
        }
        if ( empty( $_FILES['csv_file']['tmp_name'] ) || ! is_uploaded_file( $_FILES['csv_file']['tmp_name'] ) ) {
            wp_send_json_error( array( 'code' => 'no_file' ), 400 );
            wp_die();
        }
        global $wpdb;
        $table = $wpdb->prefix . 'avi_appsumo_codes';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            wp_send_json_error( array( 'code' => 'table_missing' ), 500 );
            wp_die();
        }
        $fh = fopen( $_FILES['csv_file']['tmp_name'], 'rb' );
        if ( ! $fh ) {
            wp_send_json_error( array( 'code' => 'read_error' ), 500 );
            wp_die();
        }
        $inserted = 0;
        $dup      = 0;
        $errors   = 0;
        $line_no  = 0;
		while ( ( $row = fgetcsv( $fh, 4096, ',' ) ) !== false ) {
			$line_no++;
			if ( $line_no === 1 && isset( $row[0] ) && stripos( (string) $row[0], 'code' ) !== false ) {
				continue;
			}
			$code = isset( $row[0] ) ? strtoupper( preg_replace( '/[^A-Z0-9\-_]/', '', (string) $row[0] ) ) : '';
			$plan = isset( $row[1] ) ? sanitize_key( (string) $row[1] ) : 'solo';
			if ( $code === '' ) {
				$errors++;
				continue;
			}
			if ( ! in_array( $plan, array( 'solo', 'agency_5', 'agency_20', 'unlimited' ), true ) ) {
				$errors++;
				continue;
			}
			$res = $wpdb->query(
				$wpdb->prepare(
					"INSERT IGNORE INTO `{$table}` (code, plan_type, status) VALUES (%s, %s, %s)",
					$code,
					$plan,
					'unused'
				)
			);
			if ( $res === false ) {
				$errors++;
			} elseif ( (int) $res === 1 ) {
				$inserted++;
			} else {
				$dup++;
			}
		}
		fclose( $fh );
        wp_send_json_success(
            array(
                'inserted' => $inserted,
                'duplicates' => $dup,
                'errors'   => $errors,
                'version'  => '1.0',
            )
        );
        wp_die();
    }

    /**
     * Désinscription (raison) + email de confirmation Brevo {@see avi_s11_send_unsubscribe_confirm_email()}.
     *
     * @return void
     */
    public static function handle_avision_unsubscribe_with_reason() {
        if ( ! function_exists( 'check_ajax_referer' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_nonce', 'nonce' );
        if ( ! function_exists( 'is_user_logged_in' ) || ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }
        $user_id = (int) get_current_user_id();
        global $wpdb;
        $reason = isset( $_POST['reason'] ) ? sanitize_text_field( wp_unslash( $_POST['reason'] ) ) : '';
        $custom = isset( $_POST['custom_reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['custom_reason'] ) ) : '';
        $utype  = isset( $_POST['user_type'] ) ? sanitize_key( wp_unslash( $_POST['user_type'] ) ) : '';
        if ( $custom !== '' ) {
            $reason = trim( $reason . ( $reason !== '' ? ' | ' : '' ) . $custom );
        }
        if ( $utype !== '' ) {
            $reason = trim( $utype . ( $reason !== '' ? ': ' : '' ) . $reason );
        }
        $tbl = $wpdb->prefix . 'avi_unsubscribe_feedback';
        if ( isset( $wpdb ) && $wpdb && $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) ) === $tbl ) {
            $wpdb->insert(
                $tbl,
                array(
                    'user_id'    => $user_id,
                    'reason'     => $reason,
                    'created_at' => current_time( 'mysql', true ),
                ),
                array( '%d', '%s', '%s' )
            );
        }
        if ( function_exists( 'avi_s11_send_unsubscribe_confirm_email' ) ) {
            avi_s11_send_unsubscribe_confirm_email( $user_id );
        }
        wp_send_json_success( array( 'ok' => true, 'message' => __( 'Your preferences have been updated.', 'avision-dashboard' ) ) );
        wp_die();
    }

    /**
     * POST prefs (JSON ou tableau) → colonne email_prefs (JSON).
     *
     * @return void
     */
    public static function handle_save_email_prefs() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_nonce', 'nonce' );
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }
        $uid = (int) get_current_user_id();
        $raw    = isset( $_POST['prefs'] ) ? wp_unslash( $_POST['prefs'] ) : null;
        $incoming = array();
        if ( is_string( $raw ) ) {
            $decoded = json_decode( $raw, true );
            $incoming = is_array( $decoded ) ? $decoded : array();
        } elseif ( is_array( $raw ) ) {
            $incoming = $raw;
        }
        $whitelist = array_flip( self::avision_email_prefs_whitelist_keys() );
        $filtered  = array();
        foreach ( $incoming as $k => $v ) {
            $key = is_string( $k ) ? sanitize_key( $k ) : '';
            if ( $key === '' || ! isset( $whitelist[ $key ] ) ) {
                continue;
            }
            $filtered[ $key ] = (bool) self::avision_coerce_email_pref_bool( $v );
        }
        global $wpdb;
        if ( ! isset( $wpdb ) || ! $wpdb ) {
            wp_send_json_error( array( 'code' => 'no_db' ), 500 );
            wp_die();
        }
        $tbl = $wpdb->prefix . 'avi_saas_profiles';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) ) !== $tbl ) {
            wp_send_json_error( array( 'code' => 'no_table' ), 500 );
            wp_die();
        }
        $json = wp_json_encode( $filtered );
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $ok = $wpdb->query(
            $wpdb->prepare(
                "UPDATE `{$tbl}` SET `email_prefs` = %s WHERE `user_id` = %d",
                $json,
                $uid
            )
        );
        if ( $ok === false ) {
            wp_send_json_error( array( 'code' => 'update_failed' ), 500 );
            wp_die();
        }
        if ( (int) $ok === 0 ) {
            wp_send_json_error( array( 'code' => 'no_profile_row' ), 404 );
            wp_die();
        }
        wp_send_json_success();
        wp_die();
    }

    /**
     * Retourne les préférences email (défaut : tout true).
     *
     * @return void
     */
    public static function handle_get_email_prefs() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_nonce', 'nonce' );
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }
        $uid = (int) get_current_user_id();
        $prefs = self::avision_email_prefs_defaults();
        global $wpdb;
        if ( isset( $wpdb ) && $wpdb ) {
            $tbl = $wpdb->prefix . 'avi_saas_profiles';
            if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) ) === $tbl ) {
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $row = $wpdb->get_var( $wpdb->prepare( "SELECT `email_prefs` FROM `{$tbl}` WHERE `user_id` = %d LIMIT 1", $uid ) );
                if ( is_string( $row ) && $row !== '' ) {
                    $decoded = json_decode( $row, true );
                    if ( is_array( $decoded ) ) {
                        $wl = array_flip( self::avision_email_prefs_whitelist_keys() );
                        foreach ( $decoded as $k => $v ) {
                            if ( is_string( $k ) && isset( $wl[ $k ] ) ) {
                                $prefs[ $k ] = (bool) self::avision_coerce_email_pref_bool( $v );
                            }
                        }
                    }
                }
            }
        }
        wp_send_json_success( array( 'prefs' => $prefs ) );
        wp_die();
    }

    /**
     * Archivage compte + désactivation profil + email confirmation.
     *
     * @return void
     */
    public static function handle_delete_account() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_nonce', 'nonce' );
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }
        $user_id              = (int) get_current_user_id();
        $user_type_raw        = isset( $_POST['user_type'] ) ? sanitize_key( wp_unslash( $_POST['user_type'] ) ) : 'btoc';
        $user_type            = in_array( $user_type_raw, array( 'btoc', 'btob' ), true ) ? $user_type_raw : 'btoc';
        $reason               = isset( $_POST['reason'] ) ? sanitize_text_field( wp_unslash( $_POST['reason'] ) ) : '';
        $details              = isset( $_POST['details'] ) ? sanitize_textarea_field( wp_unslash( $_POST['details'] ) ) : '';
        $retention_offered    = isset( $_POST['retention_offered'] ) ? (int) wp_unslash( $_POST['retention_offered'] ) : 0;
        $retention_accepted   = isset( $_POST['retention_accepted'] ) ? (int) wp_unslash( $_POST['retention_accepted'] ) : 0;
        $ud                   = function_exists( 'get_userdata' ) ? get_userdata( $user_id ) : null;
        $email                = $ud instanceof \WP_User ? sanitize_email( (string) $ud->user_email ) : '';

        global $wpdb;
        if ( ! isset( $wpdb ) || ! $wpdb ) {
            wp_send_json_error( array( 'code' => 'no_db' ), 500 );
            wp_die();
        }

        $deleted_table = $wpdb->prefix . 'avi_deleted_accounts';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $deleted_table ) ) !== $deleted_table ) {
            wp_send_json_error( array( 'code' => 'no_deleted_accounts_table' ), 500 );
            wp_die();
        }
        if ( $email === '' || ! is_email( $email ) ) {
            wp_send_json_error( array( 'code' => 'invalid_email' ), 400 );
            wp_die();
        }

        $ins = $wpdb->insert(
            $deleted_table,
            array(
                'user_id'             => $user_id,
                'user_type'           => $user_type,
                'email'               => $email,
                'reason'              => $reason,
                'details'             => $details,
                'retention_offered'   => $retention_offered ? 1 : 0,
                'retention_accepted'  => $retention_accepted ? 1 : 0,
            ),
            array( '%d', '%s', '%s', '%s', '%s', '%d', '%d' )
        );
        if ( $ins === false ) {
            wp_send_json_error( array( 'code' => 'insert_failed' ), 500 );
            wp_die();
        }

        $profiles_table = $wpdb->prefix . 'avi_saas_profiles';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) === $profiles_table ) {
            $now = current_time( 'mysql' );
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->query(
                $wpdb->prepare(
                    "UPDATE `{$profiles_table}` SET `account_deleted_at` = %s, `deletion_reason` = %s, `guardian_active` = 0, `radar_plan` = %s, `guardian_scan_enabled` = 0 WHERE `user_id` = %d",
                    $now,
                    $reason,
                    'offert',
                    $user_id
                )
            );
        }

        update_user_meta( $user_id, 'avi_account_deleted', '1' );
        update_user_meta( $user_id, 'avi_unsubscribed', '1' );

        if ( class_exists( '\Avision\Avision_Email_Manager' ) && $ud instanceof \WP_User ) {
            \Avision\Avision_Email_Manager::send_avi_email(
                $user_id,
                \Avision\Avision_Email_Manager::TYPE_UNSUBSCRIBE_CONFIRM,
                array(
                    'recipient_email' => $email,
                    'user_name'       => (string) $ud->display_name,
                    'dashboard_url'   => trailingslashit( home_url( '/premium-dashboard/' ) ),
                    'resubscribe_url' => trailingslashit( home_url( '/dashboard/?resubscribe=1' ) ),
                )
            );
        }

        wp_send_json_success( array( 'redirectUrl' => home_url( '/' ) ) );
        wp_die();
    }

    /**
     * @return array<int, string>
     */
    private static function avision_email_prefs_whitelist_keys() {
        return array(
            'reports',
            'guardian_alerts',
            'levels',
            'infusions',
            'reactivation',
            'promotions',
            'platform_updates',
        );
    }

    /**
     * @return array<string, bool>
     */
    private static function avision_email_prefs_defaults() {
        $out = array();
        foreach ( self::avision_email_prefs_whitelist_keys() as $key ) {
            $out[ $key ] = true;
        }
        return $out;
    }

    /**
     * @param mixed $v Valeur brute.
     */
    private static function avision_coerce_email_pref_bool( $v ) {
        if ( is_bool( $v ) ) {
            return $v;
        }
        if ( is_numeric( $v ) ) {
            return ( (int) $v ) !== 0;
        }
        if ( is_string( $v ) ) {
            $lower = strtolower( trim( $v ) );
            if ( in_array( $lower, array( 'false', '0', 'no', 'off', '' ), true ) ) {
                return false;
            }
            if ( in_array( $lower, array( 'true', '1', 'yes', 'on' ), true ) ) {
                return true;
            }
        }
        return (bool) $v;
    }

    /**
     * Clés autorisées pour la visibilité du rapport client.
     *
     * @return array<int, string>
     */
    private static function avision_report_visibility_whitelist_keys() {
        return array(
            'score-card',
            'kpi-position',
            'kpi-leaders',
            'kpi-reputation',
            'kpi-estimation',
            'kpi-guardian',
            'kpi-keywords',
            'radar-section-root',
            'radar-chart-container',
            'radar-strategic-filters',
            'radar-ask-reviews-btn',
            'guardian-section-root',
            'guardian-sealing-grid',
            'guardian-net-worth-panel',
            'guardian-feed-panel',
            'roadmap-mastery-bar',
            'roadmap-levels-grid',
            'avi-notification-bell',
            'avi-member-area-trigger',
            'avi-chat-widget-root',
        );
    }

    /**
     * Défaut : tout true sauf espace membre et widget chat.
     *
     * @return array<string, bool>
     */
    private static function avision_report_visibility_defaults() {
        $defaults = array();
        foreach ( self::avision_report_visibility_whitelist_keys() as $key ) {
            $defaults[ $key ] = ( $key === 'avi-member-area-trigger' || $key === 'avi-chat-widget-root' ) ? false : true;
        }
        return $defaults;
    }

    /**
     * @param mixed $v Valeur brute (POST / JSON).
     */
    private static function avision_coerce_report_visibility_bool( $v ) {
        if ( is_bool( $v ) ) {
            return $v;
        }
        if ( is_numeric( $v ) ) {
            return ( (int) $v ) !== 0;
        }
        if ( is_string( $v ) ) {
            $lower = strtolower( trim( $v ) );
            if ( in_array( $lower, array( 'false', '0', 'no', 'off', '' ), true ) ) {
                return false;
            }
            if ( in_array( $lower, array( 'true', '1', 'yes', 'on' ), true ) ) {
                return true;
            }
        }
        return (bool) $v;
    }

    /**
     * Fusionne une entrée utilisateur avec les défauts (whitelist uniquement).
     *
     * @param array<string, mixed> $incoming
     * @return array<string, bool>
     */
    private static function avision_merge_report_visibility_input( $incoming ) {
        $out       = self::avision_report_visibility_defaults();
        $whitelist = array_flip( self::avision_report_visibility_whitelist_keys() );
        if ( ! is_array( $incoming ) ) {
            return $out;
        }
        foreach ( $incoming as $k => $v ) {
            if ( ! isset( $whitelist[ $k ] ) ) {
                continue;
            }
            $out[ $k ] = self::avision_coerce_report_visibility_bool( $v );
        }
        return $out;
    }

    /**
     * @param string|null $json_string Valeur colonne report_visibility.
     * @return array<string, bool>
     */
    private static function avision_normalize_report_visibility_row( $json_string ) {
        $incoming = array();
        if ( $json_string !== null && $json_string !== '' ) {
            $decoded = json_decode( $json_string, true );
            if ( is_array( $decoded ) ) {
                $incoming = $decoded;
            }
        }
        return self::avision_merge_report_visibility_input( $incoming );
    }

    /**
     * Vérifie que l’utilisateur courant est admin de l’agence du profil.
     *
     * @param int $profile_id ID avi_saas_profiles.
     */
    private static function avision_ajax_agency_admin_owns_profile( $profile_id ) {
        global $wpdb;
        $profile_id = (int) $profile_id;
        $uid        = (int) get_current_user_id();
        if ( $profile_id <= 0 || $uid <= 0 ) {
            return false;
        }
        if ( ! isset( $wpdb ) || ! $wpdb ) {
            return false;
        }
        $t_prof = $wpdb->prefix . 'avi_saas_profiles';
        $t_ag   = $wpdb->prefix . 'avi_saas_agencies';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t_prof ) ) !== $t_prof ) {
            return false;
        }
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t_ag ) ) !== $t_ag ) {
            return false;
        }
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $agency_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT agency_id FROM `{$t_prof}` WHERE id = %d LIMIT 1", $profile_id ) );
        if ( $agency_id <= 0 ) {
            return false;
        }
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $admin_uid = (int) $wpdb->get_var( $wpdb->prepare( "SELECT admin_user_id FROM `{$t_ag}` WHERE id = %d LIMIT 1", $agency_id ) );
        return $admin_uid === $uid;
    }

    /**
     * POST: visibility (JSON ou tableau), nonce avision_agency_nonce.
     * — profile_id (int) OU profile_ids (JSON array / tableau de profile_id).
     */
    public static function handle_save_report_visibility() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_agency_nonce', 'nonce' );
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }
        $raw_vis = isset( $_POST['visibility'] ) ? wp_unslash( $_POST['visibility'] ) : null;
        $incoming = array();
        if ( is_string( $raw_vis ) ) {
            $decoded = json_decode( $raw_vis, true );
            $incoming = is_array( $decoded ) ? $decoded : array();
        } elseif ( is_array( $raw_vis ) ) {
            $incoming = $raw_vis;
        }
        $merged = self::avision_merge_report_visibility_input( $incoming );
        global $wpdb;
        if ( ! isset( $wpdb ) || ! $wpdb ) {
            wp_send_json_error( array( 'code' => 'no_db' ), 500 );
            wp_die();
        }
        $tbl = $wpdb->prefix . 'avi_saas_profiles';
        $json_out = wp_json_encode( $merged );

        $profile_ids_bulk = array();
        if ( isset( $_POST['profile_ids'] ) ) {
            $raw_ids = wp_unslash( $_POST['profile_ids'] );
            if ( is_string( $raw_ids ) ) {
                $decoded_ids = json_decode( $raw_ids, true );
                if ( is_array( $decoded_ids ) ) {
                    $profile_ids_bulk = array_map( 'intval', $decoded_ids );
                }
            } elseif ( is_array( $raw_ids ) ) {
                $profile_ids_bulk = array_map( 'intval', $raw_ids );
            }
        }
        $profile_ids_bulk = array_values( array_unique( array_filter( $profile_ids_bulk ) ) );

        if ( ! empty( $profile_ids_bulk ) ) {
            $saved = 0;
            foreach ( $profile_ids_bulk as $pid ) {
                $pid = (int) $pid;
                if ( $pid <= 0 ) {
                    continue;
                }
                if ( ! self::avision_ajax_agency_admin_owns_profile( $pid ) ) {
                    continue;
                }
                $upd = $wpdb->update(
                    $tbl,
                    array( 'report_visibility' => $json_out ),
                    array( 'id' => $pid ),
                    array( '%s' ),
                    array( '%d' )
                );
                if ( $upd !== false ) {
                    ++$saved;
                }
            }
            if ( $saved === 0 ) {
                wp_send_json_error( array( 'code' => 'no_valid_targets' ), 400 );
                wp_die();
            }
            wp_send_json_success( array( 'saved' => $saved ) );
            wp_die();
        }

        $profile_id = isset( $_POST['profile_id'] ) ? (int) wp_unslash( $_POST['profile_id'] ) : 0;
        if ( $profile_id <= 0 ) {
            wp_send_json_error( array( 'code' => 'invalid_profile_id' ), 400 );
            wp_die();
        }
        if ( ! self::avision_ajax_agency_admin_owns_profile( $profile_id ) ) {
            wp_send_json_error( array( 'code' => 'forbidden' ), 403 );
            wp_die();
        }
        $updated = $wpdb->update(
            $tbl,
            array( 'report_visibility' => $json_out ),
            array( 'id' => $profile_id ),
            array( '%s' ),
            array( '%d' )
        );
        if ( $updated === false ) {
            wp_send_json_error( array( 'code' => 'db_error' ), 500 );
            wp_die();
        }
        wp_send_json_success( array( 'saved' => true ) );
        wp_die();
    }

    /**
     * GET/POST: profile_id, nonce avision_agency_nonce.
     */
    public static function handle_get_report_visibility() {
        if ( ! function_exists( 'check_ajax_referer' ) || ! function_exists( 'is_user_logged_in' ) ) {
            wp_send_json_error( array( 'code' => 'missing_wp_functions' ), 500 );
            wp_die();
        }
        check_ajax_referer( 'avision_agency_nonce', 'nonce' );
        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'code' => 'not_logged_in' ), 401 );
            wp_die();
        }
        $profile_id = 0;
        if ( isset( $_REQUEST['profile_id'] ) ) {
            $profile_id = (int) wp_unslash( $_REQUEST['profile_id'] );
        }
        if ( $profile_id <= 0 ) {
            wp_send_json_error( array( 'code' => 'invalid_profile_id' ), 400 );
            wp_die();
        }
        if ( ! self::avision_ajax_agency_admin_owns_profile( $profile_id ) ) {
            wp_send_json_error( array( 'code' => 'forbidden' ), 403 );
            wp_die();
        }
        global $wpdb;
        if ( ! isset( $wpdb ) || ! $wpdb ) {
            wp_send_json_error( array( 'code' => 'no_db' ), 500 );
            wp_die();
        }
        $tbl = $wpdb->prefix . 'avi_saas_profiles';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT report_visibility FROM `{$tbl}` WHERE id = %d LIMIT 1", $profile_id ) );
        $json_str = ( $row && isset( $row->report_visibility ) ) ? $row->report_visibility : null;
        $visibility = self::avision_normalize_report_visibility_row( $json_str );
        wp_send_json_success( array( 'visibility' => $visibility ) );
        wp_die();
    }

    /**
     * Onboarding B2B — sauvegarde nom agence + URL OAuth Google (fetch admin-ajax).
     */
    public static function handle_btob_save_agency() {
        $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
        if ( ! wp_verify_nonce( $nonce, 'avision_btob_onboarding_ajax' ) ) {
            // Nonce guest fallback : formulaire rendu avant auto-login avi_alt
            $avi_alt_fb = isset( $_POST['avi_alt'] ) ? sanitize_text_field( wp_unslash( $_POST['avi_alt'] ) ) : '';
            if ( ! ( strlen( $avi_alt_fb ) >= 16 && get_transient( 'avi_alt_' . $avi_alt_fb ) ) ) {
                wp_send_json_error( array( 'message' => 'Security check failed.' ), 403 );
                wp_die();
            }
        }

        $avi_alt = isset( $_POST['avi_alt'] ) ? sanitize_text_field( wp_unslash( $_POST['avi_alt'] ) ) : '';
        if ( ! is_user_logged_in() && strlen( $avi_alt ) >= 16 ) {
            $alt_data = get_transient( 'avi_alt_' . $avi_alt );
            $alt_uid  = 0;
            if ( is_array( $alt_data ) && ! empty( $alt_data['uid'] ) ) {
                $alt_uid = (int) $alt_data['uid'];
            } elseif ( is_int( $alt_data ) && $alt_data > 0 ) {
                $alt_uid = (int) $alt_data;
            }
            if ( $alt_uid > 0 ) {
                wp_set_current_user( $alt_uid );
                wp_set_auth_cookie( $alt_uid, true, is_ssl() );
            }
        }

        if ( ! is_user_logged_in() ) {
            wp_send_json_error( array( 'message' => 'Login required.' ), 401 );
        }

        $uid  = (int) get_current_user_id();
        $user = function_exists( 'get_userdata' ) ? get_userdata( $uid ) : null;
        if ( ! $user || ! $user->exists() ) {
            wp_send_json_error( array( 'message' => 'Invalid user.' ), 400 );
        }
        $is_agency = in_array( 'avi_agency', (array) $user->roles, true );
        $is_admin  = function_exists( 'user_can' ) && user_can( $user, 'manage_options' );
        if ( ! $is_agency && ! $is_admin ) {
            wp_send_json_error( array( 'message' => 'Forbidden.' ), 403 );
        }

        $name = isset( $_POST['avi_btob_agency_name'] ) ? sanitize_text_field( wp_unslash( $_POST['avi_btob_agency_name'] ) ) : '';
        if ( $name === '' ) {
            wp_send_json_error( array( 'message' => 'Agency name required.' ), 400 );
        }

        if ( function_exists( 'avision_btob_onboarding_persist_agency_name' ) ) {
            $persist = avision_btob_onboarding_persist_agency_name( $uid, $name );
            if ( is_wp_error( $persist ) ) {
                wp_send_json_error( array( 'message' => $persist->get_error_message() ), 400 );
            }
        }

        $oauth_url = '';
        if ( function_exists( 'avision_btob_onboarding_resolve_google_oauth_url' ) ) {
            $oauth_url = avision_btob_onboarding_resolve_google_oauth_url( $uid );
        }
        if ( ! is_string( $oauth_url ) || $oauth_url === '' ) {
            wp_send_json_error( array( 'message' => 'OAuth configuration missing.' ), 500 );
        }

        wp_send_json_success( array( 'redirect' => $oauth_url ) );
    }

    /**
     * Candidature nonprofit publique (formulaire /nonprofits/).
     *
     * @return void
     */
    public static function handle_nonprofit_apply() {
        check_ajax_referer( 'avision_nonprofit_apply', 'nonce' );

        if ( ! function_exists( 'avi_nonprofit_handle_application' ) ) {
            wp_send_json_error( array( 'message' => 'Service unavailable.' ) );
            return;
        }

        $org_name      = isset( $_POST['org_name'] ) ? sanitize_text_field( wp_unslash( $_POST['org_name'] ) ) : '';
        $country       = isset( $_POST['country'] ) ? sanitize_text_field( wp_unslash( $_POST['country'] ) ) : '';
        $other_country = isset( $_POST['other_country'] ) ? sanitize_text_field( wp_unslash( $_POST['other_country'] ) ) : '';

        if ( $country === 'OTHER' && $other_country !== '' ) {
            update_option(
                'avi_np_other_country_' . time(),
                sanitize_text_field( $other_country ),
                false
            );
            wp_mail(
                'toniosbh@gmail.com',
                '[Avision] Nouveau pays demandé — nonprofit',
                "Un pays non listé a été soumis :\n\n"
                . 'Pays : ' . $other_country . "\n"
                . 'Organisation : ' . $org_name . "\n\n"
                . 'À ajouter dans le formulaire si pertinent.',
                array( 'From: Avision <no-reply@avision.org>' )
            );
        }

        $result = avi_nonprofit_handle_application(
            array(
                'org_name'   => $org_name,
                'country'    => $country,
                'org_number' => isset( $_POST['org_number'] ) ? sanitize_text_field( wp_unslash( $_POST['org_number'] ) ) : '',
                'gbp_name'   => isset( $_POST['gbp_name'] ) ? sanitize_text_field( wp_unslash( $_POST['gbp_name'] ) ) : '',
                'website'    => isset( $_POST['website'] ) ? esc_url_raw( wp_unslash( $_POST['website'] ) ) : '',
                'email'      => isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '',
                'lang'       => isset( $_POST['lang'] ) ? sanitize_text_field( wp_unslash( $_POST['lang'] ) ) : 'en',
            )
        );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ) );
            return;
        }

        wp_send_json_success( array( 'np_id' => (int) $result['np_id'] ) );
    }

    /**
     * Soumission preuve de partage réseau (nonprofit vérifié).
     *
     * @return void
     */
    public static function handle_np_submit_share() {
        $user_id = get_current_user_id();
        if ( ! $user_id ) {
            wp_send_json_error( array( 'message' => 'Authentication required' ) );
            return;
        }

        if ( ! check_ajax_referer( 'avision_np_share_nonce', 'nonce', false ) ) {
            wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
            return;
        }

        global $wpdb;

        $np = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}avi_nonprofits
                 WHERE user_id = %d
                   AND verification_status = 'verified'
                 LIMIT 1",
                $user_id
            ),
            ARRAY_A
        );

        if ( ! $np ) {
            wp_send_json_error( array( 'message' => 'No verified nonprofit found' ) );
            return;
        }

        if ( ! empty( $np['last_share_proof_at'] ) ) {
            $last         = strtotime( $np['last_share_proof_at'] );
            $months_since = ( time() - $last ) / ( 30 * 86400 );
            if ( $months_since < 6 ) {
                $next = date( 'd/m/Y', $last + ( 6 * 30 * 86400 ) );
                wp_send_json_error(
                    array(
                        'message'   => 'Next proof accepted from ' . $next,
                        'next_date' => $next,
                    )
                );
                return;
            }
        }

        $network  = sanitize_text_field( $_POST['network'] ?? '' );
        $post_url = esc_url_raw( $_POST['post_url'] ?? '' );
        $comment  = sanitize_textarea_field( $_POST['comment'] ?? '' );

        $allowed_networks = array(
            'facebook',
            'instagram',
            'linkedin',
            'twitter',
            'other',
        );
        if ( ! in_array( $network, $allowed_networks, true ) ) {
            wp_send_json_error( array( 'message' => 'Invalid network' ) );
            return;
        }

        if ( empty( $post_url ) ) {
            wp_send_json_error( array( 'message' => 'Post URL required' ) );
            return;
        }

        $pending_key = 'avi_np_share_pending_' . (int) $np['id'];
        update_option(
            $pending_key,
            array(
                'np_id'        => (int) $np['id'],
                'org_name'     => $np['org_name'],
                'user_id'      => $user_id,
                'network'      => $network,
                'post_url'     => $post_url,
                'comment'      => $comment,
                'submitted_at' => current_time( 'mysql' ),
            ),
            false
        );

        $admin_url = admin_url( 'admin.php?page=avi-nonprofits' );
        wp_mail(
            array(
                'contact@avision.org',
                'toniosbh@gmail.com',
            ),
            '[Avision] Nouvelle preuve partage — ' . $np['org_name'],
            "Organisation : " . $np['org_name'] . "\n"
            . "Réseau : " . $network . "\n"
            . "URL du post : " . $post_url . "\n"
            . "Commentaire : " . $comment . "\n\n"
            . "Valider dans WP-Admin :\n"
            . $admin_url,
            array( 'From: Avision <no-reply@avision.org>' )
        );

        $wpdb->update(
            $wpdb->prefix . 'avi_nonprofits',
            array( 'last_activity_at' => current_time( 'mysql' ) ),
            array( 'id' => (int) $np['id'] ),
            array( '%s' ),
            array( '%d' )
        );

        wp_send_json_success(
            array(
                'message' => 'Proof submitted — pending admin validation.',
            )
        );
    }
}

Avision_Ajax_Router::init();
