<?php
/**
 * Plugin Name: Avision Dashboard (SaaS V9)
 * Plugin URI: https://avision.io
 * Description: Local SEO Audit Tool & Dashboard - Gamified progression system
 * Version: 9.0.2
 * Author: Avision
 * Text Domain: avision-dashboard
 * Domain Path: /languages
 */

// WordPress mode only

// Capture anticipée du body Stripe AVANT que WordPress lise php://input (une seule lecture possible).
if ( function_exists( 'add_action' ) ) {
	add_action(
		'plugins_loaded',
		static function () {
			// i18n (WP) — charger les traductions du plugin (Text Domain: avision-dashboard).
			if ( function_exists( 'load_plugin_textdomain' ) ) {
				load_plugin_textdomain( 'avision-dashboard', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
			}

			if ( ! isset( $_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD'] ) ) {
				return;
			}
			$uri    = (string) wp_unslash( $_SERVER['REQUEST_URI'] );
			$method = strtoupper( (string) ( $_SERVER['REQUEST_METHOD'] ?? '' ) );
			if ( strpos( $uri, 'stripe-webhook' ) === false || 'POST' !== $method ) {
				return;
			}
			if ( ! function_exists( 'file_get_contents' ) || defined( 'AVI_STRIPE_RAW_BODY' ) ) {
				return;
			}
			$raw = file_get_contents( 'php://input' );
			if ( is_string( $raw ) && $raw !== '' ) {
				define( 'AVI_STRIPE_RAW_BODY', $raw );
			}
		},
		1
	);
}

if ( ! defined( 'AVISION_VERSION' ) ) {
	define( 'AVISION_VERSION', '9.0.3' );
}

// SPRINT 9 — S9-D : Heatmap (schéma anticipatoire uniquement, tables vides post-install)
if ( ! defined( 'AVI_HEATMAP_SCHEMA_VERSION' ) ) {
	define( 'AVI_HEATMAP_SCHEMA_VERSION', '1.0' );
}
if ( ! defined( 'AVI_HEATMAP_GRID_DEFAULT' ) ) {
	define( 'AVI_HEATMAP_GRID_DEFAULT', 7 );
}
if ( ! defined( 'AVI_HEATMAP_RADIUS_DEFAULT' ) ) {
	define( 'AVI_HEATMAP_RADIUS_DEFAULT', 5.0 );
}

// SPRINT 10 — S10-B : produits Stripe AVI (metadata avi_product + checkout).
if ( ! defined( 'AVI_STRIPE_PRODUCT_RADAR_HEBDO' ) ) {
	define( 'AVI_STRIPE_PRODUCT_RADAR_HEBDO', 'radar_hebdo' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_RADAR_ELITE' ) ) {
	define( 'AVI_STRIPE_PRODUCT_RADAR_ELITE', 'radar_elite' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_GUARDIAN' ) ) {
	define( 'AVI_STRIPE_PRODUCT_GUARDIAN', 'guardian' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_BUNDLE_STARTER' ) ) {
	define( 'AVI_STRIPE_PRODUCT_BUNDLE_STARTER', 'bundle_starter_pro' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_BUNDLE_ELITE' ) ) {
	define( 'AVI_STRIPE_PRODUCT_BUNDLE_ELITE', 'bundle_elite_pro' );
}
// S11 — Wholesale (9 Price IDs, tiers 1–10 / 11–50 / 51+ fiches).
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_GUARDIAN_T1' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_GUARDIAN_T1', 'ws_guardian_t1' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_GUARDIAN_T2' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_GUARDIAN_T2', 'ws_guardian_t2' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_GUARDIAN_T3' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_GUARDIAN_T3', 'ws_guardian_t3' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_HEBDO_T1' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_HEBDO_T1', 'ws_hebdo_t1' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_HEBDO_T2' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_HEBDO_T2', 'ws_hebdo_t2' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_HEBDO_T3' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_HEBDO_T3', 'ws_hebdo_t3' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_ELITE_T1' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_ELITE_T1', 'ws_elite_t1' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_ELITE_T2' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_ELITE_T2', 'ws_elite_t2' );
}
if ( ! defined( 'AVI_STRIPE_PRODUCT_WS_ELITE_T3' ) ) {
	define( 'AVI_STRIPE_PRODUCT_WS_ELITE_T3', 'ws_elite_t3' );
}
if ( ! defined( 'AVI_GUARDIAN_PILLARS_REQUIRED' ) ) {
	define( 'AVI_GUARDIAN_PILLARS_REQUIRED', 3 );
}
if ( ! defined( 'AVI_GUARDIAN_UNLOCK_LEVEL' ) ) {
	define( 'AVI_GUARDIAN_UNLOCK_LEVEL', 4 );
}
if ( ! defined( 'AVI_S10B_STRIPE_VERSION' ) ) {
	define( 'AVI_S10B_STRIPE_VERSION', '1.0' );
}
if ( ! defined( 'AVI_PRICE_RADAR_HEBDO_EUR' ) ) {
	define( 'AVI_PRICE_RADAR_HEBDO_EUR', '7€' );
}
if ( ! defined( 'AVI_PRICE_RADAR_HEBDO_USD' ) ) {
	define( 'AVI_PRICE_RADAR_HEBDO_USD', '$7' );
}
if ( ! defined( 'AVI_PRICE_RADAR_ELITE_EUR' ) ) {
	define( 'AVI_PRICE_RADAR_ELITE_EUR', '14€' );
}
if ( ! defined( 'AVI_PRICE_RADAR_ELITE_USD' ) ) {
	define( 'AVI_PRICE_RADAR_ELITE_USD', '$14' );
}
if ( ! defined( 'AVI_PRICE_GUARDIAN_EUR' ) ) {
	define( 'AVI_PRICE_GUARDIAN_EUR', '29€' );
}
if ( ! defined( 'AVI_PRICE_GUARDIAN_USD' ) ) {
	define( 'AVI_PRICE_GUARDIAN_USD', '$29' );
}
if ( ! defined( 'AVI_PRICE_BUNDLE_STARTER_EUR' ) ) {
	define( 'AVI_PRICE_BUNDLE_STARTER_EUR', '35€' );
}
if ( ! defined( 'AVI_PRICE_BUNDLE_STARTER_USD' ) ) {
	define( 'AVI_PRICE_BUNDLE_STARTER_USD', '$35' );
}
if ( ! defined( 'AVI_PRICE_BUNDLE_ELITE_EUR' ) ) {
	define( 'AVI_PRICE_BUNDLE_ELITE_EUR', '39€' );
}
if ( ! defined( 'AVI_PRICE_BUNDLE_ELITE_USD' ) ) {
	define( 'AVI_PRICE_BUNDLE_ELITE_USD', '$39' );
}

// SPRINT 10 — S10-D : UI Agency dashboard (cache score + auto-refresh alertes).
if ( ! defined( 'AVI_S10D_UI_VERSION' ) ) {
	define( 'AVI_S10D_UI_VERSION', '1.0' );
}
if ( ! defined( 'AVI_S10D_ALERTS_REFRESH_INTERVAL_S' ) ) {
	define( 'AVI_S10D_ALERTS_REFRESH_INTERVAL_S', 300 );
}

/** Table pré-inscription B2C/B2B (nom complet MySQL, sans dépendre du préfixe WP). */
if ( ! defined( 'AVISION_PRE_INSCRIPTION_TABLE' ) ) {
	define( 'AVISION_PRE_INSCRIPTION_TABLE', 'wor1515_avi_pre_inscription' );
}

/**
 * Réparation one-shot : assure la colonne google_email sur wor1515_avi_saas_profiles (affichage emails / OAuth).
 *
 * @return void
 */
function avision_repair_database_structure() {
	if ( function_exists( 'get_option' ) && get_option( 'avision_repair_google_email_column_v1', '' ) === 'done' ) {
		return;
	}
	global $wpdb;
	if ( ! isset( $wpdb ) || ! $wpdb ) {
		return;
	}
	$table = 'wor1515_avi_saas_profiles';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
		return;
	}
	$col = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$table}` LIKE %s", 'google_email' ) );
	if ( $col === 'google_email' ) {
		update_option( 'avision_repair_google_email_column_v1', 'done', true );
		return;
	}
	// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL : nom de table fixe homologué.
	$wpdb->query( "ALTER TABLE `{$table}` ADD COLUMN google_email VARCHAR(255) DEFAULT NULL AFTER email" );
	if ( $wpdb->last_error !== '' ) {
		if ( function_exists( 'error_log' ) ) {
			error_log( '[AVI DB REPAIR] Échec ALTER google_email : ' . $wpdb->last_error );
		}
		return;
	}
	update_option( 'avision_repair_google_email_column_v1', 'done', true );
}

add_action( 'plugins_loaded', 'avision_repair_database_structure', 1 );

/** User meta : premier arrivage cockpit après checkout Stripe (survit aux redirections sans &first_login=1). */
if ( ! defined( 'AVISION_PREMIUM_FIRST_LOGIN_WELCOME_META' ) ) {
	define( 'AVISION_PREMIUM_FIRST_LOGIN_WELCOME_META', 'avision_premium_first_login_welcome' );
}

/**
 * Marque l’utilisateur WP pour affichage du welcome onboarding (perdu si l’URL n’a plus first_login).
 *
 * @param int $wp_user_id ID utilisateur WordPress.
 * @return void
 */
function avision_flag_premium_first_login_welcome( $wp_user_id ) {
	$uid = (int) $wp_user_id;
	if ( $uid <= 0 || ! function_exists( 'update_user_meta' ) ) {
		return;
	}
	update_user_meta( $uid, AVISION_PREMIUM_FIRST_LOGIN_WELCOME_META, '1' );
}

/**
 * Indique si le JS cockpit doit traiter un premier login (query OU meta persistante).
 *
 * @param int $user_id ID utilisateur courant.
 * @return bool
 */
function avision_premium_is_first_login_for_js( $user_id ) {
	if ( ! function_exists( 'get_user_meta' ) ) {
		return false;
	}
	if ( isset( $_GET['first_login'] ) && (string) wp_unslash( $_GET['first_login'] ) === '1' ) {
		return true;
	}
	$uid = (int) $user_id;
	if ( $uid <= 0 ) {
		return false;
	}
	return get_user_meta( $uid, AVISION_PREMIUM_FIRST_LOGIN_WELCOME_META, true ) === '1';
}

/**
 * REST : supprime le flag premier login (après fermeture du modal côté JS).
 *
 * @param \WP_REST_Request $request Requête.
 * @return \WP_REST_Response|\WP_Error
 */
function avision_rest_consume_first_login_welcome( \WP_REST_Request $request ) {
	$uid = function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0;
	if ( $uid <= 0 ) {
		return new \WP_Error( 'not_logged_in', 'Authentication required', array( 'status' => 401 ) );
	}
	if ( function_exists( 'delete_user_meta' ) ) {
		delete_user_meta( $uid, AVISION_PREMIUM_FIRST_LOGIN_WELCOME_META );
	}
	return new \WP_REST_Response( array( 'success' => true ), 200 );
}

/**
 * Euréka / onboarding : réaffiche le welcome tour après premier paiement (?status=success&first_login=1 sur Premium).
 *
 * @return void
 */
function avision_welcome_modal() {
	if ( is_admin() || ! function_exists( 'is_page' ) || ! is_page( 'premium-dashboard' ) ) {
		return;
	}
	$uid = function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0;
	$from_get_fl = ! empty( $_GET['first_login'] ) && (string) wp_unslash( $_GET['first_login'] ) === '1';
	$from_meta   = function_exists( 'avision_premium_is_first_login_for_js' ) && $uid > 0 && get_user_meta( $uid, AVISION_PREMIUM_FIRST_LOGIN_WELCOME_META, true ) === '1';

	$status_ok = false;
	if ( ! empty( $_GET['status'] ) && function_exists( 'sanitize_text_field' ) && function_exists( 'wp_unslash' ) ) {
		$status_ok = sanitize_text_field( wp_unslash( (string) $_GET['status'] ) ) === 'success';
	}

	// first_login en query + paiement OK, ou meta serveur (OAuth / redirections intermédiaires).
	$trigger = ( $from_get_fl && $status_ok ) || $from_meta;

	if ( ! $trigger ) {
		return;
	}
	add_action(
		'wp_footer',
		static function () {
			?>
<script>
(function(){
	try { if (typeof localStorage !== 'undefined') localStorage.removeItem('avi_welcome_onboarding_seen'); } catch(e) {}
	function fire(){
		if (typeof tryShowWelcomeOnboarding === 'function') tryShowWelcomeOnboarding();
		else if (window.AvisionDashboard && typeof window.AvisionDashboard.tryShowWelcomeOnboarding === 'function') window.AvisionDashboard.tryShowWelcomeOnboarding();
	}
	setTimeout(fire, 1200);
	setTimeout(fire, 4000);
})();
</script>
			<?php
		},
		99999
	);
}
add_action( 'template_redirect', 'avision_welcome_modal', 5 );

/**
 * Table profils SaaS (nom MySQL complet, ex. wor1515_avi_saas_profiles).
 * Optionnel : define( 'AVISION_SAAS_PROFILES_TABLE', 'wor1515_avi_saas_profiles' ); dans wp-config.php
 */

/**
 * Table identité mw_user (nom MySQL complet si préfixe / nom custom).
 * Optionnel : define( 'AVISION_MW_USER_TABLE', 'mw_user' ); ou wor1515_mw_user, etc.
 */

/**
 * Stripe — configuration (tests / prod) sans interface d’admin obligatoire.
 *
 * wp-config.php (recommandé) :
 *   define( 'AVISION_STRIPE_SECRET_KEY', 'sk_test_...' );
 *   define( 'AVISION_STRIPE_PRICE_ID', 'price_...' ); // optionnel : sinon prix dynamique (unit_amount)
 * Ou options WordPress : avision_stripe_secret_key, avision_stripe_price_id
 *
 * Clés test : sk_test_… / pk_test_… utilisent la même API api.stripe.com ; le Checkout hébergé
 * fonctionne dès que la clé secrète test est configurée (le front n’a pas besoin de pk_ pour cette route).
 */

/**
 * Vérifie si la clé secrète Stripe est renseignée (appel API Checkout possible).
 *
 * @return bool
 */
function avision_stripe_secret_key_configured() {
	$const = ( defined( 'AVISION_STRIPE_SECRET_KEY' ) && is_string( AVISION_STRIPE_SECRET_KEY ) ) ? trim( AVISION_STRIPE_SECRET_KEY ) : '';
	$opt   = function_exists( 'get_option' ) ? get_option( 'avision_stripe_secret_key', '' ) : '';
	$opt   = is_string( $opt ) ? trim( $opt ) : '';
	$key   = $const !== '' ? $const : $opt;
	$ok    = is_string( $key ) && trim( $key ) !== '';
	if ( ! $ok ) {
		error_log( "[AVI STRIPE] Erreur : Clé secrète non configurée." );
	}
	return $ok;
}

/**
 * Indique si la clé secrète configurée est une clé de test Stripe.
 *
 * @return bool
 */
function avision_stripe_is_test_mode() {
	$key = defined( 'AVISION_STRIPE_SECRET_KEY' ) && AVISION_STRIPE_SECRET_KEY ? AVISION_STRIPE_SECRET_KEY : get_option( 'avision_stripe_secret_key', '' );
	return is_string( $key ) && strpos( trim( $key ), 'sk_test_' ) === 0;
}

/**
 * Expose au JS un flag Stripe (demo-dashboard).
 * Le thème enqueuse `demo-dashboard-js`; on localise ici pour éviter les divergences.
 */
function avision_localize_demo_dashboard_stripe_config() {
	if ( ! function_exists( 'wp_script_is' ) || ! function_exists( 'wp_localize_script' ) ) {
		return;
	}
	if ( ! wp_script_is( 'demo-dashboard-js', 'enqueued' ) && ! wp_script_is( 'demo-dashboard-js', 'registered' ) ) {
		return;
	}
	$configured = function_exists( 'avision_stripe_secret_key_configured' ) ? avision_stripe_secret_key_configured() : false;
	wp_localize_script(
		'demo-dashboard-js',
		'AvisionStripeConfig',
		array(
			'stripeConfigured' => (bool) $configured,
			/** X-WP-Nonce pour POST /wp-json/avision/v1/* (ne pas confondre avec nonce AJAX). */
			'rest_nonce'       => wp_create_nonce( 'wp_rest' ),
		)
	);
}
add_action( 'wp_enqueue_scripts', 'avision_localize_demo_dashboard_stripe_config', 30 );

/**
 * Renouvelle le nonce AJAX « avision_nonce » à chaque rendu de pied de page (évite un nonce périmé après rotation des SALT
 * ou cache HTML) : surcharge avisionSettings / avisionPremium et data-nonce sur #avision-dashboard-root.
 */
function avision_print_fresh_chat_nonce_script() {
	if ( ! function_exists( 'is_user_logged_in' ) || ! is_user_logged_in() ) {
		return;
	}
	$nonce = wp_create_nonce( 'avision_nonce' );
	$ajax  = admin_url( 'admin-ajax.php' );
	?>
<script>
(function(){
	var n = <?php echo wp_json_encode( $nonce ); ?>;
	var u = <?php echo wp_json_encode( $ajax ); ?>;
	if (typeof avisionSettings === 'object' && avisionSettings) {
		avisionSettings.nonce = n;
		if (u) { avisionSettings.ajaxUrl = u; }
	}
	if (typeof avisionPremium === 'object' && avisionPremium) {
		avisionPremium.nonce = n;
		if (u && (!avisionPremium.ajax_url || avisionPremium.ajax_url === '')) {
			avisionPremium.ajax_url = u;
		}
	}
	var root = document.getElementById('avision-dashboard-root');
	if (root) { root.setAttribute('data-nonce', n); }
})();
</script>
	<?php
}
add_action( 'wp_footer', 'avision_print_fresh_chat_nonce_script', 99 );

/**
 * Pages / templates où afficher la modale « préférences e-mail » (S11).
 *
 * @return bool
 */
function avi_dashboard_should_show_unsubscribe_modal(): bool {
	if ( ! function_exists( 'is_page' ) ) {
		return false;
	}
	$slugs = array( 'premium-dashboard', 'agency-dashboard', 'mon-tableau-de-bord' );
	foreach ( $slugs as $slug ) {
		if ( is_page( $slug ) ) {
			return true;
		}
	}
	if ( function_exists( 'is_page_template' ) ) {
		if ( is_page_template( 'page-premium-dashboard.php' ) || is_page_template( 'page-agency-dashboard.php' ) ) {
			return true;
		}
	}
	return false;
}

/**
 * CSS + JS modale désinscription (dashboards premium / agence).
 *
 * @return void
 */
function avi_enqueue_unsubscribe_modal_assets(): void {
	if ( ! avi_dashboard_should_show_unsubscribe_modal() || ! function_exists( 'wp_enqueue_style' ) ) {
		return;
	}
	if ( ! defined( 'AVISION_URL' ) || ! defined( 'AVISION_VERSION' ) ) {
		return;
	}
	wp_enqueue_style(
		'avi-unsubscribe-modal',
		AVISION_URL . 'assets/css/avi-unsubscribe-modal.css',
		array(),
		AVISION_VERSION
	);
	wp_enqueue_script(
		'avi-unsubscribe-modal',
		AVISION_URL . 'assets/js/avi-unsubscribe-modal.js',
		array(),
		AVISION_VERSION,
		true
	);
	if ( function_exists( 'wp_localize_script' ) && function_exists( 'wp_create_nonce' ) && function_exists( 'admin_url' ) ) {
		wp_localize_script(
			'avi-unsubscribe-modal',
			'aviUnsubscribeModal',
			array(
				'ajaxurl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( 'avision_nonce' ),
				'i18n'    => array(
					'selectReason' => __( 'Please select a reason.', 'avision-dashboard' ),
					'success'      => __( 'Your preferences have been updated.', 'avision-dashboard' ),
					'errorGeneric' => __( 'An error occurred.', 'avision-dashboard' ),
					'network'      => __( 'Network error. Please try again.', 'avision-dashboard' ),
				),
			)
		);
	}
}
add_action( 'wp_enqueue_scripts', 'avi_enqueue_unsubscribe_modal_assets', 99 );

/**
 * Modale HTML désinscription / préférences e-mail (une seule fois en pied de page).
 *
 * @return void
 */
function avi_render_unsubscribe_modal(): void {
	if ( ! avi_dashboard_should_show_unsubscribe_modal() ) {
		return;
	}
	$reasons_btoc = array(
		'not_using'  => __( 'I no longer use AVI', 'avision-dashboard' ),
		'too_many'   => __( 'Too many emails', 'avision-dashboard' ),
		'other_tool' => __( 'I prefer another tool', 'avision-dashboard' ),
		'other'      => __( 'Other', 'avision-dashboard' ),
	);
	$reasons_btob = array(
		'clients_not_interested' => __( 'My clients are no longer interested', 'avision-dashboard' ),
		'price_too_high'         => __( 'Pricing too high', 'avision-dashboard' ),
		'interface_difficult'    => __( 'Interface too complex', 'avision-dashboard' ),
		'other'                  => __( 'Other', 'avision-dashboard' ),
	);
	?>
	<div id="avi-unsubscribe-modal"
		class="avi-modal-overlay"
		style="display:none"
		role="dialog"
		aria-modal="true"
		aria-labelledby="avi-unsub-title">
		<div class="avi-modal-card" role="document" onclick="event.stopPropagation();">
			<button type="button" class="avi-modal-close"
				onclick="if (typeof aviCloseUnsubscribeModal === 'function') { aviCloseUnsubscribeModal(); }"
				aria-label="<?php echo esc_attr__( 'Close', 'avision-dashboard' ); ?>">✕</button>

			<h2 id="avi-unsub-title" class="avi-modal-title">
				<?php echo esc_html__( 'Manage your email preferences', 'avision-dashboard' ); ?>
			</h2>
			<p class="avi-modal-subtitle">
				<?php echo esc_html__( 'Help us improve — tell us why you\'re adjusting your preferences.', 'avision-dashboard' ); ?>
			</p>

			<div id="avi-unsub-reasons-btoc" class="avi-unsub-reasons" style="display:none">
				<?php foreach ( $reasons_btoc as $val => $label ) : ?>
				<label class="avi-unsub-option">
					<input type="radio" name="avi_unsub_reason" value="<?php echo esc_attr( $val ); ?>">
					<span><?php echo esc_html( $label ); ?></span>
				</label>
				<?php endforeach; ?>
			</div>

			<div id="avi-unsub-reasons-btob" class="avi-unsub-reasons" style="display:none">
				<?php foreach ( $reasons_btob as $val => $label ) : ?>
				<label class="avi-unsub-option">
					<input type="radio" name="avi_unsub_reason" value="<?php echo esc_attr( $val ); ?>">
					<span><?php echo esc_html( $label ); ?></span>
				</label>
				<?php endforeach; ?>
			</div>

			<textarea id="avi-unsub-custom"
				class="avi-unsub-textarea"
				placeholder="<?php echo esc_attr__( 'Tell us more (optional)...', 'avision-dashboard' ); ?>"
				rows="3"
				style="display:none"></textarea>

			<div id="avi-unsub-msg" class="avi-unsub-message" style="display:none"></div>

			<div class="avi-modal-actions">
				<button type="button" class="avi-btn-unsub-confirm" onclick="if (typeof aviSubmitUnsubscribe === 'function') { aviSubmitUnsubscribe(); }">
					<?php echo esc_html__( 'Confirm unsubscription', 'avision-dashboard' ); ?>
				</button>
				<button type="button" class="avi-btn-unsub-cancel" onclick="if (typeof aviCloseUnsubscribeModal === 'function') { aviCloseUnsubscribeModal(); }">
					<?php echo esc_html__( 'Keep my emails', 'avision-dashboard' ); ?>
				</button>
			</div>
		</div>
	</div>
	<?php
}
add_action( 'wp_footer', 'avi_render_unsubscribe_modal', 100 );

/**
 * Price ID Stripe (Catalogue) — si vide, Checkout utilise price_data + unit_amount.
 *
 * @return string
 */
function avision_stripe_get_price_id() {
	if ( defined( 'AVISION_STRIPE_PRICE_ID' ) && is_string( AVISION_STRIPE_PRICE_ID ) && AVISION_STRIPE_PRICE_ID !== '' ) {
		return AVISION_STRIPE_PRICE_ID;
	}
	$opt = get_option( 'avision_stripe_price_id', '' );
	return is_string( $opt ) && $opt !== '' ? $opt : '';
}

/**
 * Retourne la config d'upgrade in-app (type, montant, mode, labels).
 *
 * @param string $upgrade_type
 * @return array|null
 */
function avision_get_upgrade_checkout_config( $upgrade_type ) {
	$type = is_string( $upgrade_type ) ? strtolower( trim( $upgrade_type ) ) : '';
	$map  = [
		'guardian' => [
			'price_constant' => 'AVISION_STRIPE_PRICE_ID_GUARDIAN',
			'price_option'   => 'avision_stripe_price_id_guardian',
			'unit_amount'    => 2900,
			'mode'           => 'subscription',
			'name'           => 'AVI — Mode Gardien',
			'description'    => 'Protection continue de la fiche Google Business Profile',
		],
		'keyword_75' => [
			'price_constant' => 'AVISION_STRIPE_PRICE_ID_KEYWORD_75',
			'price_option'   => 'avision_stripe_price_id_keyword_75',
			'unit_amount'    => 700,
			'mode'           => 'subscription',
			'name'           => 'AVI — Radar Hebdomadaire (75 mots-cles)',
			'description'    => 'Scan hebdomadaire prioritaire (75 mots-cles)',
		],
		'daily_elite' => [
			'price_constant' => 'AVISION_STRIPE_PRICE_ID_DAILY_ELITE',
			'price_option'   => 'avision_stripe_price_id_daily_elite',
			'unit_amount'    => 1500,
			'mode'           => 'subscription',
			'name'           => 'AVI — Radar Journalier Elite (100 mots-cles)',
			'description'    => 'Scan journalier domination (100 mots-cles)',
		],
	];

	if ( ! isset( $map[ $type ] ) ) {
		return null;
	}

	$cfg = $map[ $type ];
	$pid = '';
	if ( ! empty( $cfg['price_constant'] ) && defined( $cfg['price_constant'] ) && is_string( constant( $cfg['price_constant'] ) ) ) {
		$pid = trim( (string) constant( $cfg['price_constant'] ) );
	}
	if ( $pid === '' && ! empty( $cfg['price_option'] ) ) {
		$opt = get_option( $cfg['price_option'], '' );
		if ( is_string( $opt ) ) {
			$pid = trim( $opt );
		}
	}

	$cfg['price_id'] = ( strpos( $pid, 'price_' ) === 0 ) ? $pid : '';
	return $cfg;
}

/**
 * URL de succès par défaut : page cockpit avec pre + session_id Stripe (Stripe remplace {CHECKOUT_SESSION_ID}).
 * Redirection finale : cookie SaaS + ?pid= (UUID) après validation (voir avision_handle_stripe_checkout_landing).
 * Filtre : avision_stripe_checkout_success_url pour surcharger l’URL complète.
 *
 * @param string $pre_token Token pré-inscription (temp_token).
 * @return string URL avec placeholder {CHECKOUT_SESSION_ID} pour Stripe.
 */
function avision_stripe_build_default_success_url( $pre_token, $landing_slug = null ) {
	$pre_token = is_string( $pre_token ) ? trim( $pre_token ) : '';
	$slug      = $landing_slug !== null && is_string( $landing_slug ) && $landing_slug !== ''
		? trim( $landing_slug, '/' )
		: null;
	if ( $slug === null || $slug === '' ) {
		$slug = apply_filters( 'avision_stripe_success_landing_slug', 'premium-dashboard', $pre_token );
	}
	// Pont de succès : on atterrit directement sur le Dashboard Premium avec session_id Stripe,
	// sans passer par /connexion/ (auto-login géré via hook init).
	$slug = is_string( $slug ) && $slug !== '' ? trim( $slug, '/' ) : 'premium-dashboard';
	if ( $slug !== 'premium-dashboard' ) {
		// Par sécurité : pour l'instant, on force Premium (B2C) comme demandé.
		$slug = 'premium-dashboard';
	}
	$base = trailingslashit( home_url( '/' . $slug . '/' ) );
	// Littéral {CHECKOUT_SESSION_ID} sans add_query_arg : Stripe exige ce placeholder exact (évite encodage %7B).
	return $base . '?status=success&session_id={CHECKOUT_SESSION_ID}';
}

/**
 * Garantit que l’URL de succès Checkout contient le placeholder Stripe {CHECKOUT_SESSION_ID}
 * (filtres ou surcharges ne doivent pas le retirer).
 *
 * @param string $url URL candidate.
 * @return string
 */
function avision_stripe_ensure_success_url_session_placeholder( $url ) {
	$ph  = '{CHECKOUT_SESSION_ID}';
	$url = is_string( $url ) ? trim( $url ) : '';
	if ( $url === '' ) {
		return trailingslashit( home_url( '/premium-dashboard/' ) ) . '?status=success&session_id=' . $ph;
	}
	if ( strpos( $url, $ph ) !== false ) {
		return $url;
	}
	// Remplacer un session_id=… statique ou erroné.
	if ( preg_match( '/[?&]session_id=/i', $url ) ) {
		return preg_replace( '/([?&])session_id=[^&]*/i', '$1session_id=' . $ph, $url, 1 );
	}
	$join = ( strpos( $url, '?' ) !== false ) ? '&' : '?';
	return $url . $join . 'session_id=' . $ph;
}

// Use WordPress function if available, otherwise use dirname
if ( function_exists( 'plugin_dir_path' ) ) {
	if ( ! defined( 'AVISION_PATH' ) ) {
		define( 'AVISION_PATH', plugin_dir_path( __FILE__ ) );
	}
	if ( ! defined( 'AVISION_PLUGIN_DIR' ) ) {
		define( 'AVISION_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
	}
} else {
	if ( ! defined( 'AVISION_PATH' ) ) {
		define( 'AVISION_PATH', __DIR__ . '/' );
	}
	if ( ! defined( 'AVISION_PLUGIN_DIR' ) ) {
		define( 'AVISION_PLUGIN_DIR', __DIR__ . '/' );
	}
}
// Chemin includes/ fiable (__DIR__ = racine du plugin ici)
if ( ! defined( 'AVISION_INCLUDES_DIR' ) ) {
	define( 'AVISION_INCLUDES_DIR', __DIR__ . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR );
}

if ( function_exists( 'plugin_dir_url' ) ) {
	if ( ! defined( 'AVISION_URL' ) ) {
		define( 'AVISION_URL', plugin_dir_url( __FILE__ ) );
	}
	if ( ! defined( 'AVISION_PLUGIN_URL' ) ) {
		define( 'AVISION_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
	}
} else {
	if ( ! defined( 'AVISION_URL' ) ) {
		define( 'AVISION_URL', '/wordpress/wp-content/plugins/avision-dashboard/' );
	}
	if ( ! defined( 'AVISION_PLUGIN_URL' ) ) {
		define( 'AVISION_PLUGIN_URL', '/wordpress/wp-content/plugins/avision-dashboard/' );
	}
}

// Autoloader Avision "blindé" — deux formats : class-*.php et *.php (ex. cron-task-manager.php)
spl_autoload_register( function ( $class ) {
	$class = ltrim( $class, '\\' );
	$prefix = 'Avision\\';
	$base_dir = plugin_dir_path( __FILE__ ) . 'includes/';
	$len = strlen( $prefix );
	if ( strncmp( $prefix, $class, $len ) !== 0 ) {
		return;
	}
	$relative_class = substr( $class, $len );
	// Format 1 : class-nom-de-la-classe.php
	$file1 = $base_dir . 'class-' . strtolower( str_replace( '_', '-', $relative_class ) ) . '.php';
	// Format 2 : nom-de-la-classe.php (pour cron-task-manager)
	$file2 = $base_dir . strtolower( str_replace( '_', '-', $relative_class ) ) . '.php';
	if ( file_exists( $file1 ) ) {
		require_once $file1;
	} elseif ( file_exists( $file2 ) ) {
		require_once $file2;
	}
} );

add_action(
	'plugins_loaded',
	static function () {
		if ( class_exists( '\Avision\Avision_AI_Interactions' ) ) {
			\Avision\Avision_AI_Interactions::init();
		}
	},
	20
);

if ( file_exists( AVISION_PATH . 'includes/avision-dataforseo-credentials.php' ) ) {
    require_once AVISION_PATH . 'includes/avision-dataforseo-credentials.php';
}
if ( file_exists( AVISION_PATH . 'includes/avision-oauth-google.php' ) ) {
    require_once AVISION_PATH . 'includes/avision-oauth-google.php';
}

if ( file_exists( AVISION_PATH . 'includes/avi-stripe-s10b.php' ) ) {
	require_once AVISION_PATH . 'includes/avi-stripe-s10b.php';
}
if ( file_exists( AVISION_PATH . 'includes/avi-s10c-performance.php' ) ) {
	require_once AVISION_PATH . 'includes/avi-s10c-performance.php';
}
if ( file_exists( AVISION_PATH . 'includes/avi-appsumo.php' ) ) {
	require_once AVISION_PATH . 'includes/avi-appsumo.php';
}
if ( file_exists( AVISION_PATH . 'includes/avi-email-s11.php' ) ) {
	require_once AVISION_PATH . 'includes/avi-email-s11.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-design-tokens.php' ) ) {
	require_once AVISION_PATH . 'includes/class-avision-design-tokens.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-components.php' ) ) {
	require_once AVISION_PATH . 'includes/class-avision-components.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-page-content.php' ) ) {
	require_once AVISION_PATH . 'includes/class-avision-page-content.php';
}
if ( file_exists( AVISION_PATH . 'admin/class-avision-theme-admin.php' ) ) {
	require_once AVISION_PATH . 'admin/class-avision-theme-admin.php';
}
if ( file_exists( AVISION_PATH . 'admin/class-avision-pages-admin.php' ) ) {
	require_once AVISION_PATH . 'admin/class-avision-pages-admin.php';
	if ( is_admin() && class_exists( 'Avision_Pages_Admin' ) ) {
		Avision_Pages_Admin::init();
	}
}

// ── Avision Pages Admin — guaranteed registration ──
add_action( 'admin_menu', function() {

	// Require the file if class not loaded yet
	if ( ! class_exists( 'Avision_Pages_Admin' ) ) {
		$file = plugin_dir_path( __FILE__ ) . 'admin/class-avision-pages-admin.php';
		if ( file_exists( $file ) ) {
			require_once $file;
		} else {
			// File missing — create inline minimal version
			add_submenu_page(
				'avision-dashboard',
				'Pages Content',
				'📄 Pages',
				'manage_options',
				'avi-pages-admin',
				'avi_inline_pages_admin_fallback'
			);
			return;
		}
	}

	// Register submenu
	add_submenu_page(
		'avision-dashboard',   // parent slug
		'Pages Content',       // page title
		'📄 Pages',            // menu label
		'manage_options',      // capability
		'avi-pages-admin',     // slug
		function() {
			if ( class_exists( 'Avision_Pages_Admin' ) ) {
				Avision_Pages_Admin::render_page();
			} else {
				avi_inline_pages_admin_fallback();
			}
		}
	);

	// Also register Theme submenu
	add_submenu_page(
		'avision-dashboard',
		'Apparence & Thème',
		'🎨 Apparence',
		'manage_options',
		'avi-theme-admin',
		function() {
			if ( class_exists( 'Avision_Theme_Admin' ) ) {
				Avision_Theme_Admin::render_page();
			}
		}
	);

}, 20 );

add_action( 'admin_menu', function() {

	add_menu_page(
		'LocalEdge',
		'LocalEdge',
		'manage_options',
		'localedge',
		'avi_localedge_admin_page',
		'data:image/svg+xml;base64,' . base64_encode(
			'<svg xmlns="http://www.w3.org/2000/svg" '
			. 'viewBox="0 0 20 20" fill="none">'
			. '<circle cx="10" cy="10" r="9" '
			. 'stroke="#f97316" stroke-width="1.5"/>'
			. '<circle cx="10" cy="10" r="6" '
			. 'stroke="#f97316" stroke-width="1.5"/>'
			. '<circle cx="10" cy="10" r="3" '
			. 'stroke="#f97316" stroke-width="1.5"/>'
			. '<line x1="10" y1="1" x2="10" y2="4" '
			. 'stroke="#f97316" stroke-width="1.5"/>'
			. '<line x1="10" y1="16" x2="10" y2="19" '
			. 'stroke="#f97316" stroke-width="1.5"/>'
			. '<line x1="1" y1="10" x2="4" y2="10" '
			. 'stroke="#f97316" stroke-width="1.5"/>'
			. '<line x1="16" y1="10" x2="19" y2="10" '
			. 'stroke="#f97316" stroke-width="1.5"/>'
			. '<circle cx="10" cy="10" r="1.5" '
			. 'fill="#f97316"/>'
			. '</svg>'
		),
		30
	);

}, 25 );

function avi_localedge_admin_page() {

	$key_id  = get_option( 'localedge_key_id', '' );
	$domain  = get_option( 'localedge_domain', '' );
	$gbp_ok  = get_option( 'localedge_gbp_connected', '' );
	$step1ok = ! empty( $key_id );
	$step2ok = ! empty( $gbp_ok );
	$lang    = substr( get_locale(), 0, 2 ) ?: 'en';

	$connect_url = '';
	if ( $step1ok ) {
		$connect_url = 'https://avision.org/localedge-connect'
			. '?key=' . rawurlencode( $key_id )
			. '&domain=' . rawurlencode( $domain )
			. '&lang=' . rawurlencode( $lang );
	}

	$embed_url = '';
	if ( $step1ok ) {
		$embed_url = 'https://avision.org/wp-json/avision/v1/embed'
			. '?key_id=' . rawurlencode( $key_id )
			. '&profile_id=0'
			. '&lang=' . rawurlencode( $lang );
	}

	?>
	<div class="wrap" id="localedge-admin-wrap">
	<h1 style="display:flex;align-items:center;gap:10px;
		font-size:22px;font-weight:600;margin-bottom:24px;">
		<svg xmlns="http://www.w3.org/2000/svg"
			width="28" height="28" viewBox="0 0 20 20" fill="none">
			<circle cx="10" cy="10" r="9"
				stroke="#f97316" stroke-width="1.5"/>
			<circle cx="10" cy="10" r="6"
				stroke="#f97316" stroke-width="1.5"/>
			<circle cx="10" cy="10" r="3"
				stroke="#f97316" stroke-width="1.5"/>
			<circle cx="10" cy="10" r="1.5" fill="#f97316"/>
		</svg>
		LocalEdge
	</h1>

	<div style="display:flex;gap:24px;max-width:860px;">

	<!-- ÉTAPE 1 -->
	<div style="flex:1;background:#fff;border:1px solid #e5e7eb;
		border-radius:10px;padding:24px;">
		<div style="display:flex;align-items:center;gap:10px;
			margin-bottom:16px;">
			<span style="width:28px;height:28px;border-radius:50%;
				background:<?php echo $step1ok
					? '#16a34a' : '#f97316'; ?>;
				color:#fff;display:flex;align-items:center;
				justify-content:center;font-weight:700;
				font-size:14px;">1</span>
			<strong style="font-size:15px;">
				<?php echo $step1ok
					? '&#10003; Licence active'
					: 'Activer la licence'; ?>
			</strong>
		</div>

		<?php if ( $step1ok ) : ?>
			<p style="margin:0 0 8px;color:#6b7280;font-size:13px;">
				Clé embed :
				<code style="background:#f3f4f6;padding:2px 6px;
					border-radius:4px;"><?php
					echo esc_html( $key_id ); ?></code>
			</p>
			<p style="margin:0;color:#6b7280;font-size:13px;">
				Domaine : <?php echo esc_html( $domain ); ?>
			</p>
		<?php else : ?>
			<p style="color:#6b7280;font-size:13px;margin:0 0 16px;">
				Entrez votre purchase code re&#231;u apr&#232;s
				l&#39;achat pour activer LocalEdge.
			</p>
			<div id="le-activate-wrap">
				<input type="text" id="le-purchase-code"
					placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
					style="width:100%;padding:8px 10px;
					border:1px solid #d1d5db;border-radius:6px;
					font-size:13px;margin-bottom:10px;
					box-sizing:border-box;"/>
				<button type="button" id="le-activate-btn"
					style="background:#f97316;color:#fff;border:none;
					border-radius:6px;padding:9px 20px;font-size:13px;
					font-weight:600;cursor:pointer;width:100%;">
					Activer la licence
				</button>
				<p id="le-activate-msg"
					style="margin:10px 0 0;font-size:13px;
					display:none;"></p>
			</div>
		<?php endif; ?>
	</div>

	<!-- ÉTAPE 2 -->
	<div style="flex:1;background:#fff;border:1px solid #e5e7eb;
		border-radius:10px;padding:24px;
		opacity:<?php echo $step1ok ? '1' : '0.45'; ?>;
		pointer-events:<?php echo $step1ok ? 'auto' : 'none'; ?>;">
		<div style="display:flex;align-items:center;gap:10px;
			margin-bottom:16px;">
			<span style="width:28px;height:28px;border-radius:50%;
				background:<?php echo $step2ok
					? '#16a34a' : '#6b7280'; ?>;
				color:#fff;display:flex;align-items:center;
				justify-content:center;font-weight:700;
				font-size:14px;">2</span>
			<strong style="font-size:15px;">
				<?php echo $step2ok
					? '&#10003; Google Business connect&#233;'
					: 'Connecter Google Business'; ?>
			</strong>
		</div>

		<?php if ( $step2ok ) : ?>
			<p style="color:#6b7280;font-size:13px;
				margin:0 0 16px;">
				Votre fiche Google Business est connect&#233;e.
			</p>
			<a href="<?php echo esc_url( $embed_url ); ?>"
				target="_blank"
				style="display:inline-block;background:#7c3aed;
				color:#fff;border-radius:6px;padding:9px 20px;
				font-size:13px;font-weight:600;
				text-decoration:none;">
				Ouvrir LocalEdge
			</a>
		<?php else : ?>
			<p style="color:#6b7280;font-size:13px;
				margin:0 0 16px;">
				Connectez votre fiche Google Business Profile
				pour commencer &#224; utiliser LocalEdge.
			</p>
			<?php if ( $step1ok ) : ?>
			<button type="button" id="le-oauth-btn"
				style="background:#4285f4;color:#fff;border:none;
				border-radius:6px;padding:9px 20px;font-size:13px;
				font-weight:600;cursor:pointer;width:100%;">
				&#128279; Connecter Google Business
			</button>
			<p id="le-oauth-msg"
				style="margin:10px 0 0;font-size:13px;
				color:#6b7280;display:none;">
				Fen&#234;tre OAuth ouverte — connectez-vous
				puis revenez ici.
			</p>
			<?php endif; ?>
		<?php endif; ?>
	</div>

	</div>
	</div>

	<script>
	(function() {

		var btn = document.getElementById('le-activate-btn');
		if (btn) {
			btn.addEventListener('click', function() {
				var code = (
					document.getElementById('le-purchase-code')
					.value || ''
				).trim();
				if (!code) return;
				var msg = document.getElementById('le-activate-msg');
				btn.disabled = true;
				btn.textContent = 'Activation...';
				fetch('<?php echo esc_url(
					rest_url( 'avision/v1/embed/activate' )
				); ?>', {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json'
					},
					body: JSON.stringify({
						purchase_code: code,
						domain: window.location.hostname,
						key_type: 'solo'
					})
				})
				.then(function(r) { return r.json(); })
				.then(function(d) {
					if (d && d.success) {
						msg.style.display = 'block';
						msg.style.color = '#16a34a';
						msg.textContent = '\u2713 Activation en cours...';
						return fetch('<?php echo esc_url(
							admin_url( 'admin-ajax.php' )
						); ?>', {
							method: 'POST',
							headers: {
								'Content-Type':
								'application/x-www-form-urlencoded'
							},
							body: 'action=localedge_save_key'
								+ '&nonce=<?php echo wp_create_nonce(
									'localedge_gbp'
								); ?>'
								+ '&key_id=' + encodeURIComponent(d.key_id)
								+ '&secret=' + encodeURIComponent(d.secret)
								+ '&domain=' + encodeURIComponent(
									window.location.hostname
								)
						});
					} else {
						msg.style.display = 'block';
						msg.style.color = '#dc2626';
						msg.textContent = (d && d.message)
							? d.message
							: 'Erreur activation.';
						btn.disabled = false;
						btn.textContent = 'Activer la licence';
						return Promise.reject('error');
					}
				})
				.then(function() {
					setTimeout(function() {
						window.location.reload();
					}, 1200);
				})
				.catch(function(e) {
					if (e !== 'error') {
						msg.style.display = 'block';
						msg.style.color = '#dc2626';
						msg.textContent = 'Erreur r\u00e9seau.';
						btn.disabled = false;
						btn.textContent = 'Activer la licence';
					}
				});
			});
		}

		var oauthBtn = document.getElementById('le-oauth-btn');
		if (oauthBtn) {
			oauthBtn.addEventListener('click', function() {
				var connectUrl = <?php echo wp_json_encode( $connect_url ); ?>;
				if (!connectUrl) return;
				var popup = window.open(
					connectUrl,
					'localedge_oauth',
					'width=600,height=700,scrollbars=yes'
				);
				var oauthMsg = document.getElementById(
					'le-oauth-msg'
				);
				if (oauthMsg) oauthMsg.style.display = 'block';
				window.addEventListener('message', function handler(e) {
					if (e.data === 'gbp_connected') {
						window.removeEventListener(
							'message', handler
						);
						if (popup && !popup.closed) popup.close();
						fetch('<?php echo esc_url(
							admin_url( 'admin-ajax.php' )
						); ?>', {
							method: 'POST',
							headers: {
								'Content-Type':
								'application/x-www-form-urlencoded'
							},
							body: 'action=localedge_set_gbp_connected'
								+ '&nonce=<?php echo wp_create_nonce(
									'localedge_gbp'
								); ?>'
						})
						.then(function() {
							window.location.reload();
						});
					}
				});
			});
		}

	})();
	</script>
	<?php
}

add_action( 'wp_ajax_localedge_set_gbp_connected', function() {
	check_ajax_referer( 'localedge_gbp', 'nonce' );
	update_option( 'localedge_gbp_connected', '1' );
	wp_send_json_success();
} );

add_action( 'wp_ajax_localedge_save_key', function() {
	check_ajax_referer( 'localedge_gbp', 'nonce' );
	$key_id = sanitize_text_field(
		isset( $_POST['key_id'] ) ? (string) wp_unslash( $_POST['key_id'] ) : ''
	);
	$secret = sanitize_text_field(
		isset( $_POST['secret'] ) ? (string) wp_unslash( $_POST['secret'] ) : ''
	);
	$domain = sanitize_text_field(
		isset( $_POST['domain'] ) ? (string) wp_unslash( $_POST['domain'] ) : ''
	);
	if ( $key_id ) {
		update_option( 'localedge_key_id', $key_id );
		update_option( 'localedge_domain', $domain );
		update_option( 'localedge_secret', $secret );
	}
	wp_send_json_success();
} );

// Fallback inline admin if file missing
if ( ! function_exists( 'avi_inline_pages_admin_fallback' ) ) {
	function avi_inline_pages_admin_fallback() {
		echo '<div class="wrap">';
		echo '<h1>📄 Avision Pages</h1>';
		echo '<div class="notice notice-error">';
		echo '<p>File missing: admin/class-avision-pages-admin.php</p>';
		echo '</div>';
		echo '</div>';
	}
}

add_action(
	'edit_form_after_title',
	static function () {
		$post = get_post();
		if ( ! $post || $post->post_type !== 'page' ) {
			return;
		}

		$avi_slugs = array(
			'home',
			'local-business',
			'about',
			'how-it-works',
			'api-security',
			'legal',
			'pricing',
			'agency-pricing',
			'nonprofits',
			'founding-sponsors',
			'sponsors-fondateurs',
			'patrocinadores-fundadores',
			'ambassador-program',
			'refer',
			'roadmap',
			'faq',
			'contact',
		);
		if ( ! in_array( (string) $post->post_name, $avi_slugs, true ) ) {
			return;
		}

		$admin_url = admin_url( 'admin.php?page=avi-pages-admin&avi_page=' . (string) $post->post_name );
		echo '<div style="background:#0D1E38;color:#fff;padding:20px;border-radius:8px;margin:20px 0;border-left:4px solid #6B46E8">';
		echo '<h3 style="margin:0 0 8px;color:#fff">📄 Avision Template Page</h3>';
		echo '<p style="margin:0 0 12px;color:#A0B4D0">This page uses an Avision custom template. The WordPress editor content is not displayed — edit page content in the Avision Pages editor.</p>';
		echo '<a href="' . esc_url( $admin_url ) . '" class="button button-primary" style="background:#6B46E8;border-color:#5030C0;font-size:14px;padding:8px 20px">✏️ Edit in Avision Pages Editor</a>';
		echo '<a href="' . esc_url( get_permalink( (int) $post->ID ) ) . '" target="_blank" class="button" style="margin-left:8px">👁 View Page</a>';
		echo '</div>';
	}
);

add_action(
	'wp_footer',
	static function () {
		if ( ! function_exists( 'current_user_can' ) || ! current_user_can( 'manage_options' ) ) {
			return;
		}
		$post = function_exists( 'get_queried_object' ) ? get_queried_object() : null;
		if ( ! ( $post instanceof \WP_Post ) ) {
			return;
		}

		$avi_slugs = array(
			'home',
			'local-business',
			'about',
			'how-it-works',
			'api-security',
			'legal',
			'pricing',
			'agency-pricing',
			'nonprofits',
			'founding-sponsors',
			'sponsors-fondateurs',
			'patrocinadores-fundadores',
			'ambassador-program',
			'refer',
			'roadmap',
			'faq',
			'contact',
		);
		if ( ! in_array( (string) $post->post_name, $avi_slugs, true ) ) {
			return;
		}

		$editor_url = admin_url( 'admin.php?page=avi-pages-admin&avi_page=' . (string) $post->post_name );
		echo '<a href="' . esc_url( $editor_url ) . '" style="position:fixed;bottom:24px;right:24px;z-index:9999;background:#6B46E8;color:#fff;padding:10px 18px;border-radius:50px;font-size:13px;font-weight:600;text-decoration:none;box-shadow:0 4px 20px rgba(107,70,232,0.5);display:flex;align-items:center;gap:6px">✏️ Edit Page</a>';
	},
	99999
);

add_action(
	'wp_head',
	static function () {
		if ( is_admin() ) {
			return;
		}
		echo '<style id="avi-footer-override">
footer#avi-footer,
footer#avi-footer > div,
body footer,
#avi-footer,
[data-avi-footer] {
	background: #080E1C !important;
	background-color: #080E1C !important;
}
</style>';
	},
	999
);

// Fichiers non autoloadés (scripts d’installation, routeur global, core legacy)
if ( file_exists( AVISION_PATH . 'includes/class-ajax-router.php' ) ) {
    require_once AVISION_PATH . 'includes/class-ajax-router.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-draft.php' ) ) {
    require_once AVISION_PATH . 'includes/class-avision-draft.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-radar-heatmap-rest.php' ) ) {
    require_once AVISION_PATH . 'includes/class-avision-radar-heatmap-rest.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-heatmap-scheduler.php' ) ) {
    require_once AVISION_PATH . 'includes/class-avision-heatmap-scheduler.php'; // Anti-régression #Sprint43-scheduler
}
// #Sprint44-Plugin — Embed iframe signé HMAC
if ( file_exists( AVISION_PATH . 'includes/class-avision-embed.php' ) ) {
    require_once AVISION_PATH . 'includes/class-avision-embed.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-shortcodes.php' ) ) {
	require_once AVISION_PATH . 'includes/class-avision-shortcodes.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-dashboard-route-guard.php' ) ) {
	require_once AVISION_PATH . 'includes/class-avision-dashboard-route-guard.php';
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-auth-shortcodes.php' ) ) {
	require_once AVISION_PATH . 'includes/class-avision-auth-shortcodes.php';
}
if ( file_exists( plugin_dir_path( __FILE__ ) . 'install-avi-core-tables.php' ) ) {
	require_once plugin_dir_path( __FILE__ ) . 'install-avi-core-tables.php';
}
// Autoload centralisé — toutes les classes admin dans includes/admin/autoload-admin.php
if ( file_exists( AVISION_PATH . 'includes/admin/autoload-admin.php' ) ) {
	require_once AVISION_PATH . 'includes/admin/autoload-admin.php';
} else {
	error_log( '[AVI] autoload-admin.php manquant — classes admin non chargées' );
}
if ( file_exists( AVISION_PATH . 'includes/class-avision-chat.php' ) ) {
	require_once AVISION_PATH . 'includes/class-avision-chat.php';
	Avision_Chat::init();
}
if ( file_exists( plugin_dir_path( __FILE__ ) . 'includes/class-avision-stripe-webhook.php' ) ) {
	require_once plugin_dir_path( __FILE__ ) . 'includes/class-avision-stripe-webhook.php';
}
// Simulateur Royal (auto-login de test) : désactivé par défaut — define( 'AVISION_ENABLE_STRIPE_SIMULATE', true ) dans wp-config pour l’activer uniquement en dev.
if ( file_exists( plugin_dir_path( __FILE__ ) . 'includes/avision-simulate-stripe-success.php' )
	&& defined( 'AVISION_ENABLE_STRIPE_SIMULATE' ) && AVISION_ENABLE_STRIPE_SIMULATE ) {
	require_once plugin_dir_path( __FILE__ ) . 'includes/avision-simulate-stripe-success.php';
}

/**
 * Lance immédiatement la Wave 3 (Mainteneurs) pour un profil dès que le statut passe à Premium.
 * À appeler après activation du Mode Gardien (REST ou paiement) pour ne pas attendre le cycle CRON.
 *
 * @param int $profile_id ID du profil (avi_saas_profiles.id). En contexte REST, audit_id peut être utilisé comme profile_id.
 */
function avision_trigger_immediate_wave3( $profile_id ) {
	$profile_id = (int) $profile_id;
	if ( $profile_id <= 0 ) {
		return;
	}
	global $wpdb;
	$table = $wpdb->prefix . 'avi_saas_profiles';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
		return;
	}
	$profile = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d LIMIT 1", $profile_id ), ARRAY_A );
	if ( ! is_array( $profile ) || empty( $profile['user_id'] ) ) {
		return;
	}
	$user_id = (int) $profile['user_id'];
	$wpdb->update(
		$table,
		[ 'guardian_active' => 1 ],
		[ 'id' => $profile_id ],
		[ '%d' ],
		[ '%d' ]
	);
	// Passer un profil avec guardian_active = 1 pour que run_wave3 déclenche le Cold Start
	// (premier post Présentation + image Unsplash si galerie vide). Sans cela, $profile chargé
	// avant l'update aurait guardian_active = 0 et le Cold Start ne s'exécuterait pas.
	$profile['guardian_active'] = 1;
	if ( class_exists( 'Avision\Avision_Guardian_Maintainers' ) ) {
		$maintainers = new Avision\Avision_Guardian_Maintainers();
		$maintainers->run_wave3( $user_id, $profile );
	}
}

if ( ! function_exists( 'avi_get_current_lang' ) ) {
	function avi_get_current_lang(): string {
		$lang = get_query_var( 'avi_lang', 'en' );
		if ( $lang === 'en' && isset( $_SERVER['REQUEST_URI'] ) ) {
			$_avi_req = $_SERVER['REQUEST_URI'];
			if ( strpos( $_avi_req, '/fr/' ) === 0 || $_avi_req === '/fr/' ) { $lang = 'fr'; }
			elseif ( strpos( $_avi_req, '/es/' ) === 0 || $_avi_req === '/es/' ) { $lang = 'es'; }
		}
		// Setter cookie avi_lang pour transmission tunnel B2C
		if ( $lang !== 'en' && ! headers_sent() ) {
			if ( ! isset( $_COOKIE['avi_lang'] ) || $_COOKIE['avi_lang'] !== $lang ) {
				setcookie( 'avi_lang', $lang, time() + 3600, '/', '', false, false );
				$_COOKIE['avi_lang'] = $lang;
			}
		}
		return in_array( $lang, array( 'fr', 'es' ), true ) ? $lang : 'en';
	}
}

if ( ! function_exists( 'avi_phase1_register_multilang_rewrite' ) ) {
	function avi_phase1_register_multilang_rewrite(): void {
		add_rewrite_rule(
			'^(fr|es)/([^/]+)/?$',
			'index.php?avi_lang=$matches[1]&pagename=$matches[1]/$matches[2]',
			'top'
		);
		add_rewrite_rule(
			'^(fr|es)/?$',
			'index.php?avi_lang=$matches[1]&pagename=$matches[1]',
			'top'
		);
	}
}
add_action( 'init', 'avi_phase1_register_multilang_rewrite', 15 );

if ( ! function_exists( 'avi_phase1_register_query_vars' ) ) {
	function avi_phase1_register_query_vars( array $vars ): array {
		$vars[] = 'avi_lang';
		return $vars;
	}
}
add_filter( 'query_vars', 'avi_phase1_register_query_vars', 15 );

if ( ! function_exists( 'avi_phase1_maybe_flush_rewrite_rules' ) ) {
	function avi_phase1_maybe_flush_rewrite_rules(): void {
		$ver = (string) get_option( 'avi_rewrite_version', '' );
		if ( $ver === 'phase2_v1' ) {
			return;
		}
		avi_phase1_register_multilang_rewrite();
		flush_rewrite_rules( false );
		update_option( 'avi_rewrite_version', 'phase2_v1', false );
	}
}
add_action( 'init', 'avi_phase1_maybe_flush_rewrite_rules', 20 );

if ( ! function_exists( 'avi_phase1_ensure_required_pages' ) ) {
	function avi_phase1_ensure_required_pages(): void {
		if ( ! function_exists( 'wp_insert_post' ) ) {
			return;
		}
		$done = (string) get_option( 'avi_phase4_pages_created', '' );
		if ( $done === '1' ) {
			$late_pages = array(
				'roadmap' => array(
					'title'    => 'Product Roadmap',
					'template' => 'avision-dashboard/templates/pages/roadmap.php',
				),
				'faq'     => array(
					'title'    => 'FAQ',
					'template' => 'avision-dashboard/templates/pages/faq.php',
				),
				'contact' => array(
					'title'    => 'Contact',
					'template' => 'avision-dashboard/templates/pages/contact.php',
				),
			);
			foreach ( $late_pages as $slug => $meta ) {
				$existing = get_page_by_path( $slug );
				if ( $existing instanceof \WP_Post ) {
					if ( 'contact' === $slug ) {
						update_post_meta( (int) $existing->ID, '_wp_page_template', (string) $meta['template'] );
					}
					continue;
				}
				if ( 'contact' === $slug ) {
					continue;
				}
				$page_id = wp_insert_post(
					array(
						'post_title'     => (string) $meta['title'],
						'post_name'      => $slug,
						'post_status'    => 'publish',
						'post_type'      => 'page',
						'comment_status' => 'closed',
						'ping_status'    => 'closed',
					),
					true
				);
				if ( ! is_wp_error( $page_id ) && (int) $page_id > 0 ) {
					update_post_meta( (int) $page_id, '_wp_page_template', (string) $meta['template'] );
				}
			}
			return;
		}

		$base_pages = array(
			'api-security' => 'API Security & OAuth',
			'about'        => 'About Avision',
			'how-it-works' => 'How It Works',
			'legal'        => 'Legal',
			'home'         => 'Home',
			'localedge-connect' => 'LocalEdge Connect',
			'local-business' => 'Local Business SEO',
			'pricing'        => 'Pricing',
			'agency-pricing' => 'Agency Pricing',
			'nonprofits'         => 'Nonprofits',
			'founding-sponsors'  => 'Founding Sponsors',
			'ambassador-program' => 'Ambassador Program',
			'refer'              => 'Refer',
			'roadmap'            => 'Product Roadmap',
			'faq'                => 'FAQ',
		);
		$multilang_pages = array(
			'api-security'   => 'API Security & OAuth',
			'about'          => 'About Avision',
			'how-it-works'   => 'How It Works',
			'legal'          => 'Legal',
			'local-business' => 'Local Business',
			'pricing'        => 'Pricing',
			'agency-pricing' => 'Agency Pricing',
			'faq'            => 'FAQ',
			'roadmap'        => 'Roadmap',
			'contact'        => 'Contact',
			'nonprofits'         => 'Nonprofits',
			'ambassador-program' => 'Ambassador Program',
			'appsumo-redeem'     => 'AppSumo Redeem',
		);
		$multilang_founding_sponsors = array(
			'fr' => array(
				'slug'     => 'sponsors-fondateurs',
				'title'    => 'Sponsors Fondateurs',
				'template' => 'founding-sponsors-fr.php',
			),
			'es' => array(
				'slug'     => 'patrocinadores-fundadores',
				'title'    => 'Patrocinadores Fundadores',
				'template' => 'founding-sponsors-es.php',
			),
		);

		$ensure_page = static function ( string $slug, string $title, int $parent_id = 0 ) {
			$existing = get_page_by_path( $slug );
			if ( $existing instanceof \WP_Post ) {
				return (int) $existing->ID;
			}
			$id = wp_insert_post(
				array(
					'post_title'     => $title,
					'post_name'      => sanitize_title( $slug ),
					'post_status'    => 'publish',
					'post_type'      => 'page',
					'post_parent'    => $parent_id,
					'comment_status' => 'closed',
					'ping_status'    => 'closed',
				),
				true
			);
			if ( is_wp_error( $id ) ) {
				return 0;
			}
			return (int) $id;
		};

		foreach ( $base_pages as $slug => $title ) {
			$page_id = $ensure_page( $slug, $title );
			if ( $page_id > 0 ) {
				update_post_meta( $page_id, '_wp_page_template', 'avision-dashboard/templates/pages/' . $slug . '.php' );
			}
		}

		$localedge_connect_page = get_page_by_path( 'localedge-connect' );
		if ( $localedge_connect_page instanceof \WP_Post ) {
			update_post_meta(
				(int) $localedge_connect_page->ID,
				'_wp_page_template',
				'page-localedge-connect.php'
			);
		}

		$contact_page = get_page_by_path( 'contact' );
		if ( $contact_page instanceof \WP_Post ) {
			update_post_meta( (int) $contact_page->ID, '_wp_page_template', 'avision-dashboard/templates/pages/contact.php' );
		}

		$home_page = get_page_by_path( 'home' );
		if ( $home_page instanceof \WP_Post ) {
			update_option( 'show_on_front', 'page', false );
			update_option( 'page_on_front', (int) $home_page->ID, false );
			update_post_meta( (int) $home_page->ID, '_wp_page_template', 'avision-dashboard/templates/pages/home.php' );
		}

		foreach ( array( 'fr' => 'FR', 'es' => 'ES' ) as $lang_slug => $lang_title ) {
			$parent_id = $ensure_page( $lang_slug, $lang_title );
			if ( $parent_id <= 0 ) {
				continue;
			}
			foreach ( $multilang_pages as $slug => $title ) {
				$child_path = $lang_slug . '/' . $slug;
				$existing_child = get_page_by_path( $child_path );
				if ( $existing_child instanceof \WP_Post ) {
					$child_id = (int) $existing_child->ID;
				} else {
					$child_id = wp_insert_post(
						array(
							'post_title'     => $title,
							'post_name'      => sanitize_title( $slug ),
							'post_status'    => 'publish',
							'post_type'      => 'page',
							'post_parent'    => $parent_id,
							'comment_status' => 'closed',
							'ping_status'    => 'closed',
						),
						true
					);
					if ( is_wp_error( $child_id ) ) {
						$child_id = 0;
					} else {
						$child_id = (int) $child_id;
					}
				}
				if ( $child_id > 0 ) {
					update_post_meta( $child_id, '_wp_page_template', 'avision-dashboard/templates/pages/' . $slug . '.php' );
				}
			}
			if ( isset( $multilang_founding_sponsors[ $lang_slug ] ) ) {
				$fs_meta = $multilang_founding_sponsors[ $lang_slug ];
				$fs_slug = (string) $fs_meta['slug'];
				$fs_path = $lang_slug . '/' . $fs_slug;
				$existing_fs = get_page_by_path( $fs_path );
				if ( $existing_fs instanceof \WP_Post ) {
					$fs_id = (int) $existing_fs->ID;
				} else {
					$fs_id = wp_insert_post(
						array(
							'post_title'     => (string) $fs_meta['title'],
							'post_name'      => sanitize_title( $fs_slug ),
							'post_status'    => 'publish',
							'post_type'      => 'page',
							'post_parent'    => $parent_id,
							'comment_status' => 'closed',
							'ping_status'    => 'closed',
						),
						true
					);
					if ( is_wp_error( $fs_id ) ) {
						$fs_id = 0;
					} else {
						$fs_id = (int) $fs_id;
					}
				}
				if ( $fs_id > 0 ) {
					update_post_meta(
						$fs_id,
						'_wp_page_template',
						'avision-dashboard/templates/pages/' . (string) $fs_meta['template']
					);
				}
			}
		}

		update_option( 'avi_phase4_pages_created', '1', false );
	}
}
add_action( 'init', 'avi_phase1_ensure_required_pages', 25 );

if ( ! function_exists( 'avi_setup_nav_menu' ) ) {
	/**
	 * One-shot: crée/peuple le menu WP principal (Apparence → Menus).
	 *
	 * NOTE: le front utilise `templates/layout/header.php` (pas `wp_nav_menu`),
	 * ce menu sert surtout de référence admin.
	 *
	 * @return void
	 */
	function avi_setup_nav_menu(): void {
		if ( ! function_exists( 'get_option' ) || ! function_exists( 'update_option' ) ) {
			return;
		}
		if ( get_option( 'avi_nav_menu_created' ) ) {
			return;
		}
		if ( ! function_exists( 'wp_create_nav_menu' ) || ! function_exists( 'wp_update_nav_menu_item' ) ) {
			return;
		}

		$menu_name = 'Avision Main Menu';
		$menu_id   = wp_create_nav_menu( $menu_name );
		if ( is_wp_error( $menu_id ) ) {
			$menu_obj = function_exists( 'get_term_by' ) ? get_term_by( 'name', $menu_name, 'nav_menu' ) : null;
			if ( ! $menu_obj || empty( $menu_obj->term_id ) ) {
				return;
			}
			$menu_id = (int) $menu_obj->term_id;
		}

		$menu_pages = array(
			'Agencies'       => '/',
			'Local Business' => '/local-business/',
			'How It Works'   => '/how-it-works/',
			'Pricing'        => '/pricing/',
			'Blog'           => '/blog/',
		);

		$order = 1;
		foreach ( $menu_pages as $label => $url ) {
			$url  = is_string( $url ) ? $url : '/';
			$path = trim( (string) $url, '/' );
			$page = function_exists( 'get_page_by_path' ) && $path !== '' ? get_page_by_path( $path ) : null;

			if ( $page instanceof \WP_Post ) {
				wp_update_nav_menu_item(
					$menu_id,
					0,
					array(
						'menu-item-type'      => 'post_type',
						'menu-item-object'    => 'page',
						'menu-item-object-id' => (int) $page->ID,
						'menu-item-title'     => (string) $label,
						'menu-item-status'    => 'publish',
						'menu-item-position'  => $order++,
					)
				);
			} else {
				wp_update_nav_menu_item(
					$menu_id,
					0,
					array(
						'menu-item-type'     => 'custom',
						'menu-item-url'      => home_url( $url ),
						'menu-item-title'    => (string) $label,
						'menu-item-status'   => 'publish',
						'menu-item-position' => $order++,
					)
				);
			}
		}

		if ( function_exists( 'get_theme_mod' ) && function_exists( 'set_theme_mod' ) ) {
			$locations = get_theme_mod( 'nav_menu_locations' );
			if ( is_array( $locations ) ) {
				foreach ( array_keys( $locations ) as $location ) {
					$locations[ $location ] = $menu_id;
				}
				set_theme_mod( 'nav_menu_locations', $locations );
			}
		}

		update_option( 'avi_nav_menu_created', true, false );
	}
}
add_action( 'init', 'avi_setup_nav_menu', 30 );

if ( ! function_exists( 'avi_phase1_legal_redirects' ) ) {
	function avi_phase1_legal_redirects(): void {
		if ( is_admin() || ! function_exists( 'is_page' ) ) {
			return;
		}
		if ( is_page( 'privacy-policy' ) ) {
			wp_redirect( home_url( '/legal/#privacy' ), 301 );
			exit;
		}
		if ( is_page( 'terms-of-use' ) ) {
			wp_redirect( home_url( '/legal/#terms' ), 301 );
			exit;
		}
	}
}
add_action( 'template_redirect', 'avi_phase1_legal_redirects', 1 );

if ( ! function_exists( 'avi_legal_lang_duplicate_redirects' ) ) {
	function avi_legal_lang_duplicate_redirects(): void {
		if ( is_admin() || ! function_exists( 'is_page' ) ) {
			return;
		}
		if ( is_page( 6770 ) ) {
			wp_redirect( home_url( '/fr/mentions-legales/' ), 301 );
			exit;
		}
		if ( is_page( 6674 ) ) {
			wp_redirect( home_url( '/es/aviso-legal/' ), 301 );
			exit;
		}
	}
}
add_action( 'template_redirect', 'avi_legal_lang_duplicate_redirects', 1 );

if ( ! function_exists( 'avi_phase1_template_include' ) ) {
	function avi_phase1_template_include( string $template ): string {
		if ( ! function_exists( 'is_page' ) || ! is_page() ) {
			return $template;
		}
		$page_id = get_queried_object_id();
		if ( $page_id <= 0 ) {
			return $template;
		}
		$meta = (string) get_post_meta( $page_id, '_wp_page_template', true );
		$prefix = 'avision-dashboard/templates/pages/';
		if ( strpos( $meta, $prefix ) !== 0 ) {
			return $template;
		}
		$rel = str_replace( 'avision-dashboard/', '', $meta );
		$candidate = trailingslashit( AVISION_PATH ) . ltrim( $rel, '/\\' );
		if ( file_exists( $candidate ) ) {
			return $candidate;
		}
		return $template;
	}
}
add_filter( 'template_include', 'avi_phase1_template_include', 99 );

add_action(
	'plugins_loaded',
	static function () {
		if ( ! class_exists( 'Avision_Design_Tokens' ) && file_exists( plugin_dir_path( __FILE__ ) . 'includes/class-avision-design-tokens.php' ) ) {
			require_once plugin_dir_path( __FILE__ ) . 'includes/class-avision-design-tokens.php';
		}
		if ( ! class_exists( 'Avision_Components' ) && file_exists( plugin_dir_path( __FILE__ ) . 'includes/class-avision-components.php' ) ) {
			require_once plugin_dir_path( __FILE__ ) . 'includes/class-avision-components.php';
		}
		if ( ! class_exists( 'Avision_Theme_Admin' ) && file_exists( plugin_dir_path( __FILE__ ) . 'admin/class-avision-theme-admin.php' ) ) {
			require_once plugin_dir_path( __FILE__ ) . 'admin/class-avision-theme-admin.php';
		}
		if ( class_exists( 'Avision_Design_Tokens' ) ) {
			Avision_Design_Tokens::init();
		}
		if ( class_exists( 'Avision_Components' ) ) {
			Avision_Components::init();
		}
		if ( is_admin() && class_exists( 'Avision_Theme_Admin' ) ) {
			Avision_Theme_Admin::init();
		}
	},
	5
);

if ( ! function_exists( 'avi_maybe_create_flag_svgs' ) ) {
	function avi_maybe_create_flag_svgs(): void {
		$done = (string) get_option( 'avi_flags_created', '' );
		if ( $done === '1' ) {
			return;
		}
		$dir = trailingslashit( plugin_dir_path( __FILE__ ) ) . 'assets/images/flags/';
		if ( ! is_dir( $dir ) ) {
			wp_mkdir_p( $dir );
		}
		$uk_flag = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30"><clipPath id="a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="b"><path d="M30 15h30v15zv15H0zH0V0zV0h30z"/></clipPath><g clip-path="url(#a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>';
		$fr_flag = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="1" height="2" fill="#002395"/><rect x="1" width="1" height="2" fill="#fff"/><rect x="2" width="1" height="2" fill="#ED2939"/></svg>';
		$es_flag = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><rect width="3" height="2" fill="#c60b1e"/><rect y=".5" width="3" height="1" fill="#ffc400"/></svg>';

		if ( ! file_exists( $dir . 'flag-en.svg' ) ) {
			@file_put_contents( $dir . 'flag-en.svg', $uk_flag );
		}
		if ( ! file_exists( $dir . 'flag-fr.svg' ) ) {
			@file_put_contents( $dir . 'flag-fr.svg', $fr_flag );
		}
		if ( ! file_exists( $dir . 'flag-es.svg' ) ) {
			@file_put_contents( $dir . 'flag-es.svg', $es_flag );
		}

		update_option( 'avi_flags_created', '1', false );
	}
}
add_action( 'init', 'avi_maybe_create_flag_svgs', 1 );

function avision_init() {
    // Force clean : forcer PHP à relire les fichiers (invalidation OPcache).
    if ( function_exists( 'opcache_reset' ) ) {
        $last_opcache_reset = get_transient( 'avi_opcache_last_reset' );
        if ( ! $last_opcache_reset ) {
            opcache_reset();
            set_transient( 'avi_opcache_last_reset', 1, HOUR_IN_SECONDS );
        }
    }
    // Nettoyage forcé : aucun ancien réglage ne doit polluer la génération (exorcisme texte/image).
    delete_option( 'avi_default_post_text' );
    delete_transient( 'avision_cold_start_lock_1' );

    if (class_exists('Avision\\Avision_Main')) {
        $avision = new Avision\Avision_Main();
        $avision->run();
    }
    if (class_exists('Avision\\Avision_Cron')) {
        new Avision\Avision_Cron();
    }
    if (class_exists('Avision\\Cron_Task_Manager')) {
        new Avision\Cron_Task_Manager();
    }
    add_shortcode( 'avision_audit_london_test', 'avision_render_local_audit_form_shortcode' );
    add_shortcode( 'avision_onboarding', 'avision_render_onboarding_shortcode' );
	if ( function_exists( 'avision_register_secure_dashboard_shortcodes' ) ) {
		avision_register_secure_dashboard_shortcodes();
	}
    // Clé OpenRouter : priorité à la constante (chargée depuis .env) sur get_option
    add_filter( 'avision_openrouter_api_key', function ( $value ) {
        if ( defined( 'AVISION_OPENROUTER_API_KEY' ) && AVISION_OPENROUTER_API_KEY !== '' ) {
            return AVISION_OPENROUTER_API_KEY;
        }
        return $value;
    }, 1 );
}

// Callbacks REST autonomes pour pending-posts / pending-reviews (éviter 404 si chargement Guardian_REST_API différé)
function avision_rest_pending_posts( \WP_REST_Request $request ) {
	$audit_id = (int) $request->get_param( 'audit_id' );
	if ( $audit_id <= 0 ) {
		return new \WP_REST_Response( [ 'success' => true, 'pending' => [], 'first_media_url' => null ], 200 );
	}
	global $wpdb;
	$table = $wpdb->prefix . 'avi_saas_posts';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
		return new \WP_REST_Response( [ 'success' => true, 'pending' => [], 'first_media_url' => null ], 200 );
	}
	$rows = $wpdb->get_results( $wpdb->prepare(
		"SELECT id, title, content, status, created_at, post_type FROM `{$table}` WHERE profile_id = %d AND status IN ('draft', 'pending_review') ORDER BY created_at DESC LIMIT 20",
		$audit_id
	), ARRAY_A );
	if ( ! is_array( $rows ) ) {
		$rows = [];
	}
	$profile_city = '';
	$t_profiles = $wpdb->prefix . 'avi_saas_profiles';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t_profiles ) ) === $t_profiles ) {
		$profile_row = $wpdb->get_row( $wpdb->prepare( "SELECT city FROM `{$t_profiles}` WHERE id = %d LIMIT 1", $audit_id ), ARRAY_A );
		if ( ! empty( $profile_row['city'] ) ) {
			$profile_city = (string) $profile_row['city'];
		}
	}
	$t_media = $wpdb->prefix . 'avi_saas_media';
	$first_media_url = null;
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t_media ) ) === $t_media ) {
		$first_media = $wpdb->get_row( $wpdb->prepare(
			"SELECT file_path FROM `{$t_media}` WHERE profile_id = %d AND status = 'in_stock' ORDER BY created_at DESC LIMIT 1",
			$audit_id
		), ARRAY_A );
		if ( ! empty( $first_media['file_path'] ) ) {
			$first_media_url = (string) $first_media['file_path'];
		}
		// Fallback : si des brouillons existent mais aucun média pour ce profil, prendre le dernier in_stock (Cold Start possible avec profil unique)
		if ( $first_media_url === null && count( $rows ) > 0 ) {
			$fallback = $wpdb->get_row(
				"SELECT file_path FROM `{$t_media}` WHERE status = 'in_stock' ORDER BY created_at DESC LIMIT 1",
				ARRAY_A
			);
			if ( ! empty( $fallback['file_path'] ) ) {
				$first_media_url = (string) $fallback['file_path'];
			}
		}
	}
	$cold_start_title = 'Présentation — Bienvenue';
	$pending = [];
	foreach ( $rows as $i => $r ) {
		$title   = isset( $r['title'] ) ? (string) $r['title'] : 'Post proposé';
		$content = isset( $r['content'] ) ? (string) $r['content'] : '';
		$ptype   = isset( $r['post_type'] ) ? (string) $r['post_type'] : 'update';
		$item = [
			'post_id'       => (int) ( $r['id'] ?? 0 ),
			'id'            => (int) ( $r['id'] ?? 0 ),
			'title'         => $title,
			'content'       => $content,
			'summary'       => wp_trim_words( $content, 30 ),
			'status'        => isset( $r['status'] ) ? (string) $r['status'] : 'draft',
			'created_at'    => isset( $r['created_at'] ) ? (string) $r['created_at'] : null,
			'is_cold_start' => ( $title === $cold_start_title ),
			'city'          => $profile_city,
			'post_type'     => $ptype,
		];
		if ( $ptype === 'testimonial' || ( strpos( $title, 'Témoignage Client' ) !== false ) ) {
			$item['is_testimonial']   = true;
			$item['rating']          = 5;
			$item['review_text']     = '';
			$item['reviewer_name']   = 'Client';
			$item['response_text']   = '';
			if ( preg_match( '/[«"]([^»"]+)[»"]/u', $content, $m ) ) {
				$item['review_text'] = trim( $m[1] );
			}
			if ( preg_match( '/—\s*([^\n.]+)/u', $title, $mName ) ) {
				$item['reviewer_name'] = trim( $mName[1], " \t\n\r." );
			} elseif ( preg_match( '/—\s*([^\n]+)/u', $content, $mName ) ) {
				$item['reviewer_name'] = trim( $mName[1], " \t\n\r." );
			}
			$response_block = $content;
			if ( preg_match( '/—\s*[^\n]+\s*\n\s*\n(.*)/us', $content, $mResp ) ) {
				$response_block = trim( $mResp[1] );
			}
			$item['response_text'] = preg_replace( '/\s*#\w+(\s*#\w+)*\s*$/u', '', $response_block );
			$item['response_text'] = trim( $item['response_text'] );
		}
		if ( $i === 0 && $first_media_url !== null ) {
			$item['image_url'] = $first_media_url;
		}
		$pending[] = $item;
	}
	return new \WP_REST_Response( [ 'success' => true, 'pending' => $pending, 'first_media_url' => $first_media_url, 'profile_city' => $profile_city ], 200 );
}

function avision_rest_pending_reviews( \WP_REST_Request $request ) {
	$audit_id = (int) $request->get_param( 'audit_id' );
	if ( $audit_id <= 0 ) {
		return new \WP_REST_Response( [ 'success' => true, 'pending' => [] ], 200 );
	}
	global $wpdb;
	$table = $wpdb->prefix . 'avi_review_responses';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) {
		$rows = $wpdb->get_results( $wpdb->prepare(
			"SELECT id, google_review_id, reviewer_name, rating, review_text, response_text, status, created_at FROM `{$table}` WHERE profile_id = %d AND status IN ('draft', 'pending_review', 'approved') ORDER BY created_at DESC LIMIT 50",
			$audit_id
		), ARRAY_A );
		if ( is_array( $rows ) && count( $rows ) > 0 ) {
			$pending = array_map( function ( $r ) {
				return [
					'review_id'      => isset( $r['google_review_id'] ) ? (string) $r['google_review_id'] : '',
					'reviewer_name'  => isset( $r['reviewer_name'] ) ? (string) $r['reviewer_name'] : '',
					'rating'         => (int) ( $r['rating'] ?? 0 ),
					'review_text'    => isset( $r['review_text'] ) ? (string) $r['review_text'] : '',
					'response_text'  => isset( $r['response_text'] ) ? (string) $r['response_text'] : '',
					'avi_justification' => '',
					'received_at'    => isset( $r['created_at'] ) ? (string) $r['created_at'] : null,
				];
			}, $rows );
			return new \WP_REST_Response( [ 'success' => true, 'pending' => $pending ], 200 );
		}
	}
	if ( class_exists( 'Avision\Guardian_REST_API' ) ) {
		$api = new Avision\Guardian_REST_API();
		return $api->get_pending_reviews( $request );
	}
	return new \WP_REST_Response( [ 'success' => true, 'pending' => [] ], 200 );
}

/** Archive un post (refus sans publication) : status → rejected. */
function avision_rest_post_archive( \WP_REST_Request $request ) {
	$audit_id  = (int) $request->get_param( 'audit_id' );
	$post_id   = (int) $request->get_param( 'post_id' );
	if ( $audit_id <= 0 || $post_id <= 0 ) {
		return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid parameters' ], 400 );
	}
	global $wpdb;
	$table = $wpdb->prefix . 'avi_saas_posts';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
		return new \WP_REST_Response( [ 'success' => false ], 404 );
	}
	$updated = $wpdb->update(
		$table,
		[ 'status' => 'rejected' ],
		[ 'id' => $post_id, 'profile_id' => $audit_id ],
		[ '%s' ],
		[ '%d', '%d' ]
	);
	return new \WP_REST_Response( [ 'success' => $updated !== false ], 200 );
}

/** Publie un post via le handler Guardian (Google Business Profile). */
function avision_rest_publish_post( \WP_REST_Request $request ) {
	if ( class_exists( 'Avision\Guardian_REST_API' ) ) {
		$api = new Avision\Guardian_REST_API();
		return $api->publish_post( $request );
	}
	return new \WP_REST_Response( [ 'success' => false, 'error' => 'guardian_api_unavailable' ], 503 );
}

/**
 * Synchronise les avis Google vers avi_review_responses (lance la Wave2 pour un profil).
 * Utile pour remonter les avis après un dépôt sur la fiche GBP (cron ou appel manuel).
 *
 * @param \WP_REST_Request $request audit_id (profile_id)
 * @return \WP_REST_Response { success, synced, error? }
 */
function avision_rest_sync_reviews( \WP_REST_Request $request ) {
	$audit_id = (int) $request->get_param( 'audit_id' );
	if ( $audit_id <= 0 ) {
		return new \WP_REST_Response( [ 'success' => false, 'synced' => 0, 'error' => 'audit_id requis' ], 400 );
	}
	global $wpdb;
	$table = $wpdb->prefix . 'avi_saas_profiles';
	$profile = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d LIMIT 1", $audit_id ), ARRAY_A );
	if ( ! $profile || empty( $profile['user_id'] ) ) {
		return new \WP_REST_Response( [ 'success' => false, 'synced' => 0, 'error' => 'Profil introuvable' ], 404 );
	}
	if ( ! class_exists( 'Avision\Avision_Guardian_Creators' ) ) {
		return new \WP_REST_Response( [ 'success' => false, 'synced' => 0, 'error' => 'Guardian_Creators indisponible' ], 500 );
	}
	$user_id = (int) $profile['user_id'];
	$creators = new Avision\Avision_Guardian_Creators();
	$wave2 = $creators->run_wave2( $user_id, $profile );
	$synced = (int) ( ( $wave2['proofs'] ?? 0 ) + ( $wave2['responses'] ?? 0 ) + ( $wave2['crises'] ?? 0 ) );
	return new \WP_REST_Response( [ 'success' => true, 'synced' => $synced, 'proofs' => $wave2['proofs'] ?? 0, 'responses' => $wave2['responses'] ?? 0, 'crises' => $wave2['crises'] ?? 0 ], 200 );
}

/**
 * Résout un profile_id depuis la requête REST (profile_id, audit_id ou pid UUID).
 *
 * @param \WP_REST_Request $request
 * @return int
 */
function avision_rest_extract_profile_id_from_request( \WP_REST_Request $request ) {
	// Certaines routes utilisent ?profile_id=, d'autres ?audit_id= (même valeur = avi_saas_profiles.id).
	$p_id = (int) $request->get_param( 'profile_id' );
	if ( $p_id <= 0 ) {
		$p_id = (int) $request->get_param( 'audit_id' );
	}
	if ( $p_id <= 0 ) {
		$p_pid = $request->get_param( 'pid' );
		if ( is_numeric( $p_pid ) && (int) $p_pid > 0 ) {
			$p_id = (int) $p_pid;
		}
	}
	if ( $p_id <= 0 && method_exists( $request, 'get_json_params' ) ) {
		$json = $request->get_json_params();
		if ( is_array( $json ) ) {
			if ( ! empty( $json['profile_id'] ) ) {
				$p_id = (int) $json['profile_id'];
			} elseif ( ! empty( $json['audit_id'] ) ) {
				$p_id = (int) $json['audit_id'];
			}
		}
	}
	$profile_id = $p_id;
	if ( $profile_id > 0 ) {
		return $profile_id;
	}

	$pid = $request->get_param( 'pid' );
	if ( is_string( $pid ) && preg_match( '/^[a-f0-9\-]{32,36}$/i', $pid ) && function_exists( 'avision_get_profile_id_by_uuid' ) ) {
		$resolved = (int) avision_get_profile_id_by_uuid( sanitize_text_field( $pid ) );
		if ( $resolved > 0 ) {
			return $resolved;
		}
	}
	return 0;
}

/**
 * Résout un avi_saas_profiles.id depuis les paramètres « visibles » en premier (profile_id, pid numérique ou UUID).
 * Utilisé pour le bypass REST rapport partagé avant tout le reste (y compris audit_id dans le corps JSON).
 *
 * @param \WP_REST_Request $request
 * @return int
 */
function avi_rest_quick_profile_id_from_primary_params( \WP_REST_Request $request ) {
	$raw = $request->get_param( 'profile_id' );
	if ( null === $raw || '' === (string) $raw ) {
		$raw = $request->get_param( 'pid' );
	}
	if ( is_numeric( $raw ) && (int) $raw > 0 ) {
		return absint( $raw );
	}
	if ( is_string( $raw ) && preg_match( '/^[a-f0-9\-]{32,36}$/i', trim( $raw ) ) && function_exists( 'avi_theme_resolve_profile_id_by_uuid' ) ) {
		return (int) avi_theme_resolve_profile_id_by_uuid( strtolower( trim( $raw ) ) );
	}
	return 0;
}

/**
 * Gate REST multi-tenant: autorise uniquement le propriétaire du profil (ou admin).
 * Si aucune cible profil n'est fournie, laisse passer (routes publiques onboarding/checkout, etc.).
 *
 *
 * @param \WP_REST_Request $request
 * @return bool|\WP_Error
 */
function avi_rest_check_profile_access( \WP_REST_Request $request ) {
	// TOUT EN PREMIER : lecture seule rapport /report/… (URI + Referer) — avant admin, extract complet, session, ACL MySQL.
	$quick_pid = avi_rest_quick_profile_id_from_primary_params( $request );
	if ( $quick_pid > 0 && function_exists( 'avi_premium_report_request_uri_or_referer_authorizes_profile' )
		&& avi_premium_report_request_uri_or_referer_authorizes_profile( $quick_pid ) ) {
		return true;
	}

	$profile_id = avision_rest_extract_profile_id_from_request( $request );

	// Même bypass si l’id n’était que dans audit_id / JSON (pas dans profile_id/pid en query).
	if ( $profile_id > 0 && function_exists( 'avi_premium_report_request_uri_or_referer_authorizes_profile' )
		&& avi_premium_report_request_uri_or_referer_authorizes_profile( $profile_id ) ) {
		return true;
	}

	if ( current_user_can( 'manage_options' ) ) {
		return true;
	}

	$route = method_exists( $request, 'get_route' ) ? (string) $request->get_route() : '';
	if ( $profile_id <= 0 ) {
		// Route publique ou sans cible profile explicite.
		return true;
	}

	// Vérification centralisée si disponible (source de vérité du projet).
	if ( function_exists( 'avi_current_user_can_access_profile' ) ) {
		if ( avi_current_user_can_access_profile( $profile_id ) ) {
			return true;
		}
		return new \WP_Error( 'forbidden_profile_access', 'Accès interdit à ce profil', [ 'status' => 403 ] );
	}

	// Repli : propriétaire WordPress (wp_user_id) si le thème expose le helper — typage entier strict.
	if ( function_exists( 'avi_mysql_profile_owner_ids_for_acl' ) && function_exists( 'get_current_user_id' ) ) {
		$acl_row             = avi_mysql_profile_owner_ids_for_acl( $profile_id );
		$profile_owner_wp_id = absint( $acl_row['wp_user_id'] ?? 0 );
		if ( $profile_owner_wp_id > 0 && $profile_owner_wp_id === absint( get_current_user_id() ) ) {
			return true;
		}
	}

	// Fallback strict via session SaaS.
	$session = function_exists( 'avision_validate_session' ) ? avision_validate_session() : null;
	$session_user_id = ( is_array( $session ) && isset( $session['user_id'] ) && '' !== (string) $session['user_id'] ) ? absint( $session['user_id'] ) : 0;
	if ( $session_user_id <= 0 ) {
		return new \WP_Error( 'forbidden_profile_access', 'Session invalide', [ 'status' => 403 ] );
	}

	global $wpdb;
	$table = $wpdb->prefix . 'avi_saas_profiles';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
		return new \WP_Error( 'forbidden_profile_access', 'Table profils indisponible', [ 'status' => 503 ] );
	}
	$owner_user_id = absint( $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM `{$table}` WHERE id = %d LIMIT 1", $profile_id ) ) );
	if ( $owner_user_id > 0 && $owner_user_id === absint( $session_user_id ) ) {
		return true;
	}

	return new \WP_Error( 'forbidden_profile_access', 'Accès interdit à ce profil', [ 'status' => 403 ] );
}

function avision_google_updates_proxy_handler( \WP_REST_Request $request ) {
	$status   = (string) $request->get_param( 'status' );
	$category = (string) $request->get_param( 'category' );
	$lang     = (string) $request->get_param( 'lang' );
	$audience = (string) $request->get_param( 'audience' );
	$limit    = (int) $request->get_param( 'limit' );

	$status   = $status !== '' ? sanitize_text_field( $status ) : 'injected';
	$category = $category !== '' ? sanitize_text_field( $category ) : '';
	$lang     = $lang !== '' ? sanitize_text_field( $lang ) : 'en';
	$audience = $audience !== '' ? sanitize_text_field( $audience ) : '';
	$limit    = $limit > 0 ? absint( $limit ) : 50;

	$url = add_query_arg(
		array(
			'status'   => $status,
			'category' => $category,
			'lang'     => $lang,
			'audience' => $audience,
			'limit'    => $limit,
		),
		'http://127.0.0.1:8001/api/v1/google-updates'
	);

	$response = wp_remote_get( $url, array( 'timeout' => 10 ) );
	if ( is_wp_error( $response ) ) {
		return new \WP_REST_Response( array( 'updates' => array(), 'total' => 0 ), 200 );
	}

	$body = (string) wp_remote_retrieve_body( $response );
	$data = json_decode( $body, true );
	if ( ! is_array( $data ) ) {
		return new \WP_REST_Response( array( 'updates' => array(), 'total' => 0 ), 200 );
	}

	return new \WP_REST_Response( $data, 200 );
}

function avision_register_google_updates_proxy_route() {
	register_rest_route(
		'avision/v1',
		'/google-updates-proxy',
		array(
			'methods'             => 'GET',
			'callback'            => 'avision_google_updates_proxy_handler',
			'permission_callback' => '__return_true',
		)
	);
}

function avision_rest_get_banners( $request ) {
	global $wpdb;
	$audience   = sanitize_text_field( $request->get_param( 'audience' ) ?: 'both' );
	$table      = $wpdb->prefix . 'avi_banners';
	$tbl_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
	if ( ! $tbl_exists ) {
		return rest_ensure_response( array( 'banners' => array() ) );
	}

	$rows = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT * FROM {$table}
             WHERE active = 1
             AND (audience = %s OR audience = 'both')
             ORDER BY id DESC",
			$audience
		),
		ARRAY_A
	);
	return rest_ensure_response( array( 'banners' => $rows ? $rows : array() ) );
}

function avision_register_banners_route() {
	register_rest_route(
		'avision/v1',
		'/banners',
		array(
			'methods'             => 'GET',
			'callback'            => 'avision_rest_get_banners',
			'permission_callback' => '__return_true',
		)
	);
}

// Enregistrer les routes REST API du Mode Gardien + audit-register (liaison formulaire local-seo → SaaS)
function avision_register_rest_routes() {
	if ( ! class_exists( '\\Avision\\Guardian_REST_API' ) ) {
		return;
	}
	// Enregistrer en premier les routes Centre d'intervention (pending-posts / pending-reviews) pour éviter 404
	register_rest_route( 'avision/v1', '/guardian/pending-posts', [
		'methods'             => 'GET',
		'callback'            => 'avision_rest_pending_posts',
		'permission_callback' => 'avi_rest_check_profile_access',
		'args'                => [ 'audit_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ] ],
	] );
	register_rest_route( 'avision/v1', '/guardian/pending-reviews', [
		'methods'             => 'GET',
		'callback'            => 'avision_rest_pending_reviews',
		'permission_callback' => 'avi_rest_check_profile_access',
		'args'                => [ 'audit_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ] ],
	] );
	register_rest_route( 'avision/v1', '/guardian/post-archive', [
		'methods'             => 'POST',
		'callback'            => 'avision_rest_post_archive',
		'permission_callback' => 'avi_rest_check_profile_access',
		'args'                => [
			'audit_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
			'post_id'  => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
		],
	] );
	register_rest_route( 'avision/v1', '/guardian/publish-post', [
		'methods'             => 'POST',
		'callback'            => 'avision_rest_publish_post',
		'permission_callback' => 'avi_rest_check_profile_access',
		'args'                => [
			'audit_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
			'post_id'  => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
		],
	] );
	register_rest_route( 'avision/v1', '/guardian/sync-reviews', [
		'methods'             => 'POST',
		'callback'            => 'avision_rest_sync_reviews',
		'permission_callback' => 'avi_rest_check_profile_access',
		'args'                => [ 'audit_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ] ],
	] );

	$guardian_api = new Avision\Guardian_REST_API();
	$guardian_api->register_routes();

	// Code Guard REST routes (admin).
	register_rest_route(
		'avision/v1',
		'/code-guard/metrics',
		array(
			'methods'             => 'GET',
			'callback'            => function () {
				if ( ! current_user_can( 'manage_options' ) ) {
					return new WP_Error( 'forbidden', 'Access denied', array( 'status' => 403 ) );
				}
				$output = shell_exec( 'sudo /root/scripts/file_metrics.sh 2>/dev/null' );
				$data   = json_decode( $output, true );
				return rest_ensure_response( $data ? $data : array() );
			},
			'permission_callback' => '__return_true',
		)
	);

    register_rest_route('avision/v1', '/audit-register', [
        'methods' => 'POST',
        'callback' => 'avision_rest_audit_register',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args' => [
            'email' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_email'],
            'business_name' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
            'city' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
            'category' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
            'neighborhood' => ['required' => false, 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
            'country' => ['required' => false, 'type' => 'string', 'default' => 'FR', 'sanitize_callback' => 'sanitize_text_field'],
            'language' => ['required' => false, 'type' => 'string', 'default' => 'en', 'sanitize_callback' => 'sanitize_text_field'],
            'score' => ['required' => false, 'type' => 'integer', 'default' => null],
        ],
    ]);

    register_rest_route( 'avision/v1', '/onboarding-save', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_onboarding_save',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'type'          => [ 'required' => true, 'type' => 'string', 'enum' => [ 'b2c', 'b2b' ], 'sanitize_callback' => 'sanitize_text_field' ],
            'email'         => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_email' ],
            'business_name' => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'city'          => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'country'       => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'neighborhood'  => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'category'      => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'agency_name'   => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'contact_name'  => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'password'         => [ 'required' => false, 'type' => 'string' ],
            'password_confirm' => [ 'required' => false, 'type' => 'string' ],
            'volume_fiches' => [ 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
        ],
    ] );

    register_rest_route( 'avision/v1', '/stripe-create-checkout-session', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_stripe_create_checkout_session',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'pre_token'        => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'email'            => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_email' ],
            'rid'              => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'report_id'        => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'addon_guardian'   => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'addon_radar'      => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'upgrade_type'     => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'profile_id'       => [ 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
            /** Champs formulaire onboarding — injectés dans Stripe metadata (priorité sur pré-inscription). */
            'business_name'    => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'category'         => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'city'             => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'country'          => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'neighborhood'     => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'phone'            => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'agency_name'      => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'contact_name'     => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
            'volume_fiches'    => [ 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
        ],
    ] );

    register_rest_route( 'avision/v1', '/connexion-verify', [
        'methods'             => 'GET',
        'callback'            => 'avision_rest_connexion_verify',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'session_id' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
        ],
    ] );

    register_rest_route( 'avision/v1', '/consume-first-login-welcome', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_consume_first_login_welcome',
        'permission_callback' => static function () {
            return function_exists( 'is_user_logged_in' ) && is_user_logged_in();
        },
    ] );

    register_rest_route( 'avision/v1', '/first-sync', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_first_sync',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'profile_id'      => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
            'pid'             => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
        ],
    ] );

    // Libère le navigateur immédiatement après envoi de "sync_started" (évite 502 / timeout proxy).
    add_filter( 'rest_pre_serve_request', function ( $served, $response, $request, $server ) {
        if ( ! $request || strpos( $request->get_route(), 'first-sync' ) === false ) {
            return $served;
        }
        if ( ! $response instanceof \WP_REST_Response ) {
            return $served;
        }
        $data = $response->get_data();
        $code = $response->get_status();
        if ( $code !== 200 || empty( $data['status'] ) || $data['status'] !== 'sync_started' ) {
            return $served;
        }
        if ( ! headers_sent() ) {
            header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) );
            status_header( $code );
        }
        echo wp_json_encode( $data );
        if ( function_exists( 'fastcgi_finish_request' ) ) {
            fastcgi_finish_request();
        }
        $profile_id = (int) $request->get_param( 'profile_id' );
        if ( $profile_id > 0 ) {
            error_log( '[Avision First Sync] After fastcgi_finish_request: starting run_initial_sync for profile_id=' . $profile_id );
            $sync = avi_run_initial_sync( $profile_id );
            if ( $sync === false ) {
                error_log( '[Avision First Sync] run_initial_sync failed for profile_id=' . $profile_id );
            } else {
                $calc = avi_run_visibility_calculation( $profile_id );
                error_log( '[Avision First Sync] Completed for profile_id=' . $profile_id . ', visibility_calc=' . ( $calc !== false ? 'ok' : 'fail' ) );
            }
        }
        return true;
    }, 10, 4 );

    register_rest_route( 'avision/v1', '/radar-keywords', [
        'methods'             => 'GET',
        'callback'            => 'avision_rest_radar_keywords',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'profile_id'     => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
            'limit'          => [ 'required' => false, 'type' => 'integer', 'default' => 75, 'sanitize_callback' => 'absint' ],
            'force'          => [ 'required' => false, 'type' => 'boolean', 'default' => false ],
            'pid'            => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
        ],
    ] );

    register_rest_route( 'avision/v1', '/radar-keywords-save', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_radar_keywords_save',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'profile_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
            'keywords'   => [ 'required' => true, 'type' => 'array' ],
        ],
    ] );

    register_rest_route( 'avision/v1', '/add-keywords-batch', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_add_keywords_batch',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'profile_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
            'keywords'   => [ 'required' => true, 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
        ],
    ] );

    register_rest_route( 'avision/v1', '/delete-keyword', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_delete_keyword',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'profile_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
            'keyword'    => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
        ],
    ] );

    // CRON : statut et dernier passage pour une fiche (admin ou propriétaire du profil)
    register_rest_route( 'avision/v1', '/cron-status', [
        'methods'             => 'GET',
        'callback'            => 'avision_rest_cron_status',
        'permission_callback' => 'avi_rest_check_profile_access',
        'args'                => [
            'profile_id' => [ 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
            'pid'        => [ 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
        ],
    ] );

    // CRON : lancer l'orchestrateur pour une fiche (admin uniquement)
    register_rest_route( 'avision/v1', '/cron-run-profile', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_cron_run_profile',
        'permission_callback' => function () { return current_user_can( 'manage_options' ); },
        'args'                => [
            'profile_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
        ],
    ] );

    // Scan de démo (même flux que admin.php?page=avi-install-core-tables&avi_demo_scan=1) — admin uniquement
    register_rest_route( 'avision/v1', '/demo-scan', [
        'methods'             => 'POST',
        'callback'            => 'avision_rest_demo_scan',
        'permission_callback' => function () { return current_user_can( 'manage_options' ); },
        'args'                => [
            'profile_id' => [ 'required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint' ],
        ],
    ] );
}

/**
 * Shortcode [avision_local_audit_form] : formulaire onboarding avec Quartier (pour pages en Snippet / shortcode).
 * Affiche le formulaire complet (business_name, city, neighborhood, category, email) et envoie vers audit-register.
 */
function avision_render_local_audit_form_shortcode() {
    ob_start();
    if ( file_exists( AVISION_PATH . 'templates/shortcode-local-audit-form.php' ) ) {
        echo "";
        include AVISION_PATH . 'templates/shortcode-local-audit-form.php';
    } else {
        echo '<p class="avision-error">Formulaire onboarding introuvable.</p>';
    }
    return ob_get_clean();
}

/**
 * Shortcode [avision_onboarding] : tunnels B2C et B2B (pré-inscription, liée au PID).
 * Affiche le template templates/onboarding.php.
 */
function avision_render_onboarding_shortcode() {
    ob_start();
    if ( file_exists( AVISION_PATH . 'templates/onboarding.php' ) ) {
        include AVISION_PATH . 'templates/onboarding.php';
    } else {
        echo '<p class="avision-error">Template onboarding introuvable.</p>';
    }
    return ob_get_clean();
}

add_action( 'wp_enqueue_scripts', 'avision_enqueue_local_audit_form_styles_when_shortcode' );
function avision_enqueue_local_audit_form_styles_when_shortcode() {
    if ( ! is_singular() ) {
        return;
    }
    $post = get_post();
    if ( ! $post || ( ! has_shortcode( $post->post_content, 'avision_local_audit_form' ) && ! has_shortcode( $post->post_content, 'avision_audit_london_test' ) ) ) {
        return;
    }
    $url = defined( 'AVISION_PLUGIN_URL' ) ? AVISION_PLUGIN_URL : plugin_dir_url( __FILE__ );
    wp_enqueue_style(
        'avision-local-audit-form',
        $url . 'assets/css/local-seo.css',
        array(),
        defined( 'AVISION_VERSION' ) ? AVISION_VERSION : '9.0.2'
    );
}

/**
 * Liaison formulaire audit local-seo → tables SaaS.
 * Crée ou récupère l'utilisateur (avi_saas_users), crée le profil (avi_saas_profiles),
 * crée une session (avi_saas_sessions) et définit le cookie avision_session_token.
 * Retourne sid pour redirection /demo-dashboard/?sid=token et promotion_expires_at.
 */
function avision_rest_audit_register(\WP_REST_Request $request) {
    if (!class_exists('Avision\Avision_Database')) {
        return new \WP_REST_Response(['success' => false, 'error' => 'Database unavailable'], 503);
    }
    $email = $request->get_param('email');
    $business_name = $request->get_param('business_name');
    $city = $request->get_param('city');
    $category = $request->get_param('category');
    $neighborhood = $request->get_param('neighborhood');
    $country = $request->get_param('country');
    $score = $request->get_param('score');
    if (empty($email) || !is_email($email)) {
        return new \WP_REST_Response(['success' => false, 'error' => 'invalid_email'], 400);
    }
    $db = new \Avision\Avision_Database();
    $db->init();
    $user_id = $db->get_or_create_saas_user($email);
    if ($user_id === null) {
        return new \WP_REST_Response(['success' => false, 'error' => 'user_create_failed'], 500);
    }
    $score_int = $score !== null && $score !== '' ? (int) $score : null;
    $neighborhood_str = $neighborhood !== null ? trim((string) $neighborhood) : '';
    $country_code = in_array(strtoupper(trim((string) $country)), ['FR', 'GB', 'US', 'ES'], true) ? strtoupper(trim((string) $country)) : 'FR';
    $profile = $db->create_saas_profile($user_id, $business_name, $city, $category, null, $score_int, $neighborhood_str, $country_code);
    if ($profile === null) {
        return new \WP_REST_Response(['success' => false, 'error' => 'profile_create_failed'], 500);
    }
    $phone_audit = $request->get_param( 'phone' );
    $phone_audit = is_string( $phone_audit ) ? sanitize_text_field( trim( $phone_audit ) ) : '';
    if ( $phone_audit !== '' && ! empty( $profile['id'] ) && isset( $GLOBALS['wpdb'] ) && $GLOBALS['wpdb'] ) {
        global $wpdb;
        $pid_audit = (int) $profile['id'];
        foreach ( array_unique( array( 'wor1515_avi_saas_profiles', $wpdb->prefix . 'avi_saas_profiles' ) ) as $tbl_audit ) {
            if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl_audit ) ) !== $tbl_audit ) {
                continue;
            }
            $row_ok = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$tbl_audit}` WHERE id = %d LIMIT 1", $pid_audit ) );
            if ( $row_ok !== $pid_audit ) {
                continue;
            }
            if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$tbl_audit}` LIKE %s", 'phone' ) ) === 'phone' ) {
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from liste validée SHOW TABLES.
                $wpdb->update( $tbl_audit, array( 'phone' => $phone_audit ), array( 'id' => $pid_audit ), array( '%s' ), array( '%d' ) );
            }
        }
    }
    if (function_exists('avi_generate_initial_keywords')) {
        avi_generate_initial_keywords($profile['id']);
    }
    $session = $db->create_saas_session($user_id, $profile['id'], 604800); // 7 jours
    if (!$session) {
        return new \WP_REST_Response(['success' => false, 'error' => 'session_create_failed'], 500);
    }
    $promotion_expires_at = isset($profile['promotion_expires_at']) ? $profile['promotion_expires_at'] : gmdate('Y-m-d\TH:i:s\Z', strtotime('+24 hours'));
    if (strpos($promotion_expires_at, 'T') === false && strpos($promotion_expires_at, ' ') !== false) {
        $promotion_expires_at = date('c', strtotime($promotion_expires_at . ' UTC'));
        if ($promotion_expires_at === false) {
            $promotion_expires_at = gmdate('c', strtotime('+24 hours'));
        }
    }
    $response = new \WP_REST_Response([
        'success' => true,
        'user_id' => $user_id,
        'profile_id' => $profile['id'],
        'current_level' => $profile['current_level'],
        'created_at' => $profile['created_at'],
        'promotion_expires_at' => $promotion_expires_at,
        'sid' => $session['token'],
    ], 200);
    $cookie_value = $session['token'];
    $max_age = 604800; // 7 jours
    $secure = is_ssl();
    $response->header('Set-Cookie', sprintf(
        'avision_session_token=%s; path=/; max-age=%d; SameSite=Lax; HttpOnly%s',
        rawurlencode($cookie_value),
        $max_age,
        $secure ? '; Secure' : ''
    ));
    return $response;
}

/**
 * Message d'erreur onboarding (bandeau) : priorité à l'erreur SQL brute ($wpdb->last_error).
 *
 * @param string $fallback Si last_error est vide (ex. code interne).
 * @return string
 */
function avision_onboarding_wpdb_error_msg( $fallback = '' ) {
    global $wpdb;
    $e = isset( $wpdb->last_error ) ? trim( (string) $wpdb->last_error ) : '';
    if ( $e !== '' ) {
        return 'Erreur : ' . $e;
    }
    $fb = is_string( $fallback ) ? trim( $fallback ) : '';
    if ( $fb !== '' ) {
        return 'Erreur : ' . $fb;
    }
    return 'Erreur : (aucun détail SQL)';
}

/**
 * Réponse d'erreur JSON pour le endpoint REST onboarding (ne pas utiliser wp_send_json_error ici).
 *
 * @param string               $message Message affichable.
 * @param int                  $status  Code HTTP.
 * @param array<string, mixed> $extra   Clés additionnelles (ghost, etc.).
 * @return \WP_REST_Response
 */
function avision_rest_onboarding_error_response( $message, $status = 400, $extra = array() ) {
    $data = array_merge(
        array(
            'success' => false,
        ),
        is_array( $extra ) ? $extra : array()
    );
    $msg = is_string( $message ) ? $message : '';
    if ( $msg !== '' ) {
        $data['message'] = $msg;
        $data['error']   = $msg;
    }
    return new \WP_REST_Response( $data, (int) $status );
}

/**
 * Supprime un utilisateur WordPress si besoin (REST onboarding — rollback).
 *
 * @param int $user_id ID wp_users.
 */
function avision_rest_onboarding_maybe_delete_wp_user( $user_id ) {
    $uid = (int) $user_id;
    if ( $uid <= 0 ) {
        return;
    }
    // Janitor strike (B2B test) : aucune suppression de compte WP.
    // if ( ! function_exists( 'wp_delete_user' ) ) {
    // require_once ABSPATH . 'wp-admin/includes/user.php';
    // }
    // if ( function_exists( 'wp_delete_user' ) ) {
    // wp_delete_user( $uid );
    // }
}

/**
 * wp_create_user : échec car l'e-mail est déjà enregistré dans WordPress.
 *
 * @param \WP_Error $wp_error Erreur retournée par wp_create_user.
 * @return bool
 */
function avision_rest_onboarding_is_wp_duplicate_email_error( $wp_error ) {
    if ( ! is_wp_error( $wp_error ) ) {
        return false;
    }
    if ( $wp_error->get_error_code() === 'existing_user_email' ) {
        return true;
    }
    $msg = strtolower( (string) $wp_error->get_error_message() );
    foreach ( array( 'already been registered', 'email already', 'existe déjà', 'déjà utilisé', 'déjà enregistré', 'already exists' ) as $needle ) {
        if ( $needle !== '' && strpos( $msg, $needle ) !== false ) {
            return true;
        }
    }
    return false;
}

/**
 * Supprime les lignes pré-inscription pour cet e-mail (délègue au thème si disponible).
 * Utilisé par l’onboarding REST pour ne pas dépendre du prochain wp_login.
 *
 * @param string $email E-mail normalisé.
 * @return int Lignes supprimées (approximatif).
 */
function avision_pre_inscription_delete_rows_for_email( $email ) {
    if ( function_exists( 'avi_pre_inscription_delete_rows_for_email' ) ) {
        return (int) avi_pre_inscription_delete_rows_for_email( $email );
    }
    global $wpdb;
    $email = trim( (string) $email );
    if ( $email === '' || ! is_email( $email ) ) {
        return 0;
    }
    $candidates = array();
    if ( defined( 'AVISION_PRE_INSCRIPTION_TABLE' ) && is_string( AVISION_PRE_INSCRIPTION_TABLE ) ) {
        $candidates[] = AVISION_PRE_INSCRIPTION_TABLE;
    }
    $candidates[] = 'wor1515_avi_pre_inscription';
    $candidates[] = $wpdb->prefix . 'avi_pre_inscription';
    $deleted      = 0;
    foreach ( array_unique( array_filter( $candidates ) ) as $tbl ) {
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) ) !== $tbl ) {
            continue;
        }
        $col = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$tbl}` LIKE %s", 'email' ) );
        if ( $col !== 'email' ) {
            continue;
        }
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nom de table whitelisté.
        $n = $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM `{$tbl}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s))",
                $email
            )
        );
        if ( false !== $n ) {
            $deleted += (int) $n;
        }
    }
    return $deleted;
}

/**
 * Bouclier anti-ghost-delete : bloque la suppression d'un profil fraîchement créé (< 5 minutes).
 *
 * @param string $profiles_table Table profils.
 * @param string $email          E-mail cible.
 * @return bool True si suppression bloquée.
 */
function avision_should_block_recent_profile_delete( $profiles_table, $email ) {
    global $wpdb;
    $profiles_table = is_string( $profiles_table ) ? trim( $profiles_table ) : '';
    $email          = trim( (string) $email );
    if ( $profiles_table === '' || $email === '' || ! is_email( $email ) ) {
        return false;
    }
    if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) !== $profiles_table ) {
        return false;
    }
    if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'created_at' ) ) !== 'created_at' ) {
        return false;
    }
    $created_at = $wpdb->get_var(
        $wpdb->prepare(
            "SELECT created_at FROM `{$profiles_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id DESC LIMIT 1",
            $email
        )
    );
    if ( ! empty( $created_at ) ) {
        $created_ts = strtotime( (string) $created_at );
        if ( $created_ts !== false && ( time() - $created_ts ) < 300 ) {
            if ( function_exists( 'error_log' ) ) {
                error_log( 'AVI SHIELD: Deletion blocked for ' . $email . ' (Created less than 5min ago)' );
            }
            return true;
        }
    }
    return false;
}

/**
 * Force la création/synchronisation d'un profil SaaS B2B pour un utilisateur WordPress.
 *
 * @param int    $wp_user_id    ID WordPress.
 * @param string $email         E-mail du compte.
 * @param string $business_name Nom affiché.
 * @param int    $volume_fiches Volume fiches.
 * @return bool
 */
function avision_force_b2b_saas_profile_row( $wp_user_id, $email, $business_name = '', $volume_fiches = 0 ) {
    global $wpdb;
    $wp_user_id = (int) $wp_user_id;
    $email      = sanitize_email( (string) $email );
    if ( $wp_user_id <= 0 || ! is_email( $email ) ) {
        return false;
    }
    $table = 'wor1515_avi_saas_profiles';
    if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
        return false;
    }
    $cols = $wpdb->get_col( "SHOW COLUMNS FROM `{$table}`", 0 );
    $cols = is_array( $cols ) ? $cols : array();
    $now  = current_time( 'mysql' );
    $bn   = trim( (string) $business_name );
    if ( $bn === '' ) {
        $u  = get_userdata( $wp_user_id );
        $bn = ( $u && ! empty( $u->display_name ) ) ? (string) $u->display_name : current( explode( '@', $email ) );
    }
    $update = array();
    if ( in_array( 'user_id', $cols, true ) ) { $update['user_id'] = $wp_user_id; }
    if ( in_array( 'email', $cols, true ) ) { $update['email'] = $email; }
    if ( in_array( 'user_type', $cols, true ) ) { $update['user_type'] = 'agency'; }
    if ( in_array( 'radar_plan', $cols, true ) ) { $update['radar_plan'] = 'agency_tier1'; }
    if ( in_array( 'business_name', $cols, true ) ) { $update['business_name'] = $bn; }
    if ( in_array( 'category', $cols, true ) ) { $update['category'] = 'agency'; }
    if ( in_array( 'city', $cols, true ) ) { $update['city'] = 'N/A'; }
    if ( in_array( 'neighborhood', $cols, true ) ) { $update['neighborhood'] = 'N/A'; }
    if ( in_array( 'updated_at', $cols, true ) ) { $update['updated_at'] = $now; }
    if ( in_array( 'created_at', $cols, true ) ) { $update['created_at'] = $now; }
    if ( $volume_fiches > 0 && in_array( 'max_keywords', $cols, true ) ) { $update['max_keywords'] = (int) $volume_fiches; }
    if ( in_array( 'uuid', $cols, true ) && function_exists( 'wp_generate_uuid4' ) ) { $update['uuid'] = wp_generate_uuid4(); }

    $existing_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$table}` WHERE user_id = %d ORDER BY id ASC LIMIT 1", $wp_user_id ) );
    if ( $existing_id <= 0 ) {
        $existing_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id ASC LIMIT 1", $email ) );
    }
    if ( $existing_id > 0 ) {
        $wpdb->update( $table, $update, array( 'id' => $existing_id ) );
    } else {
        $fmt = array();
        foreach ( $update as $val ) {
            $fmt[] = is_int( $val ) ? '%d' : '%s';
        }
        $wpdb->insert( $table, $update, $fmt );
    }

    $final_id = (int) $wpdb->get_var(
        $wpdb->prepare(
            "SELECT id FROM `{$table}` WHERE user_id = %d OR LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id ASC LIMIT 1",
            $wp_user_id,
            $email
        )
    );
    return $final_id > 0;
}

/**
 * Complète $insert_data / $insert_format pour les colonnes mw_user NOT NULL sans défaut (scanner structure).
 *
 * @param array<string,mixed> $insert_data
 * @param array<int,string>  $insert_format
 */
function avision_onboarding_mw_user_fill_not_null_columns( $wpdb, $mw_table, &$insert_data, &$insert_format, $username, $email, $plain_password, $first_name, $last_name, $mw_display_name, $user_uid_max_len ) {
    $mw_meta = $wpdb->get_results( "SHOW FULL COLUMNS FROM `{$mw_table}`", ARRAY_A );
    if ( ! is_array( $mw_meta ) ) {
        return;
    }
    foreach ( $mw_meta as $col ) {
        $field = isset( $col['Field'] ) ? (string) $col['Field'] : '';
        if ( $field === '' || array_key_exists( $field, $insert_data ) ) {
            continue;
        }
        if ( strtoupper( (string) ( $col['Null'] ?? '' ) ) !== 'NO' ) {
            continue;
        }
        $def = $col['Default'] ?? null;
        if ( $def !== null && $def !== '' ) {
            continue;
        }
        if ( stripos( (string) ( $col['Extra'] ?? '' ), 'auto_increment' ) !== false ) {
            continue;
        }
        $type = strtolower( (string) ( $col['Type'] ?? '' ) );
        $val  = null;
        $fmt  = '%s';

        if ( in_array( $field, array( 'username', 'user_login' ), true ) ) {
            $val = $username;
        } elseif ( $field === 'email' ) {
            $val = $email;
        } elseif ( in_array( $field, array( 'password', 'user_pass', 'passwd' ), true ) ) {
            $val = wp_hash_password( $plain_password );
        } elseif ( $field === 'user_uid' ) {
            $val = uniqid( 'avi_' );
            if ( $user_uid_max_len > 0 && strlen( $val ) > $user_uid_max_len ) {
                $val = substr( 'avi_' . bin2hex( random_bytes( 16 ) ), 0, $user_uid_max_len );
            }
        } elseif ( in_array( $field, array( 'date_added', 'created_at', 'date_created', 'updated_at' ), true ) ) {
            $val = current_time( 'mysql' );
        } elseif ( $field === 'status' ) {
            $val = 'active';
        } elseif ( $field === 'language_id' ) {
            $val = 1;
            $fmt = '%d';
        } elseif ( in_array( $field, array( 'display_name', 'name', 'full_name' ), true ) ) {
            $val = $mw_display_name;
        } elseif ( $field === 'first_name' ) {
            $val = $first_name;
        } elseif ( $field === 'last_name' ) {
            $val = $last_name;
        } elseif ( strpos( $type, 'int' ) !== false || strpos( $type, 'decimal' ) !== false || strpos( $type, 'float' ) !== false || strpos( $type, 'double' ) !== false || strpos( $type, 'bigint' ) !== false ) {
            $val = 0;
            $fmt = '%d';
        } elseif ( strpos( $type, 'date' ) !== false || strpos( $type, 'time' ) !== false ) {
            $val = current_time( 'mysql' );
        } else {
            $val = '';
        }

        $insert_data[ $field ] = $val;
        $insert_format[]       = $fmt;
    }
}

/**
 * Enregistre une pré-inscription B2C ou B2B en table wor1515_avi_pre_inscription (AVISION_PRE_INSCRIPTION_TABLE).
 * Retourne un token temporaire pour liaison ultérieure au PID.
 */
function avision_rest_onboarding_save( \WP_REST_Request $request ) {
    $params = function_exists( 'avision_rest_merge_request_params' ) ? avision_rest_merge_request_params( $request ) : $request->get_params();
    if ( ! is_array( $params ) ) {
        $params = array();
    }
    $type  = isset( $params['type'] ) ? sanitize_text_field( $params['type'] ) : $request->get_param( 'type' );
    $email = isset( $params['email'] ) ? sanitize_email( $params['email'] ) : $request->get_param( 'email' );
    if ( ! in_array( $type, [ 'b2c', 'b2b' ], true ) || empty( $email ) || ! is_email( $email ) ) {
        return new \WP_REST_Response( [ 'success' => false, 'error' => 'invalid_params' ], 400 );
    }
    $email = strtolower( trim( (string) $email ) );
    global $wpdb;

    /** Hash WP pour JSON `data` (tunnel B2C — pas de création de compte avant paiement). */
    $b2c_password_hash = null;

    // Tunnel B2C : mot de passe + anti-fantôme (mw_user / wor1515_avi_saas_profiles) — tout en pré-inscription uniquement.
    if ( $type === 'b2c' ) {
        $plain_password = isset( $params['password'] ) ? (string) $params['password'] : (string) $request->get_param( 'password' );
        $plain_confirm  = isset( $params['password_confirm'] ) ? (string) $params['password_confirm'] : (string) $request->get_param( 'password_confirm' );
        if ( $plain_password !== $plain_confirm ) {
            return avision_rest_onboarding_error_response(
                __( 'Les mots de passe ne correspondent pas.', 'avision-dashboard' ),
                400,
                array( 'code' => 'password_mismatch' )
            );
        }
        if ( strlen( $plain_password ) < 8 ) {
            return avision_rest_onboarding_error_response( 'password_too_short', 400, array( 'code' => 'password_too_short' ) );
        }
        $b2c_password_hash = wp_hash_password( $plain_password );

        $user_exists = $wpdb->get_var(
            $wpdb->prepare(
                'SELECT user_id FROM mw_user WHERE email = %s LIMIT 1',
                $email
            )
        );
        if ( $user_exists ) {
            $ghost_uid = (int) $user_exists;
            return new \WP_REST_Response(
                array(
                    'success' => false,
                    'message' => sprintf(
                        /* translators: %d: mw_user id */
                        __( 'Un compte existe déjà pour cet e-mail (mw_user ID: %d).', 'avision-dashboard' ),
                        $ghost_uid
                    ),
                    'ghost'    => 'mw_user',
                    'ghost_id' => $ghost_uid,
                ),
                409
            );
        }
        $saas_exists = $wpdb->get_var(
            $wpdb->prepare(
                'SELECT id FROM wor1515_avi_saas_profiles WHERE email = %s LIMIT 1',
                $email
            )
        );
        if ( $saas_exists ) {
            $ghost_pid = (int) $saas_exists;
            return new \WP_REST_Response(
                array(
                    'success' => false,
                    'message' => sprintf(
                        /* translators: %d: profile id */
                        __( 'Un profil existe déjà pour cet e-mail (ID: %d).', 'avision-dashboard' ),
                        $ghost_pid
                    ),
                    'ghost'    => 'wor1515_avi_saas_profiles',
                    'ghost_id' => $ghost_pid,
                ),
                409
            );
        }
    }

    /** ID utilisateur WordPress pour auto-login après inscription B2B réussie (0 si création WP échouée). */
    $avi_b2b_wp_for_cookie = 0;
    /** Rollback si échec après création B2B (mw_user + wor1515_avi_saas_profiles). */
    $b2b_rollback_mw_user_id   = 0;
    $b2b_rollback_profile_id     = 0;
    /** True uniquement si wp_create_user vient de créer un nouveau compte WP (rollback = supprimer ce user). */
    $b2b_wp_user_created_here = false;
    /** Table mw_user pour rollback / requêtes B2B (surchargée dans le bloc B2B si constante définie). */
    $b2b_mw_user_table = 'mw_user';

    // Tunnel B2B unifié : création immédiate compte WordPress + meta agence + profil SaaS.
    if ( $type === 'b2b' ) {
        $b2b_saas_profile_ready = false;
        $b2b_mw_user_table = ( defined( 'AVISION_MW_USER_TABLE' ) && is_string( AVISION_MW_USER_TABLE ) && AVISION_MW_USER_TABLE !== '' )
            ? AVISION_MW_USER_TABLE
            : 'mw_user';
        // Table métier SaaS : constante AVISION_SAAS_PROFILES_TABLE si définie, sinon wor1515_avi_saas_profiles.
        $b2b_profiles_table = ( defined( 'AVISION_SAAS_PROFILES_TABLE' ) && is_string( AVISION_SAAS_PROFILES_TABLE ) && AVISION_SAAS_PROFILES_TABLE !== '' )
            ? AVISION_SAAS_PROFILES_TABLE
            : 'wor1515_avi_saas_profiles';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $b2b_profiles_table ) ) !== $b2b_profiles_table ) {
            $b2b_profiles_table = 'wor1515_avi_saas_profiles';
            if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $b2b_profiles_table ) ) !== $b2b_profiles_table ) {
                $b2b_profiles_table = function_exists( 'avi_get_onboarding_profiles_table' ) ? avi_get_onboarding_profiles_table() : ( $wpdb->prefix . 'avi_saas_profiles' );
            }
        }
        $user_exists = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT user_id FROM `{$b2b_mw_user_table}` WHERE email = %s LIMIT 1",
                $email
            )
        );
        $saas_exists = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM `{$b2b_profiles_table}` WHERE email = %s LIMIT 1",
                $email
            )
        );
        if ( $user_exists ) {
            $found_user = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT user_id, email FROM `{$b2b_mw_user_table}` WHERE email = %s LIMIT 1",
                    $email
                )
            );
            $ghost_uid = ( $found_user && isset( $found_user->user_id ) ) ? (int) $found_user->user_id : (int) $user_exists;
            // Réponse REST 409 (wp_send_json_error coupe le cycle REST) — message exploitable en phpMyAdmin.
            return new \WP_REST_Response(
                [
                    'success' => false,
                    'message' => sprintf(
                        'Fantôme trouvé dans mw_user (ID: %d)',
                        $ghost_uid
                    ),
                    'ghost'    => 'mw_user',
                    'ghost_id' => $ghost_uid,
                ],
                409
            );
        }
        if ( $saas_exists ) {
            $found_saas = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT id, email FROM `{$b2b_profiles_table}` WHERE email = %s LIMIT 1",
                    $email
                )
            );
            $ghost_pid = ( $found_saas && isset( $found_saas->id ) ) ? (int) $found_saas->id : (int) $saas_exists;
            return new \WP_REST_Response(
                [
                    'success' => false,
                    'message' => sprintf(
                        'Fantôme trouvé dans SaaS_Profiles (ID: %d)',
                        $ghost_pid
                    ),
                    'ghost'    => 'wor1515_avi_saas_profiles',
                    'ghost_id' => $ghost_pid,
                ],
                409
            );
        }

        // Email absent de mw_user ET de wor1515_avi_saas_profiles : création chaînée sans faute —
        // 1) compte WordPress (si possible) + ligne mw_user (Identité), 2) wor1515_avi_saas_profiles (Métier) avec le même user_id que mw_user.

        $agency_name  = isset( $params['agency_name'] ) ? sanitize_text_field( (string) $params['agency_name'] ) : sanitize_text_field( (string) $request->get_param( 'agency_name' ) );
        $contact_name = isset( $params['contact_name'] ) ? sanitize_text_field( (string) $params['contact_name'] ) : sanitize_text_field( (string) $request->get_param( 'contact_name' ) );
        $agency_name  = trim( $agency_name );
        $contact_name = trim( $contact_name );
        $volume_fiches = isset( $params['volume_fiches'] ) ? absint( $params['volume_fiches'] ) : absint( $request->get_param( 'volume_fiches' ) );
        $plain_password = isset( $params['password'] ) ? (string) $params['password'] : (string) $request->get_param( 'password' );
        $plain_confirm  = isset( $params['password_confirm'] ) ? (string) $params['password_confirm'] : (string) $request->get_param( 'password_confirm' );
        if ( $plain_password !== $plain_confirm ) {
            return new \WP_REST_Response( [ 'success' => false, 'error' => 'Les mots de passe ne correspondent pas.' ], 400 );
        }
        $contact_parts = preg_split( '/\s+/', trim( $contact_name ) );
        $first_name = '';
        $last_name  = '';
        if ( is_array( $contact_parts ) && ! empty( $contact_parts ) ) {
            $first_name = (string) array_shift( $contact_parts );
            $last_name  = trim( implode( ' ', $contact_parts ) );
        }
        if ( strlen( $plain_password ) < 8 ) {
            return new \WP_REST_Response( [ 'success' => false, 'error' => 'password_too_short' ], 400 );
        }

        $username_base = sanitize_user( current( explode( '@', $email ) ), true );
        if ( $username_base === '' ) {
            $username_base = 'agency_user';
        }
        $username = $username_base;
        $suffix   = 1;
        while ( username_exists( $username ) ) {
            $suffix++;
            $username = $username_base . $suffix;
        }

        $mw_table    = $b2b_mw_user_table;
        $mw_columns  = $wpdb->get_col( "SHOW COLUMNS FROM `{$mw_table}`", 0 );
        $mw_columns  = is_array( $mw_columns ) ? $mw_columns : array();

        $user_uid_max_len = 0;
        if ( in_array( 'user_uid', $mw_columns, true ) ) {
            $uid_col_row = $wpdb->get_row( "SHOW FULL COLUMNS FROM `{$mw_table}` WHERE Field = 'user_uid'" );
            if ( $uid_col_row && ! empty( $uid_col_row->Type ) && preg_match( '/\((\d+)\)/', (string) $uid_col_row->Type, $uid_m ) ) {
                $user_uid_max_len = max( 1, (int) $uid_m[1] );
            }
        }

        /**
         * Insertion complète mw_user (connexion custom) : user_uid, email, hash WP, status, date, language_id, noms + colonnes NOT NULL détectées.
         *
         * @return array{user_id:int, sql_error:string}
         */
        $avi_insert_mw_user = function () use ( $wpdb, $mw_table, $mw_columns, $username, $email, $plain_password, $first_name, $last_name, $agency_name, $user_uid_max_len ) {
            $insert_data   = array();
            $insert_format = array();
            if ( in_array( 'username', $mw_columns, true ) ) {
                $insert_data['username'] = $username;
                $insert_format[]       = '%s';
            }
            if ( in_array( 'email', $mw_columns, true ) ) {
                $insert_data['email'] = $email;
                $insert_format[]      = '%s';
            }
            if ( in_array( 'password', $mw_columns, true ) ) {
                $insert_data['password'] = wp_hash_password( $plain_password );
                $insert_format[]         = '%s';
            }
            if ( in_array( 'user_uid', $mw_columns, true ) ) {
                $uid_val = uniqid( 'avi_' );
                if ( $user_uid_max_len > 0 && strlen( $uid_val ) > $user_uid_max_len ) {
                    $uid_val = substr( 'avi_' . bin2hex( random_bytes( 16 ) ), 0, $user_uid_max_len );
                }
                $insert_data['user_uid'] = $uid_val;
                $insert_format[]         = '%s';
            }
            if ( in_array( 'date_added', $mw_columns, true ) ) {
                $insert_data['date_added'] = current_time( 'mysql' );
                $insert_format[]           = '%s';
            }
            if ( in_array( 'status', $mw_columns, true ) ) {
                $insert_data['status'] = 'active';
                $insert_format[]       = '%s';
            }
            if ( in_array( 'language_id', $mw_columns, true ) ) {
                $insert_data['language_id'] = 1;
                $insert_format[]          = '%d';
            }
            if ( in_array( 'first_name', $mw_columns, true ) ) {
                $insert_data['first_name'] = $first_name;
                $insert_format[]           = '%s';
            }
            if ( in_array( 'last_name', $mw_columns, true ) ) {
                $insert_data['last_name'] = $last_name;
                $insert_format[]          = '%s';
            }
            $mw_display_name = $contact_name !== '' ? $contact_name : ( $agency_name !== '' ? $agency_name : $username );
            avision_onboarding_mw_user_fill_not_null_columns( $wpdb, $mw_table, $insert_data, $insert_format, $username, $email, $plain_password, $first_name, $last_name, $mw_display_name, $user_uid_max_len );
            if ( empty( $insert_data ) ) {
                return array( 'user_id' => 0, 'sql_error' => 'mw_user_no_insertable_columns' );
            }
            $ok = $wpdb->insert( $mw_table, $insert_data, $insert_format );
            if ( ! $ok ) {
                return array( 'user_id' => 0, 'sql_error' => $wpdb->last_error ? $wpdb->last_error : 'mw_user_insert_failed' );
            }
            if ( in_array( 'user_id', $mw_columns, true ) ) {
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nom de table contrôlé (AVISION_MW_USER_TABLE).
                $new_id = (int) $wpdb->get_var(
                    $wpdb->prepare(
                        "SELECT user_id FROM `{$mw_table}` WHERE email = %s ORDER BY user_id DESC LIMIT 1",
                        $email
                    )
                );
                return array( 'user_id' => $new_id, 'sql_error' => '' );
            }
            return array( 'user_id' => (int) $wpdb->insert_id, 'sql_error' => '' );
        };

        $wp_user_id  = 0;
        $mw_user_id  = 0;
        // Purge immédiate de pré-inscription avant la naissance WP (source de vérité nettoyée en amont).
        avision_pre_inscription_delete_rows_for_email( $email );
        $wp_created  = wp_create_user( $username, $plain_password, $email );
        if ( ! is_wp_error( $wp_created ) ) {
            $wp_user_id = (int) $wp_created;
            $wp_user_obj = get_userdata( $wp_user_id );
            if ( $wp_user_obj instanceof \WP_User ) {
                $wp_user_obj->set_role( 'avi_agency' );
            }
            $avi_b2b_wp_for_cookie = $wp_user_id;
            $b2b_wp_user_created_here = true;
            // Diagnostic B2B : table cible forcée (élimine tout doute avant INSERT).
            $profiles_table = 'wor1515_avi_saas_profiles';
            $business_name_insert = $agency_name !== '' ? $agency_name : ( $contact_name !== '' ? $contact_name : $username );
            $business_name_insert = trim( (string) $business_name_insert );
            if ( $business_name_insert === '' ) {
                $business_name_insert = 'N/A';
            }
            $manual_insert_data = array(
                'user_id'        => $wp_user_id,
                'email'          => $email,
                'business_name'  => $business_name_insert,
                'neighborhood'   => 'N/A',
                'radar_plan'     => 'agency_tier1',
                'current_level'  => 1,
                'uuid'           => function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : wp_generate_uuid(),
                'created_at'     => current_time( 'mysql' ),
                'updated_at'     => current_time( 'mysql' ),
            );
            $has_uuid_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'uuid' ) ) === 'uuid' );
            if ( ! $has_uuid_col ) {
                unset( $manual_insert_data['uuid'] );
            }
            $has_created_at_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'created_at' ) ) === 'created_at' );
            if ( ! $has_created_at_col ) {
                unset( $manual_insert_data['created_at'] );
            }
            $has_updated_at_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'updated_at' ) ) === 'updated_at' );
            if ( ! $has_updated_at_col ) {
                unset( $manual_insert_data['updated_at'] );
            }
            $has_neighborhood_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'neighborhood' ) ) === 'neighborhood' );
            if ( ! $has_neighborhood_col ) {
                unset( $manual_insert_data['neighborhood'] );
            }
            $has_current_level_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'current_level' ) ) === 'current_level' );
            if ( ! $has_current_level_col ) {
                unset( $manual_insert_data['current_level'] );
            }
            // Janitor : uniquement par email avant INSERT (pas par user_id — libère la ligne quel que soit l’ancien ID, évite duplicate entry).
            if ( ! avision_should_block_recent_profile_delete( $profiles_table, $email ) ) {
                $wpdb->query(
                    $wpdb->prepare(
                        "DELETE FROM `{$profiles_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s))",
                        $email
                    )
                );
            }
            // Béton test : table + valeurs par défaut au moment critique.
            $profiles_table = 'wor1515_avi_saas_profiles';
            if ( isset( $manual_insert_data['business_name'] ) && trim( (string) $manual_insert_data['business_name'] ) === '' ) {
                $manual_insert_data['business_name'] = 'Agency Test';
            }
            if ( isset( $manual_insert_data['neighborhood'] ) && trim( (string) $manual_insert_data['neighborhood'] ) === '' ) {
                $manual_insert_data['neighborhood'] = 'Default';
            }
            $manual_insert_format = array();
            foreach ( $manual_insert_data as $key => $val ) {
                $manual_insert_format[] = ( is_int( $val ) ) ? '%d' : '%s';
            }
            $manual_profile_inserted = $wpdb->insert( $profiles_table, $manual_insert_data, $manual_insert_format );
            if ( ! $manual_profile_inserted ) {
                $err_msg = $wpdb->last_error ? (string) $wpdb->last_error : 'wpdb->insert returned false';
                update_user_meta( $wp_user_id, 'avi_last_sql_error', $err_msg );
                if ( function_exists( 'error_log' ) ) {
                    error_log( 'AVI DB INSERT ERROR: ' . $wpdb->last_error );
                }
                wp_die( 'SQL ERROR: ' . $err_msg );
            }
            update_user_meta( $wp_user_id, 'avi_last_sql_success', 'Profile created in ' . $profiles_table );
            $b2b_saas_profile_ready = true;
            update_user_meta( $wp_user_id, 'is_agency_master', '1' );
            if ( function_exists( 'wp_set_current_user' ) && function_exists( 'wp_set_auth_cookie' ) ) {
                wp_set_current_user( $wp_user_id );
                wp_set_auth_cookie( $wp_user_id, true, '' );
            }
            if ( function_exists( 'error_log' ) ) {
                error_log( 'AVI BIRTH: User created with ID ' . $wp_user_id . '. SaaS Profile created: ' . ( $b2b_saas_profile_ready ? 'YES' : 'NO' ) );
            }
            $mw_user_id = (int) $wpdb->get_var(
                $wpdb->prepare( "SELECT user_id FROM `{$mw_table}` WHERE email = %s LIMIT 1", $email )
            );
            if ( $mw_user_id <= 0 ) {
                $mw_ins = $avi_insert_mw_user();
                $mw_user_id = (int) $mw_ins['user_id'];
                if ( $mw_user_id <= 0 ) {
                    error_log( 'AVI ERROR: mw_user insert after wp_create_user failed. SQL: ' . $mw_ins['sql_error'] );
                    // avision_rest_onboarding_maybe_delete_wp_user( $wp_user_id );
                    return avision_rest_onboarding_error_response( avision_onboarding_wpdb_error_msg( $mw_ins['sql_error'] ), 400 );
                }
            }
        } elseif ( avision_rest_onboarding_is_wp_duplicate_email_error( $wp_created ) ) {
            // E-mail déjà présent dans wp_users : on ne bloque pas — connexion si le mot de passe est bon + mw_user manuel si besoin.
            $existing = get_user_by( 'email', $email );
            if ( $existing ) {
                if ( ! wp_check_password( $plain_password, $existing->user_pass, $existing->ID ) ) {
                    return avision_rest_onboarding_error_response(
                        __( 'Un compte existe déjà avec cet e-mail. Mot de passe incorrect.', 'avision-dashboard' ),
                        403
                    );
                }
                $wp_user_id = (int) $existing->ID;
                $avi_b2b_wp_for_cookie = $wp_user_id;
                update_user_meta( $wp_user_id, 'is_agency_master', '1' );
                $bn_for_force = $agency_name !== '' ? $agency_name : ( $contact_name !== '' ? $contact_name : $username );
                $b2b_saas_profile_ready = avision_force_b2b_saas_profile_row( $wp_user_id, $email, $bn_for_force, $volume_fiches );
                if ( function_exists( 'wp_set_current_user' ) && function_exists( 'wp_set_auth_cookie' ) ) {
                    wp_set_current_user( $wp_user_id );
                    wp_set_auth_cookie( $wp_user_id, true, '' );
                }
                if ( function_exists( 'error_log' ) ) {
                    error_log( 'AVI BIRTH: User created with ID ' . $wp_user_id . '. SaaS Profile created: ' . ( $b2b_saas_profile_ready ? 'YES' : 'NO' ) );
                }
                $mw_user_id = (int) $wpdb->get_var(
                    $wpdb->prepare( "SELECT user_id FROM `{$mw_table}` WHERE email = %s LIMIT 1", $email )
                );
                if ( $mw_user_id <= 0 ) {
                    $mw_ins = $avi_insert_mw_user();
                    $mw_user_id = (int) $mw_ins['user_id'];
                    if ( $mw_user_id <= 0 ) {
                        return avision_rest_onboarding_error_response( avision_onboarding_wpdb_error_msg( $mw_ins['sql_error'] ), 400 );
                    }
                }
            } else {
                $wp_error_message = $wp_created->get_error_message();
                error_log( 'AVI ERROR: duplicate email reported but get_user_by empty. ' . $wp_error_message );
                $mw_ins     = $avi_insert_mw_user();
                $mw_user_id = (int) $mw_ins['user_id'];
                if ( $mw_user_id <= 0 ) {
                    return avision_rest_onboarding_error_response( avision_onboarding_wpdb_error_msg( $mw_ins['sql_error'] ) . ' | WP : ' . $wp_error_message, 400 );
                }
            }
        } else {
            $wp_error_message = $wp_created->get_error_message();
            error_log( 'AVI ERROR: User creation failed. Message: ' . $wp_error_message );
            $mw_ins     = $avi_insert_mw_user();
            $mw_user_id = (int) $mw_ins['user_id'];
            if ( $mw_user_id <= 0 ) {
                return avision_rest_onboarding_error_response( avision_onboarding_wpdb_error_msg( $mw_ins['sql_error'] ) . ' | WP : ' . $wp_error_message, 400 );
            }
        }

        if ( $mw_user_id <= 0 ) {
            return avision_rest_onboarding_error_response( avision_onboarding_wpdb_error_msg( 'mw_user_id_missing' ), 400 );
        }

        $profiles_table = 'wor1515_avi_saas_profiles';
        $has_radar_plan = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'radar_plan' ) ) === 'radar_plan' );
        $has_uuid_col   = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'uuid' ) ) === 'uuid' );
        // Session WordPress : lier le profil SaaS à l’ID wp_users (sinon mw_user seul pour parcours sans compte WP).
        $saas_profile_user_id = ( $wp_user_id > 0 ) ? (int) $wp_user_id : (int) $mw_user_id;
        // Libellé affiché + e-mail : insérés ensemble pour le dashboard / AJAX (évite profil « sans nom »).
        $business_name_insert = $agency_name !== '' ? $agency_name : ( $contact_name !== '' ? $contact_name : $username );
        $business_name_insert = trim( (string) $business_name_insert );
        if ( $business_name_insert === '' ) {
            $business_name_insert = 'N/A';
        }
        $insert_data = [
            'user_id'       => $saas_profile_user_id,
            'business_name' => $business_name_insert,
            'city'          => 'N/A',
            'neighborhood'  => 'N/A',
            'category'      => 'agency',
            'email'         => $email,
        ];
        $insert_format = [ '%d', '%s', '%s', '%s', '%s', '%s' ];
        if ( $has_radar_plan ) {
            $insert_data['radar_plan'] = 'agency_tier1';
            $insert_format[] = '%s';
        }
        if ( $has_uuid_col && function_exists( 'wp_generate_uuid4' ) ) {
            $insert_data['uuid'] = wp_generate_uuid4();
            $insert_format[]   = '%s';
        }
        if ( $volume_fiches > 0 && $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'max_keywords' ) ) === 'max_keywords' ) {
            $insert_data['max_keywords'] = $volume_fiches;
            $insert_format[] = '%d';
        }

        if ( ! $b2b_saas_profile_ready ) {
            $profiles_table = 'wor1515_avi_saas_profiles';
            if ( isset( $insert_data['business_name'] ) && trim( (string) $insert_data['business_name'] ) === '' ) {
                $insert_data['business_name'] = 'Agency Test';
            }
            if ( isset( $insert_data['neighborhood'] ) && trim( (string) $insert_data['neighborhood'] ) === '' ) {
                $insert_data['neighborhood'] = 'Default';
            }
            // Même janitor email-only qu’au premier INSERT B2B (évite duplicate entry sur wor1515).
            if ( ! avision_should_block_recent_profile_delete( $profiles_table, $email ) ) {
                $wpdb->query(
                    $wpdb->prepare(
                        "DELETE FROM `{$profiles_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s))",
                        $email
                    )
                );
            }
            $profile_inserted = $wpdb->insert( $profiles_table, $insert_data, $insert_format );
            if ( ! $profile_inserted ) {
                // TEMP diagnostic B2B : pas de suppression mw_user.
                // $wpdb->delete( $mw_table, array( 'user_id' => $mw_user_id ), array( '%d' ) );
                $ins_err = $wpdb->last_error ? (string) $wpdb->last_error : 'wpdb->insert returned false';
                if ( $wp_user_id > 0 ) {
                    update_user_meta( $wp_user_id, 'avi_last_sql_error', $ins_err );
                }
                wp_die( 'SQL ERROR: ' . $ins_err );
            }
            $b2b_rollback_profile_id = (int) $wpdb->insert_id;
        } else {
            $b2b_rollback_profile_id = (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT id FROM `{$profiles_table}` WHERE user_id = %d OR LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id ASC LIMIT 1",
                    (int) $saas_profile_user_id,
                    $email
                )
            );
        }
        $b2b_rollback_mw_user_id = (int) $mw_user_id;

        // Janitor immédiat : plus de salle d’attente pour cet e-mail (évite d’attendre wp_login).
        avision_pre_inscription_delete_rows_for_email( $email );
        if ( function_exists( 'error_log' ) ) {
            error_log( 'AVI SUCCESS: User ' . $email . ' created and pre-inscription cleared.' );
        }
    }

    $table = AVISION_PRE_INSCRIPTION_TABLE;
    $get = function( $key ) use ( $request, $params ) {
        return isset( $params[ $key ] ) ? $params[ $key ] : $request->get_param( $key );
    };
    $temp_token   = bin2hex( random_bytes( 32 ) );
    $business_name = $type === 'b2c' ? $get( 'business_name' ) : null;
    $city         = $type === 'b2c' ? $get( 'city' ) : null;
    $country      = $type === 'b2c' ? $get( 'country' ) : null;
    $neighborhood = $type === 'b2c' ? $get( 'neighborhood' ) : null;
    $category     = $type === 'b2c' ? $get( 'category' ) : null;
    $agency_name  = $type === 'b2b' ? $get( 'agency_name' ) : null;
    $contact_name = $type === 'b2b' ? $get( 'contact_name' ) : null;
    $volume_fiches = $type === 'b2b' ? (int) $get( 'volume_fiches' ) : null;

    // Schéma wor1515 : id, email, token, data — tout le reste en JSON dans `data`.
    $data_to_store = array(
        'type' => $type,
    );
    if ( $type === 'b2c' ) {
        $data_to_store['business_name'] = $business_name ? sanitize_text_field( $business_name ) : '';
        $data_to_store['city']           = $city ? sanitize_text_field( $city ) : '';
        $data_to_store['country']        = $country ? sanitize_text_field( $country ) : '';
        $data_to_store['neighborhood']   = $neighborhood ? sanitize_text_field( $neighborhood ) : '';
        $data_to_store['category']       = $category ? sanitize_text_field( $category ) : '';
        $phone_pre = $get( 'phone' );
        if ( $phone_pre !== null && $phone_pre !== '' ) {
            $data_to_store['phone'] = sanitize_text_field( (string) $phone_pre );
        }
        $score_pre = isset( $params['score'] ) ? (int) $params['score'] : (int) $request->get_param( 'score' );
        if ( $score_pre > 0 ) {
            $data_to_store['score'] = $score_pre;
        }
        $lang_pre = isset( $params['language'] ) ? sanitize_text_field( (string) $params['language'] ) : sanitize_text_field( (string) $request->get_param( 'language' ) );
        if ( $lang_pre !== '' ) {
            $data_to_store['language'] = $lang_pre;
        }
        if ( $b2c_password_hash !== null ) {
            $data_to_store['password_hash'] = $b2c_password_hash;
        }
    } else {
        $data_to_store['agency_name']   = $agency_name ? sanitize_text_field( $agency_name ) : '';
        $data_to_store['contact_name']  = $contact_name ? sanitize_text_field( $contact_name ) : '';
        $data_to_store['volume_fiches'] = $volume_fiches > 0 ? $volume_fiches : null;
        $plain_for_json = isset( $params['password'] ) ? (string) $params['password'] : (string) $request->get_param( 'password' );
        if ( $plain_for_json !== '' ) {
            $data_to_store['password'] = $plain_for_json;
        }
    }
    $data_json = wp_json_encode( $data_to_store, JSON_UNESCAPED_UNICODE );

    // ═══════════════════════════════════════════════════════════════
    // ANTI-RÉGRESSION — colonnes dédiées pré-inscription B2C :
    // neighborhood, city, category DOIVENT être sauvés en colonnes
    // dédiées EN PLUS du JSON data — le webhook les lit directement
    // via $meta->neighborhood (class-avision-stripe-webhook.php L317)
    // et via report_meta (L328). Sans ça, neighborhood = NULL →
    // keywords initiaux sans quartier → Local SEO incomplet.
    // NE JAMAIS supprimer ces colonnes de l'INSERT.
    // AUTORISATION ANTHONY REQUISE POUR TOUTE MODIFICATION.
    // ═══════════════════════════════════════════════════════════════
    $insert_row    = array(
        'email' => sanitize_email( $email ),
        'token' => $temp_token,
        'data'  => $data_json,
    );
    $insert_format = array( '%s', '%s', '%s' );
    if ( $type === 'b2c' ) {
        if ( ! empty( $city ) ) {
            $insert_row['city']         = sanitize_text_field( $city );
            $insert_format[]            = '%s';
        }
        if ( ! empty( $neighborhood ) ) {
            $insert_row['neighborhood'] = sanitize_text_field( $neighborhood );
            $insert_format[]            = '%s';
        }
        if ( ! empty( $category ) ) {
            $insert_row['category']     = sanitize_text_field( $category );
            $insert_format[]            = '%s';
        }
        // ANTI-RÉGRESSION — language pré-inscription :
        // Colonne language obligatoire pour transmettre fr/en/es au profil.
        // Source : sélecteur langue nav (drapeaux) → param ?lang= ou ?language=
        // NE JAMAIS supprimer — sans language, le profil reste en 'fr' par défaut.
        $lang_insert = '';
        if ( ! empty( $lang_pre ) && in_array( $lang_pre, array( 'fr', 'en', 'es' ), true ) ) {
            $lang_insert = $lang_pre;
        } elseif ( isset( $params['lang'] ) && in_array( $params['lang'], array( 'fr', 'en', 'es' ), true ) ) {
            $lang_insert = sanitize_text_field( $params['lang'] );
        }
        if ( $lang_insert !== '' ) {
            $insert_row['language'] = $lang_insert;
            $insert_format[]        = '%s';
        }
    }
    $inserted = $wpdb->insert( $table, $insert_row, $insert_format );
    if ( ! $inserted ) {
        if ( $type === 'b2b' && $b2b_rollback_profile_id > 0 ) {
                // TEMP diagnostic B2B : rollback profil désactivé.
                // $rollback_profiles_table = ( isset( $b2b_profiles_table ) && is_string( $b2b_profiles_table ) && $b2b_profiles_table !== '' ) ? $b2b_profiles_table : 'wor1515_avi_saas_profiles';
                // $wpdb->delete( $rollback_profiles_table, array( 'id' => $b2b_rollback_profile_id ), array( '%d' ) );
        }
        if ( $type === 'b2b' && $b2b_rollback_mw_user_id > 0 ) {
            // TEMP diagnostic B2B : rollback mw_user désactivé.
            // $wpdb->delete( $b2b_mw_user_table, array( 'user_id' => $b2b_rollback_mw_user_id ), array( '%d' ) );
        }
        if ( $type === 'b2b' && $avi_b2b_wp_for_cookie > 0 && $b2b_wp_user_created_here ) {
            // avision_rest_onboarding_maybe_delete_wp_user( $avi_b2b_wp_for_cookie );
        }
        $sql_err = isset( $wpdb->last_error ) ? trim( (string) $wpdb->last_error ) : '';
        $fail_msg = ( $sql_err !== '' ) ? $sql_err : avision_onboarding_wpdb_error_msg( 'avi_pre_inscription_insert_failed' );
        return avision_rest_onboarding_error_response(
            $fail_msg,
            400,
            array(
                'code'      => 'avi_pre_inscription_insert_failed',
                'sql_error' => $sql_err,
            )
        );
    }
    // B2B sans Stripe : supprimer la ligne pré-inscription tout juste insérée (l’auth repose sur le cookie WP ; pas besoin de salle d’attente).
    if ( $type === 'b2b' && $b2b_rollback_profile_id > 0 ) {
        $stripe_ok = function_exists( 'avision_stripe_secret_key_configured' ) && avision_stripe_secret_key_configured();
        if ( ! $stripe_ok ) {
            avision_pre_inscription_delete_rows_for_email( $email );
        }
    }
    if ( function_exists( 'error_log' ) ) {
        error_log( 'AVI SUCCESS: Pre-inscription stored for ' . $email );
    }

    // B2B : métas agence + type de compte AVANT cookie et URL (sinon le « Portier » thème peut traiter l’utilisateur comme premium).
    if ( $type === 'b2b' && $avi_b2b_wp_for_cookie > 0 ) {
        $uid_b2b = (int) $avi_b2b_wp_for_cookie;
        update_user_meta( $uid_b2b, 'is_agency_master', '1' );
        update_user_meta( $uid_b2b, 'avi_user_type', 'b2b' );
        if ( function_exists( 'avi_set_account_type' ) ) {
            avi_set_account_type( $uid_b2b, 'agency' );
        } else {
            update_user_meta( $uid_b2b, 'avi_account_type', 'agency' );
        }
        if ( function_exists( 'clean_user_cache' ) ) {
            clean_user_cache( $uid_b2b );
        }
    }

    // Auto-login B2B : l’ID est celui créé / retrouvé pour cet e-mail (pas un ID fixe).
    if ( $type === 'b2b' && $avi_b2b_wp_for_cookie > 0 && function_exists( 'wp_set_current_user' ) && function_exists( 'wp_set_auth_cookie' ) ) {
        wp_set_current_user( (int) $avi_b2b_wp_for_cookie );
        wp_set_auth_cookie( (int) $avi_b2b_wp_for_cookie, true, '' );
    }
    $response_payload = array(
        'success'             => true,
        'token'               => $temp_token,
        'pre_token'           => $temp_token,
        /** Après pré-inscription : le front appelle POST stripe-create-checkout-session puis redirige vers Stripe. */
        'stripe_checkout'     => true,
        'checkout_create_url' => function_exists( 'rest_url' ) ? rest_url( 'avision/v1/stripe-create-checkout-session' ) : home_url( '/wp-json/avision/v1/stripe-create-checkout-session' ),
    );
    // Sans clé Stripe : mode test / dev — redirection directe vers le dashboard (comportement précédent).
    if ( ! function_exists( 'avision_stripe_secret_key_configured' ) || ! avision_stripe_secret_key_configured() ) {
        if ( $type === 'b2b' ) {
            // URL absolue : onboarding + pre en une fois (évite ?…? ou perte de param selon home_url / slash final).
            $agency_dash = trailingslashit( home_url( '/agency-dashboard' ) );
            $response_payload['redirect'] = add_query_arg(
                array(
                    'onboarding' => 'success',
                    'pre'        => $temp_token,
                ),
                $agency_dash
            );
        } else {
            $premium_dash = trailingslashit( home_url( '/premium-dashboard' ) );
            $response_payload['redirect'] = add_query_arg(
                array(
                    'onboarding' => 'success',
                    'pre'        => $temp_token,
                ),
                $premium_dash
            );
        }
        $response_payload['stripe_skipped']       = true;
        $response_payload['stripe_skip_reason']    = 'stripe_not_configured';
    }
    if ( $type === 'b2b' && $avi_b2b_wp_for_cookie > 0 ) {
        $wp_user_id = (int) $avi_b2b_wp_for_cookie;
        $wor_table    = 'wor1515_avi_saas_profiles';
        $count        = 0;
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wor_table ) ) === $wor_table ) {
            $count = (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT COUNT(1) FROM `{$wor_table}` WHERE LOWER(TRIM(COALESCE(email,''))) = %s",
                    $email
                )
            );
        }
        update_user_meta( $wp_user_id, 'avi_final_presence_check', (string) $count );
        if ( function_exists( 'error_log' ) ) {
            error_log( 'AVI CHECK: Count for ' . $email . ' is ' . $count . ' just before redirect.' );
        }
        update_user_meta( $wp_user_id, 'avi_final_step_reached', 'yes' );
    }
    return new \WP_REST_Response( $response_payload, 200 );
}

/**
 * Fusionne une ligne wor1515 (email, token, data JSON) pour les consommateurs (Stripe, etc.).
 *
 * @param array<string,mixed> $row Ligne brute SQL.
 * @return array<string,mixed>
 */
function avision_pre_inscription_row_merged( array $row ) {
    $decoded = array();
    if ( isset( $row['data'] ) && $row['data'] !== '' && $row['data'] !== null ) {
        $decoded = json_decode( (string) $row['data'], true );
        if ( ! is_array( $decoded ) ) {
            $decoded = array();
        }
    }
    $tok = isset( $row['token'] ) ? (string) $row['token'] : ( isset( $row['temp_token'] ) ? (string) $row['temp_token'] : '' );
    $base = array(
        'email'      => isset( $row['email'] ) ? (string) $row['email'] : '',
        'temp_token' => $tok,
    );
    // ANTI-RÉGRESSION — colonnes dédiées prioritaires sur JSON data :
    // neighborhood, city, category, language sont en colonnes SQL dédiées
    // ET dans le JSON data. Les colonnes SQL sont plus fiables (pas de
    // troncature JSON, pas de perte de données). Priorité : colonne > JSON.
    // NE JAMAIS supprimer cette logique — garantit neighborhood + language.
    $dedicated = array();
    foreach ( array( 'neighborhood', 'city', 'country', 'category', 'language', 'business_name', 'type' ) as $_col ) {
        if ( isset( $row[ $_col ] ) && (string) $row[ $_col ] !== '' ) {
            $dedicated[ $_col ] = (string) $row[ $_col ];
        }
    }
    return array_merge( $base, $decoded, $dedicated );
}

/**
 * Enregistre profile_id dans le JSON `data` pour la dernière pré-inscription de cet e-mail (webhook Stripe).
 *
 * @param string $email      E-mail.
 * @param int    $profile_id ID profil SaaS.
 * @return bool
 */
function avision_merge_pre_inscription_profile_id_by_email( $email, $profile_id ) {
    if ( ! is_email( $email ) || (int) $profile_id <= 0 ) {
        return false;
    }
    global $wpdb;
    $table = AVISION_PRE_INSCRIPTION_TABLE;
    $row   = $wpdb->get_row(
        $wpdb->prepare(
            "SELECT id, data FROM `{$table}` WHERE email = %s ORDER BY id DESC LIMIT 1",
            sanitize_email( $email )
        ),
        ARRAY_A
    );
    if ( ! is_array( $row ) || empty( $row['id'] ) ) {
        return false;
    }
    $data = json_decode( isset( $row['data'] ) ? (string) $row['data'] : '{}', true );
    if ( ! is_array( $data ) ) {
        $data = array();
    }
    $data['profile_id'] = (int) $profile_id;
    $json               = wp_json_encode( $data, JSON_UNESCAPED_UNICODE );
    $updated            = $wpdb->update(
        $table,
        array( 'data' => $json ),
        array( 'id' => (int) $row['id'] ),
        array( '%s' ),
        array( '%d' )
    );
    return $updated !== false && $updated > 0;
}

/**
 * Lie une pré-inscription (temp_token) au profile_id (PID) après création du profil.
 * À appeler après create_saas_profile quand l'inscription vient du tunnel onboarding (param pre=).
 *
 * @param string $temp_token Token retourné par onboarding-save.
 * @param int    $profile_id ID du profil (avi_saas_profiles.id).
 * @return bool True si une ligne a été mise à jour.
 */
function avision_link_pre_inscription_to_profile( $temp_token, $profile_id ) {
    if ( empty( $temp_token ) || (int) $profile_id <= 0 ) {
        return false;
    }
    global $wpdb;
    $table = AVISION_PRE_INSCRIPTION_TABLE;
    $row   = $wpdb->get_row(
        $wpdb->prepare(
            "SELECT id, data FROM `{$table}` WHERE token = %s LIMIT 1",
            sanitize_text_field( $temp_token )
        ),
        ARRAY_A
    );
    if ( ! is_array( $row ) ) {
        return false;
    }
    $data = json_decode( isset( $row['data'] ) ? (string) $row['data'] : '{}', true );
    if ( ! is_array( $data ) ) {
        $data = array();
    }
    $data['profile_id'] = (int) $profile_id;
    $json               = wp_json_encode( $data, JSON_UNESCAPED_UNICODE );
    $updated            = $wpdb->update(
        $table,
        array( 'data' => $json ),
        array( 'token' => sanitize_text_field( $temp_token ) ),
        array( '%s' ),
        array( '%s' )
    );
    return $updated !== false && $updated > 0;
}

/**
 * Récupère une ligne de pré-inscription par temp_token.
 *
 * @param string $temp_token Token retourné par onboarding-save.
 * @return array|null Ligne (type, email, business_name, ...) ou null.
 */
function avision_get_pre_inscription_by_token( $temp_token ) {
    if ( empty( $temp_token ) ) {
        return null;
    }
    global $wpdb;
    $table = AVISION_PRE_INSCRIPTION_TABLE;
    $row   = $wpdb->get_row(
        $wpdb->prepare(
            // ANTI-RÉGRESSION : SELECT * obligatoire — même règle que
            // avision_get_latest_pre_inscription_by_email (l. 3287)
            // NE JAMAIS réduire à SELECT email, token, data
            "SELECT * FROM `{$table}` WHERE token = %s LIMIT 1",
            sanitize_text_field( $temp_token )
        ),
        ARRAY_A
    );
    if ( ! is_array( $row ) ) {
        return null;
    }
    return avision_pre_inscription_row_merged( $row );
}

/**
 * Dernière pré-inscription pour un e-mail (ORDER BY id DESC).
 *
 * @param string $email E-mail.
 * @return array<string,mixed>|null
 */
function avision_get_latest_pre_inscription_by_email( $email ) {
    if ( ! is_email( $email ) ) {
        return null;
    }
    global $wpdb;
    $table = AVISION_PRE_INSCRIPTION_TABLE;
    $row   = $wpdb->get_row(
        $wpdb->prepare(
            // ANTI-RÉGRESSION : SELECT doit inclure toutes les colonnes dédiées
            // neighborhood, city, category, language sont sauvés en colonnes ET en JSON
            // sans SELECT *, avision_pre_inscription_row_merged ne voit que le JSON data
            // NE JAMAIS réduire à SELECT email, token, data — perd neighborhood + language
            "SELECT * FROM `{$table}` WHERE email = %s ORDER BY id DESC LIMIT 1",
            sanitize_email( $email )
        ),
        ARRAY_A
    );
    if ( ! is_array( $row ) ) {
        return null;
    }
    return avision_pre_inscription_row_merged( $row );
}

/**
 * Tunnel Demo (sans session SaaS) : crée ou réutilise une ligne pré-inscription « guest »
 * à partir du transient d’audit `avision_{rid}` pour porter email + métadonnées jusqu’à Stripe.
 *
 * @param string $rid Identifiant rapport (URL).
 * @param string $override_email E-mail fourni par le client (URL / localStorage), prioritaire sur le transient.
 * @return string Token pré-inscription ou chaîne vide si impossible.
 */
function avision_ensure_demo_guest_pre_token_from_rid( $rid, $override_email = '' ) {
    $rid = is_string( $rid ) ? trim( $rid ) : '';
    if ( $rid === '' ) {
        return '';
    }
    $session = get_transient( 'avision_' . $rid );
    if ( ! is_array( $session ) ) {
        return '';
    }
    $meta = ( isset( $session['meta'] ) && is_array( $session['meta'] ) ) ? $session['meta'] : array();
    $data = ( isset( $session['data'] ) && is_array( $session['data'] ) ) ? $session['data'] : array();

    $email_in = is_string( $override_email ) ? trim( $override_email ) : '';
    if ( $email_in !== '' && is_email( $email_in ) ) {
        $email = sanitize_email( $email_in );
    } elseif ( ! empty( $meta['email'] ) && is_email( $meta['email'] ) ) {
        $email = sanitize_email( $meta['email'] );
    } else {
        $host = parse_url( home_url(), PHP_URL_HOST );
        $host = is_string( $host ) && $host !== '' ? $host : 'localhost';
        $email = 'guest+' . preg_replace( '/[^a-z0-9_-]/i', '', $rid ) . '@' . $host;
        if ( ! is_email( $email ) ) {
            $email = 'guest+' . sanitize_key( $rid ) . '@example.com';
        }
    }

    global $wpdb;
    $table = AVISION_PRE_INSCRIPTION_TABLE;
    if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
        return '';
    }

    $needle_fragment = '"report_id":"' . $rid . '"';
    $existing_token  = $wpdb->get_var(
        $wpdb->prepare(
            "SELECT token FROM `{$table}` WHERE data LIKE %s ORDER BY id DESC LIMIT 1",
            '%' . $wpdb->esc_like( $needle_fragment ) . '%'
        )
    );
    if ( is_string( $existing_token ) && $existing_token !== '' ) {
        return $existing_token;
    }

    $business_name = isset( $meta['name'] ) ? sanitize_text_field( (string) $meta['name'] ) : '';
    $city          = isset( $meta['city'] ) ? sanitize_text_field( (string) $meta['city'] ) : '';
    $country       = isset( $meta['country'] ) ? sanitize_text_field( (string) $meta['country'] ) : '';
    $neighborhood  = isset( $meta['neighborhood'] ) ? sanitize_text_field( (string) $meta['neighborhood'] ) : '';
    $category      = isset( $meta['category'] ) ? sanitize_text_field( (string) $meta['category'] ) : '';
    if ( $category === '' && isset( $meta['cat'] ) ) {
        $category = sanitize_text_field( (string) $meta['cat'] );
    }
    $score_pre = isset( $data['score_global'] ) ? (int) $data['score_global'] : 0;
    $lang_pre  = '';
    if ( function_exists( 'avision_detect_language_by_ip' ) ) {
        $lang_pre = (string) avision_detect_language_by_ip();
    }

    $data_to_store = array(
        'type'          => 'b2c',
        'business_name' => $business_name,
        'city'          => $city,
        'country'       => $country,
        'neighborhood'  => $neighborhood,
        'category'      => $category,
        'report_id'     => $rid,
        'guest_demo'    => true,
    );
    if ( $score_pre > 0 ) {
        $data_to_store['score'] = $score_pre;
    }
    if ( $lang_pre !== '' ) {
        $data_to_store['language'] = $lang_pre;
    }

    $temp_token = bin2hex( random_bytes( 32 ) );
    $data_json  = wp_json_encode( $data_to_store, JSON_UNESCAPED_UNICODE );

    $inserted = $wpdb->insert(
        $table,
        array(
            'email' => $email,
            'token' => $temp_token,
            'data'  => $data_json,
        ),
        array( '%s', '%s', '%s' )
    );

    return $inserted ? $temp_token : '';
}

/**
 * Insère une ligne mw_user avec mot de passe déjà hashé (après paiement B2C).
 *
 * @param \wpdb  $wpdb          Instance wpdb.
 * @param string $mw_table      Nom table (mw_user).
 * @param string $username      Login SaaS.
 * @param string $email         E-mail.
 * @param string $password_hash Hash WP (wp_hash_password).
 * @param string $display_name  Nom affiché.
 * @return int user_id mw_user ou 0.
 */
function avision_mw_user_insert_with_password_hash( $wpdb, $mw_table, $username, $email, $password_hash, $display_name ) {
    $mw_columns = $wpdb->get_col( "SHOW COLUMNS FROM `{$mw_table}`", 0 );
    $mw_columns = is_array( $mw_columns ) ? $mw_columns : array();
    $user_uid_max_len = 0;
    if ( in_array( 'user_uid', $mw_columns, true ) ) {
        $uid_col_row = $wpdb->get_row( "SHOW FULL COLUMNS FROM `{$mw_table}` WHERE Field = 'user_uid'" );
        if ( $uid_col_row && ! empty( $uid_col_row->Type ) && preg_match( '/\((\d+)\)/', (string) $uid_col_row->Type, $uid_m ) ) {
            $user_uid_max_len = max( 1, (int) $uid_m[1] );
        }
    }
    $contact_parts = preg_split( '/\s+/', trim( $display_name ) );
    $first_name    = '';
    $last_name     = '';
    if ( is_array( $contact_parts ) && ! empty( $contact_parts ) ) {
        $first_name = (string) array_shift( $contact_parts );
        $last_name  = trim( implode( ' ', $contact_parts ) );
    }
    $insert_data   = array();
    $insert_format = array();
    if ( in_array( 'username', $mw_columns, true ) ) {
        $insert_data['username'] = $username;
        $insert_format[]       = '%s';
    }
    if ( in_array( 'email', $mw_columns, true ) ) {
        $insert_data['email'] = $email;
        $insert_format[]     = '%s';
    }
    if ( in_array( 'password', $mw_columns, true ) ) {
        $insert_data['password'] = $password_hash;
        $insert_format[]         = '%s';
    }
    if ( in_array( 'user_uid', $mw_columns, true ) ) {
        $uid_val = uniqid( 'avi_' );
        if ( $user_uid_max_len > 0 && strlen( $uid_val ) > $user_uid_max_len ) {
            $uid_val = substr( 'avi_' . bin2hex( random_bytes( 16 ) ), 0, $user_uid_max_len );
        }
        $insert_data['user_uid'] = $uid_val;
        $insert_format[]         = '%s';
    }
    if ( in_array( 'date_added', $mw_columns, true ) ) {
        $insert_data['date_added'] = current_time( 'mysql' );
        $insert_format[]           = '%s';
    }
    if ( in_array( 'status', $mw_columns, true ) ) {
        $insert_data['status'] = 'active';
        $insert_format[]       = '%s';
    }
    if ( in_array( 'language_id', $mw_columns, true ) ) {
        $insert_data['language_id'] = 1;
        $insert_format[]          = '%d';
    }
    if ( in_array( 'first_name', $mw_columns, true ) ) {
        $insert_data['first_name'] = $first_name;
        $insert_format[]           = '%s';
    }
    if ( in_array( 'last_name', $mw_columns, true ) ) {
        $insert_data['last_name'] = $last_name;
        $insert_format[]          = '%s';
    }
    avision_onboarding_mw_user_fill_not_null_columns( $wpdb, $mw_table, $insert_data, $insert_format, $username, $email, '', $first_name, $last_name, $display_name, $user_uid_max_len );
    if ( empty( $insert_data ) ) {
        return 0;
    }
    $ok = $wpdb->insert( $mw_table, $insert_data, $insert_format );
    if ( ! $ok ) {
        return 0;
    }
    if ( in_array( 'user_id', $mw_columns, true ) ) {
        return (int) $wpdb->get_var(
            $wpdb->prepare(
                'SELECT user_id FROM mw_user WHERE email = %s ORDER BY user_id DESC LIMIT 1',
                $email
            )
        );
    }
    return (int) $wpdb->insert_id;
}

/**
 * Compte Solo (B2C) : avi_account_type = premium, avi_user_type = b2c, suppression du flag agence.
 *
 * @param int $wp_user_id ID wp_users.
 */
function avision_ensure_b2c_solo_account_meta( $wp_user_id ) {
    $uid = (int) $wp_user_id;
    if ( $uid <= 0 ) {
        return;
    }
    delete_user_meta( $uid, 'is_agency_master' );
    update_user_meta( $uid, 'avi_user_type', 'b2c' );
    if ( function_exists( 'avi_set_account_type' ) ) {
        avi_set_account_type( $uid, 'premium' );
    } else {
        update_user_meta( $uid, 'avi_account_type', 'premium' );
    }
    if ( function_exists( 'clean_user_cache' ) ) {
        clean_user_cache( $uid );
    }
}

/**
 * Compte agence (B2B) : métas onboarding ; rôle avi_agency uniquement si la requête est un retour Stripe sur /agency-dashboard/ (add_role, sans retirer les autres rôles).
 *
 * @param int $wp_user_id ID wp_users.
 */
function avision_ensure_b2b_agency_account_meta( $wp_user_id ) {
	$uid = (int) $wp_user_id;
	if ( $uid <= 0 ) {
		return;
	}
	$user = function_exists( 'get_userdata' ) ? get_userdata( $uid ) : null;
	if ( ! $user || ! $user->exists() ) {
		return;
	}
	// Rôle : signal fort = chemin de landing (évite d’écraser d’autres rôles avec set_role).
	if ( function_exists( 'avision_stripe_is_agency_checkout_landing' ) && avision_stripe_is_agency_checkout_landing() ) {
		if ( ! in_array( 'administrator', (array) $user->roles, true ) && class_exists( 'WP_User' ) ) {
			$wp_user_obj = new WP_User( $uid );
			if ( ! in_array( 'avi_agency', (array) $wp_user_obj->roles, true ) ) {
				$wp_user_obj->add_role( 'avi_agency' );
			}
			if ( function_exists( 'error_log' ) ) {
				error_log( '[AVI STRIPE] Succès : Attribution rôle avi_agency et redirection vers Agency Dashboard pour user_id: ' . (int) $wp_user_obj->ID );
			}
		}
	}
	update_user_meta( $uid, 'is_agency_master', '1' );
	update_user_meta( $uid, 'avi_user_type', 'b2b' );
	if ( function_exists( 'avi_set_account_type' ) ) {
		avi_set_account_type( $uid, 'agency' );
	} else {
		update_user_meta( $uid, 'avi_account_type', 'agency' );
	}
	if ( function_exists( 'clean_user_cache' ) ) {
		clean_user_cache( $uid );
	}
}

/**
 * Webhook Stripe B2C : crée mw_user + wor1515_avi_saas_profiles (pas de rôle agence) si pré-inscription avec password_hash.
 *
 * @param string $email         E-mail client.
 * @param string $company_name  Metadata Stripe.
 * @param string $city          Ville.
 * @param string $country       Pays.
 * @param string $neighborhood  Quartier.
 * @param string $category      Métier.
 * @param string $phone         Téléphone (métadonnées Stripe / pré-inscription).
 * @return array{profile_id:int,mw_user_id:int,wp_user_id:int}|null Null = laisser le fallback SaaS (legacy).
 */
function avision_stripe_provision_b2c_from_pre_inscription( $email, $company_name, $city, $country, $neighborhood, $category, $phone = '' ) {
    if ( ! is_email( $email ) ) {
        return null;
    }
    $pre = avision_get_latest_pre_inscription_by_email( $email );
    if ( ! is_array( $pre ) || ( isset( $pre['type'] ) && $pre['type'] !== 'b2c' ) ) {
        return null;
    }
    $password_hash = isset( $pre['password_hash'] ) ? (string) $pre['password_hash'] : '';
    if ( $password_hash === '' && ! empty( $pre['password'] ) ) {
        $password_hash = wp_hash_password( (string) $pre['password'] );
    }
    if ( $password_hash === '' ) {
        return null;
    }

    global $wpdb;
    $profiles_table = 'wor1515_avi_saas_profiles';
    $mw_table       = 'mw_user';

    $existing_profile_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$profiles_table}` WHERE email = %s LIMIT 1", $email ) );
    if ( $existing_profile_id > 0 ) {
        // ═══════════════════════════════════════════════════════════════
        // ANTI-RÉGRESSION — UPDATE neighborhood/city/category sur profil existant :
        // Le profil peut être créé AVANT la provision (webhook race condition)
        // avec neighborhood=NULL, city='-', category='Client'.
        // → Mettre à jour avec les données de la pré-inscription.
        // NE JAMAIS supprimer ce UPDATE — sans lui neighborhood reste NULL
        // sur les profils créés par le fallback webhook avant la provision.
        // AUTORISATION ANTHONY REQUISE POUR TOUTE MODIFICATION.
        // ═══════════════════════════════════════════════════════════════
        $neighborhood_pre = $neighborhood !== '' ? $neighborhood : ( isset( $pre['neighborhood'] ) ? (string) $pre['neighborhood'] : '' );
        $city_pre         = $city !== '' ? $city : ( isset( $pre['city'] ) ? (string) $pre['city'] : '' );
        $category_pre     = $category !== '' ? $category : ( isset( $pre['category'] ) ? (string) $pre['category'] : '' );
        $update_data   = array();
        $update_format = array();
        if ( $neighborhood_pre !== '' ) {
            $update_data['neighborhood'] = $neighborhood_pre;
            $update_format[]             = '%s';
        }
        if ( $city_pre !== '' && $city_pre !== '-' ) {
            $update_data['city'] = $city_pre;
            $update_format[]     = '%s';
        }
        if ( $category_pre !== '' && $category_pre !== 'Client' ) {
            $update_data['category'] = $category_pre;
            $update_format[]         = '%s';
        }
        if ( ! empty( $update_data ) ) {
            $wpdb->update( $profiles_table, $update_data, array( 'id' => $existing_profile_id ), $update_format, array( '%d' ) );
        }
        // ═══════════════════════════════════════════════════════════════════
        // ANTI-RÉGRESSION — Régénération KW2 si neighborhood vient d'être
        // mis à jour sur profil existant (règle #151 — chaîne neighborhood).
        // Cas : OAuth crée profil avec neighborhood=NULL → KW2 non généré.
        // Stripe provision → UPDATE neighborhood → KW2 inséré ici si absent.
        // NE JAMAIS supprimer sans autorisation Anthony.
        // ═══════════════════════════════════════════════════════════════════
        if ( isset( $update_data['neighborhood'] ) && $neighborhood_pre !== '' && $category_pre !== '' && $category_pre !== 'Client' ) {
            $tbl_kw      = $wpdb->prefix . 'avi_saas_keywords';
            $kw2_keyword = trim( $category_pre . ' ' . $neighborhood_pre );
            $kw2_exists  = (int) $wpdb->get_var( $wpdb->prepare(
                "SELECT id FROM `{$tbl_kw}` WHERE profile_id = %d AND keyword = %s LIMIT 1",
                $existing_profile_id,
                $kw2_keyword
            ) );
            if ( $kw2_exists === 0 ) {
                $wpdb->insert(
                    $tbl_kw,
                    array(
                        'profile_id'  => $existing_profile_id,
                        'keyword'     => $kw2_keyword,
                        'target_city' => $neighborhood_pre,
                        'scan_status' => 'pending',
                        'created_at'  => current_time( 'mysql' ),
                    ),
                    array( '%d', '%s', '%s', '%s', '%s' )
                );
                error_log( '[AVI] KW2 inséré post-provision neighborhood profile_id=' . $existing_profile_id . ' kw=' . $kw2_keyword );
            }
        }
        $mw_uid = (int) $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM `{$mw_table}` WHERE email = %s LIMIT 1", $email ) );
        $wp_u   = get_user_by( 'email', $email );
        if ( $wp_u && function_exists( 'avision_ensure_b2c_solo_account_meta' ) ) {
            avision_ensure_b2c_solo_account_meta( (int) $wp_u->ID );
        }
        return array(
            'profile_id' => $existing_profile_id,
            'mw_user_id' => $mw_uid,
            'wp_user_id' => $wp_u ? (int) $wp_u->ID : 0,
        );
    }

    $business_name    = $company_name !== '' ? $company_name : ( isset( $pre['business_name'] ) ? (string) $pre['business_name'] : __( 'Profil', 'avision-dashboard' ) );
    $city_use         = $city !== '' ? $city : ( isset( $pre['city'] ) ? (string) $pre['city'] : 'N/A' );
    $country_use      = $country !== '' ? strtoupper( (string) $country ) : ( isset( $pre['country'] ) ? strtoupper( (string) $pre['country'] ) : 'FR' );
    if ( ! in_array( $country_use, array( 'FR', 'GB', 'US', 'ES' ), true ) ) {
        $country_use = 'FR';
    }
    $neighborhood_use = $neighborhood !== '' ? $neighborhood : ( isset( $pre['neighborhood'] ) ? (string) $pre['neighborhood'] : '' );
    $category_use     = $category !== '' ? $category : ( isset( $pre['category'] ) ? (string) $pre['category'] : 'Client' );

    $username_base = sanitize_user( current( explode( '@', $email ) ), true );
    if ( $username_base === '' ) {
        $username_base = 'user';
    }
    $username = $username_base;
    $suffix   = 1;
    while ( username_exists( $username ) ) {
        $suffix++;
        $username = $username_base . $suffix;
    }

    $wp_user_id = wp_create_user( $username, wp_generate_password( 32, true, true ), $email );
    if ( is_wp_error( $wp_user_id ) ) {
        if ( ! avision_rest_onboarding_is_wp_duplicate_email_error( $wp_user_id ) ) {
            return null;
        }
        $existing_wp = get_user_by( 'email', $email );
        if ( ! $existing_wp ) {
            return null;
        }
        $wp_user_id = (int) $existing_wp->ID;
    } else {
        $wp_user_id = (int) $wp_user_id;
    }

    $wpdb->update( $wpdb->users, array( 'user_pass' => $password_hash ), array( 'ID' => $wp_user_id ), array( '%s' ), array( '%d' ) );
    wp_cache_delete( $wp_user_id, 'users' );
    if ( function_exists( 'avision_ensure_b2c_solo_account_meta' ) ) {
        avision_ensure_b2c_solo_account_meta( $wp_user_id );
    } else {
        update_user_meta( $wp_user_id, 'avi_user_type', 'b2c' );
    }

    $mw_user_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM `{$mw_table}` WHERE email = %s LIMIT 1", $email ) );
    if ( $mw_user_id <= 0 ) {
        $mw_user_id = avision_mw_user_insert_with_password_hash( $wpdb, $mw_table, $username, $email, $password_hash, $business_name );
    }
    if ( $mw_user_id <= 0 ) {
        return null;
    }

    $has_radar        = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'radar_plan' ) ) === 'radar_plan' );
    $has_uuid         = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'uuid' ) ) === 'uuid' );
    $has_user_type_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'user_type' ) ) === 'user_type' );
    $has_country_col   = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'country' ) ) === 'country' );
    $has_phone_col     = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'phone' ) ) === 'phone' );
    $phone_pre         = isset( $pre['phone'] ) ? sanitize_text_field( trim( (string) $pre['phone'] ) ) : '';
    $phone_use         = ( $phone !== '' && $phone !== null ) ? sanitize_text_field( trim( (string) $phone ) ) : $phone_pre;

    $insert_data   = array(
        'user_id'       => $mw_user_id,
        'business_name' => $business_name,
        'city'          => $city_use,
        'neighborhood'  => $neighborhood_use,
        'category'      => $category_use,
        'email'         => $email,
    );
    $insert_format = array( '%d', '%s', '%s', '%s', '%s', '%s' );
    if ( $has_radar ) {
        $insert_data['radar_plan'] = 'offert';
        $insert_format[]           = '%s';
    }
    if ( $has_uuid && function_exists( 'wp_generate_uuid4' ) ) {
        $insert_data['uuid'] = wp_generate_uuid4();
        $insert_format[]     = '%s';
    }
    if ( $has_user_type_col ) {
        $insert_data['user_type'] = 'b2c';
        $insert_format[]          = '%s';
    }
    if ( $has_country_col ) {
        $insert_data['country'] = $country_use;
        $insert_format[]        = '%s';
    }
    if ( $has_phone_col ) {
        $insert_data['phone'] = $phone_use;
        $insert_format[]      = '%s';
    }

    $ok = $wpdb->insert( $profiles_table, $insert_data, $insert_format );
    if ( ! $ok ) {
        return null;
    }

    $new_profile_id = (int) $wpdb->insert_id;
    if ( $new_profile_id > 0 && function_exists( 'avision_stripe_persist_profile_lang_column' ) ) {
        $ml = array( 'country' => $country_use );
        if ( ! empty( $pre['language'] ) ) {
            $ml['language'] = (string) $pre['language'];
        }
        if ( ! empty( $pre['lang'] ) ) {
            $ml['lang'] = (string) $pre['lang'];
        }
        avision_stripe_persist_profile_lang_column( $new_profile_id, $ml );
    }

    return array(
        'profile_id' => $new_profile_id,
        'mw_user_id' => $mw_user_id,
        'wp_user_id' => $wp_user_id,
    );
}

/**
 * URL front « activation » (magic link) pour définir le mot de passe — mêmes paramètres que le reset WP (`key`, `login`), sans wp-login.php.
 *
 * @param \WP_User $user Compte WordPress cible.
 * @return string|\WP_Error URL absolue ou erreur.
 */
function avision_get_user_activation_magic_link_url( $user ) {
	if ( ! ( $user instanceof \WP_User ) || ! $user->exists() ) {
		return new \WP_Error( 'avi_activation_no_user', 'Invalid user.' );
	}
	if ( ! function_exists( 'get_password_reset_key' ) ) {
		return new \WP_Error( 'avi_activation_no_fn', 'get_password_reset_key unavailable.' );
	}
	$key = get_password_reset_key( $user );
	if ( is_wp_error( $key ) ) {
		return $key;
	}
	// Même base que les e-mails « mot de passe oublié » (filtre retrieve_password_message) : slug paramétrable via auth shortcodes.
	if ( function_exists( 'avision_custom_auth_reset_page_url' ) ) {
		$base = avision_custom_auth_reset_page_url();
	} else {
		$path = apply_filters( 'avision_activation_page_path', '/activation/' );
		$path = is_string( $path ) && $path !== '' ? $path : '/activation/';
		$base = trailingslashit( home_url( $path ) );
	}
	$url  = add_query_arg(
		array(
			'key'   => $key,
			'login' => $user->user_login,
		),
		$base
	);
	return apply_filters( 'avision_activation_magic_link_url', $url, $user );
}

/**
 * URLs publiques Cockpit AVI pour les emails B2C (liens premium, sans URL wp-login brute).
 *
 * @param string $pid UUID profil optionnel (paramètre pid).
 * @return array{dashboard_url:string,login_page_url:string}
 */
function avision_get_b2c_welcome_public_urls( $pid = '' ) {
	$dashboard_base = 'https://avision.org/premium-dashboard/';
	$pid_str        = is_string( $pid ) ? trim( $pid ) : '';
	$dashboard_url  = ( $pid_str !== '' ) ? add_query_arg( 'pid', rawurlencode( $pid_str ), $dashboard_base ) : $dashboard_base;
	return array(
		'dashboard_url'  => $dashboard_url,
		'login_page_url' => 'https://avision.org/connexion/',
	);
}

/**
 * Corps HTML de l’email de bienvenue B2C (aucune mention de « WordPress », jamais de mot de passe en clair).
 *
 * @param string $opening_html    Introduction (HTML déjà échappé côté appelant pour les données dynamiques).
 * @param string $dashboard_url   URL du dashboard.
 * @param string $identifier      Identifiant (email) affiché ; vide pour masquer la ligne.
 * @param string $after_links     Texte d’aide après les liens (sera échappé).
 * @param string $activation_url  Lien front /activation/ (key + login) ; vide si indisponible.
 * @param string $email_lang      fr|en|es.
 * @return string
 */
function avision_build_b2c_welcome_email_html( $opening_html, $dashboard_url, $identifier = '', $after_links = '', $activation_url = '', $email_lang = 'fr' ) {
	$lang = strtolower( trim( (string) $email_lang ) );
	if ( ! in_array( $lang, array( 'fr', 'en', 'es' ), true ) ) {
		$lang = 'fr';
	}
	$copy = array(
		'fr' => array(
			'access_intro'  => 'Voici les accès à votre Cockpit AVI&nbsp;:',
			'id_label'      => 'Identifiant',
			'connexion'     => 'Accéder à la page de connexion',
			'dashboard'     => 'Ouvrir mon Dashboard Premium',
			'activate'      => 'Définir mon mot de passe et activer mon accès',
			'activate_note' => 'Ce lien sécurisé expire sous 24 heures. Ne le partagez pas.',
			'signoff'       => 'À bientôt,',
			'team'          => 'L’équipe AVI',
		),
		'en' => array(
			'access_intro'  => 'Here is how to access your AVI Cockpit:',
			'id_label'      => 'Sign-in email',
			'connexion'     => 'Go to the sign-in page',
			'dashboard'     => 'Open my Premium Dashboard',
			'activate'      => 'Set my password and activate my access',
			'activate_note' => 'This secure link expires in 24 hours. Do not share it.',
			'signoff'       => 'See you soon,',
			'team'          => 'The AVI team',
		),
		'es' => array(
			'access_intro'  => 'Así puedes acceder a tu Cockpit AVI:',
			'id_label'      => 'Correo de acceso',
			'connexion'     => 'Ir a la página de inicio de sesión',
			'dashboard'     => 'Abrir mi Dashboard Premium',
			'activate'      => 'Definir mi contraseña y activar mi acceso',
			'activate_note' => 'Este enlace seguro caduca en 24 horas. No lo compartas.',
			'signoff'       => 'Hasta pronto,',
			'team'          => 'El equipo AVI',
		),
	);
	$c          = $copy[ $lang ];
	$urls       = avision_get_b2c_welcome_public_urls();
	$login_page = isset( $urls['login_page_url'] ) ? $urls['login_page_url'] : 'https://avision.org/connexion/';
	$login_link = '<a href="' . esc_url( $login_page ) . '">' . esc_html( $c['connexion'] ) . '</a>';
	$dash_link  = '<a href="' . esc_url( $dashboard_url ) . '">' . esc_html( $c['dashboard'] ) . '</a>';
	$html       = '<!DOCTYPE html><html><body style="font-family:system-ui,-apple-system,sans-serif;line-height:1.6;color:#111827;font-size:16px;">';
	$html      .= $opening_html;
	$html      .= '<p>' . esc_html( $c['access_intro'] ) . '</p>';
	if ( $identifier !== '' ) {
		$html .= '<p><strong>' . esc_html( $c['id_label'] ) . '&nbsp;:</strong> ' . esc_html( $identifier ) . '</p>';
	}
	if ( $activation_url !== '' ) {
		$html .= '<p><strong><a href="' . esc_url( $activation_url ) . '">' . esc_html( $c['activate'] ) . '</a></strong></p>';
		$html .= '<p><em>' . esc_html( $c['activate_note'] ) . '</em></p>';
	}
	$html .= '<p>' . $login_link . '</p>';
	$html .= '<p>' . $dash_link . '</p>';
	if ( $after_links !== '' ) {
		$html .= '<p>' . esc_html( $after_links ) . '</p>';
	}
	$html .= '<p>' . esc_html( $c['signoff'] ) . '<br><strong>' . esc_html( $c['team'] ) . '</strong></p>';
	$html .= '</body></html>';
	return $html;
}

/**
 * Email B2C après paiement Stripe : compte utilisateur déjà existant (pas de mot de passe à transmettre par email).
 *
 * @param string $email          Email du compte.
 * @param string $business_name  Nom affiché.
 * @param string $pid            UUID profil (query pid).
 * @param int    $profile_row_id ID avi_saas_profiles (vérif existence si > 0).
 * @param string $email_lang     fr|en|es (metadata checkout / URL).
 * @return bool
 */
function avision_send_welcome_b2c_email_checkout_paid_existing_wp_user( $email, $business_name, $pid, $profile_row_id = 0, $email_lang = 'fr' ) {
	$email = sanitize_email( (string) $email );
	if ( ! is_email( $email ) ) {
		return false;
	}
	$profile_row_id = (int) $profile_row_id;
	if ( $profile_row_id > 0 && isset( $GLOBALS['wpdb'] ) ) {
		global $wpdb;
		$pt_chk = 'wor1515_avi_saas_profiles';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pt_chk ) ) !== $pt_chk ) {
			$pt_chk = $wpdb->prefix . 'avi_saas_profiles';
		}
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pt_chk ) ) === $pt_chk ) {
			$row_ok = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$pt_chk}` WHERE id = %d LIMIT 1", $profile_row_id ) );
			if ( $row_ok !== $profile_row_id ) {
				if ( function_exists( 'error_log' ) ) {
					error_log( '[BREVO] WELCOME (post-checkout, sans MDP) annulé : profil id=' . $profile_row_id . ' introuvable.' );
				}
				return false;
			}
		}
	}
	$bn            = $business_name !== '' ? $business_name : 'Profil ' . $email;
	$pid_str       = is_string( $pid ) ? trim( $pid ) : '';
	$urls          = avision_get_b2c_welcome_public_urls( $pid_str );
	$dashboard_url = $urls['dashboard_url'];
	$lang          = strtolower( trim( (string) $email_lang ) );
	if ( ! in_array( $lang, array( 'fr', 'en', 'es' ), true ) ) {
		$lang = 'fr';
	}
	$activation_url = '';
	$wp_user        = get_user_by( 'email', $email );
	if ( $wp_user instanceof \WP_User && $wp_user->exists() && function_exists( 'avision_get_user_activation_magic_link_url' ) ) {
		$act = avision_get_user_activation_magic_link_url( $wp_user );
		if ( ! is_wp_error( $act ) ) {
			$activation_url = $act;
		} elseif ( function_exists( 'error_log' ) ) {
			error_log( '[BREVO] Magic link (existing user welcome) : ' . $act->get_error_message() );
		}
	}
	if ( $lang === 'en' ) {
		$subj    = sprintf( '[AVISION] Your AVI Cockpit is ready — %s', $bn );
		$opening = '<p>Hello,</p><p>Your payment is confirmed. Your AVI Cockpit is available.</p>';
		$after   = $activation_url !== '' ? '' : 'Use the password you chose when you registered.';
	} elseif ( $lang === 'es' ) {
		$subj    = sprintf( '[AVISION] Tu Cockpit AVI está listo — %s', $bn );
		$opening = '<p>Hola,</p><p>Tu pago está confirmado. Tu Cockpit AVI está disponible.</p>';
		$after   = $activation_url !== '' ? '' : 'Usa la contraseña que definiste al registrarte.';
	} else {
		$subj    = sprintf( '[AVISION] Votre Cockpit AVI est prêt — %s', $bn );
		$opening = '<p>Bonjour,</p><p>Votre paiement est confirmé. Votre Cockpit AVI est disponible.</p>';
		$after   = $activation_url !== '' ? '' : 'Utilisez le mot de passe défini lors de votre inscription.';
	}
	$body          = avision_build_b2c_welcome_email_html(
		$opening,
		$dashboard_url,
		'',
		$after,
		$activation_url,
		$lang
	);
	$headers = array( 'Content-Type: text/html; charset=UTF-8' );
	if ( function_exists( 'error_log' ) ) {
		error_log( '[BREVO] Envoi WELCOME post-checkout (sans MDP en clair) : ' . $email );
	}
	$wp_user = get_user_by( 'email', $email );
	if ( $wp_user instanceof \WP_User && class_exists( '\Avision\Avision_Email_Manager' ) ) {
		$sent_brevo = \Avision\Avision_Email_Manager::send_avi_email(
			(int) $wp_user->ID,
			\Avision\Avision_Email_Manager::TYPE_WELCOME_COCKPIT,
			array(
				'recipient_email' => $email,
				'user_name'       => (string) $wp_user->display_name,
				'user_email'      => (string) $wp_user->user_email,
				'dashboard_url'   => trailingslashit( home_url( '/premium-dashboard/' ) ),
				'login_url'       => trailingslashit( home_url( '/login/' ) ),
				'business_name'   => (string) $bn,
				'email_lang'      => $lang,
			)
		);
		if ( $sent_brevo ) {
			return true;
		}
	}
	if ( ! function_exists( 'wp_mail' ) ) {
		return false;
	}
	return (bool) wp_mail( $email, $subj, $body, $headers );
}

/**
 * Envoie l'email Brevo WELCOME_B2C (identifiant, lien d’activation / reset front, dashboard / connexion).
 * Aucun mot de passe en clair. Si $plain_password est vide (compte déjà existant), délègue à l’email « cockpit prêt ».
 *
 * @param string $email           Email du compte utilisateur.
 * @param string $business_name   Nom affiché.
 * @param string $pid             UUID profil (paramètre pid du dashboard).
 * @param string $plain_password  Non vide = compte WP vient d’être créé avec ce mot de passe (jamais envoyé par email).
 * @param int    $profile_row_id  ID ligne avi_saas_profiles : si > 0, vérifie que la ligne existe avant send_avi_email (template 41).
 * @param string $email_lang      fr|en|es (ex. metadata Stripe lang).
 * @return bool
 */
function avision_send_welcome_b2c_email_after_convert( $email, $business_name, $pid, $plain_password, $profile_row_id = 0, $email_lang = 'fr' ) {
	$email = sanitize_email( (string) $email );
	if ( ! is_email( $email ) ) {
		return false;
	}
	$lang = strtolower( trim( (string) $email_lang ) );
	if ( ! in_array( $lang, array( 'fr', 'en', 'es' ), true ) ) {
		$lang = 'fr';
	}
	if ( $plain_password === '' ) {
		return avision_send_welcome_b2c_email_checkout_paid_existing_wp_user( $email, $business_name, $pid, $profile_row_id, $lang );
	}
	if ( ! class_exists( '\Avision\Avision_Email_Manager' ) ) {
		return false;
	}
	$profile_row_id = (int) $profile_row_id;
	if ( $profile_row_id > 0 && isset( $GLOBALS['wpdb'] ) ) {
		global $wpdb;
		$pt_chk = 'wor1515_avi_saas_profiles';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pt_chk ) ) !== $pt_chk ) {
			$pt_chk = $wpdb->prefix . 'avi_saas_profiles';
		}
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pt_chk ) ) === $pt_chk ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nom de table validé par SHOW TABLES.
			$row_ok = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$pt_chk}` WHERE id = %d LIMIT 1", $profile_row_id ) );
			if ( $row_ok !== $profile_row_id ) {
				if ( function_exists( 'error_log' ) ) {
					error_log( '[BREVO] Envoi WELCOME_B2C (41) annulé : profil id=' . $profile_row_id . ' introuvable en base.' );
				}
				return false;
			}
		}
	}
	if ( function_exists( 'error_log' ) ) {
		error_log( '[BREVO] Tentative WELCOME_B2C (magic link, sans MDP en clair) : ' . $email );
	}
	$saas_user_id = 0;
	if ( class_exists( '\Avision\Avision_Database' ) ) {
		$db = new \Avision\Avision_Database();
		$db->init();
		if ( method_exists( $db, 'get_or_create_saas_user' ) ) {
			$uid = $db->get_or_create_saas_user( $email );
			if ( $uid !== null && (int) $uid > 0 ) {
				$saas_user_id = (int) $uid;
			}
		}
	}
	global $wpdb;
	$saas_users_table = 'wor1515_avi_saas_users';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) !== $saas_users_table ) {
		$saas_users_table = $wpdb->prefix . 'avi_saas_users';
	}
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) !== $saas_users_table ) {
		$saas_users_table = '';
	}
	if ( $saas_user_id <= 0 && $saas_users_table !== '' ) {
		$existing = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM `{$saas_users_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id DESC LIMIT 1",
				$email
			)
		);
		if ( $existing > 0 ) {
			$saas_user_id = $existing;
		} else {
			$ok_ins = $wpdb->insert( $saas_users_table, [ 'email' => $email ], [ '%s' ] );
			if ( $ok_ins ) {
				$saas_user_id = (int) $wpdb->insert_id;
			}
		}
	}
	if ( $saas_users_table !== '' && ( $saas_user_id <= 0 || 1 === $saas_user_id ) ) {
		$resolved = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM `{$saas_users_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id DESC LIMIT 1",
				$email
			)
		);
		if ( $resolved > 0 ) {
			$saas_user_id = $resolved;
		}
	}
	$bn              = $business_name !== '' ? $business_name : 'Profil ' . $email;
	$pid_str         = is_string( $pid ) ? trim( $pid ) : '';
	$urls            = avision_get_b2c_welcome_public_urls( $pid_str );
	$dashboard_url   = $urls['dashboard_url'];
	$login_url       = $urls['login_page_url'];

	$wp_user = get_user_by( 'email', $email );
	if ( ! ( $wp_user instanceof \WP_User ) || ! $wp_user->exists() ) {
		if ( function_exists( 'error_log' ) ) {
			error_log( '[BREVO] WELCOME_B2C annulé : aucun utilisateur WordPress pour l’email ' . $email );
		}
		return false;
	}
	$activation_url = '';
	if ( function_exists( 'avision_get_user_activation_magic_link_url' ) ) {
		$act = avision_get_user_activation_magic_link_url( $wp_user );
		if ( ! is_wp_error( $act ) ) {
			$activation_url = $act;
		} elseif ( function_exists( 'error_log' ) ) {
			error_log( '[BREVO] Magic link : ' . $act->get_error_message() );
		}
	}

	$b2c_template_id = 41;
	if ( defined( '\Avision\Avision_Email_Manager::TEMPLATE_WELCOME_B2C' ) ) {
		$t_const = constant( '\Avision\Avision_Email_Manager::TEMPLATE_WELCOME_B2C' );
		if ( is_numeric( $t_const ) && (int) $t_const > 0 ) {
			$b2c_template_id = (int) $t_const;
		}
	}

	$sent = \Avision\Avision_Email_Manager::send_avi_email(
		$saas_user_id > 0 ? $saas_user_id : 0,
		$b2c_template_id,
		[
			'business_name'    => $bn,
			'dashboard_url'    => $dashboard_url,
			'login_url'        => $login_url,
			'activation_url'   => $activation_url,
			'recipient_email'  => $email,
			'email_lang'       => $lang,
		]
	);
	if ( $sent ) {
		return true;
	}
	if ( $lang === 'en' ) {
		$subj    = sprintf( '[AVISION] Your AVI Cockpit — access %s', $bn );
		$opening = '<p>Welcome ' . esc_html( $bn ) . ',</p><p>Your AVI Cockpit is ready. You can manage your local visibility now.</p>';
	} elseif ( $lang === 'es' ) {
		$subj    = sprintf( '[AVISION] Tu Cockpit AVI — acceso %s', $bn );
		$opening = '<p>Bienvenido/a ' . esc_html( $bn ) . ',</p><p>Tu Cockpit AVI está listo. Ya puedes pilotar tu visibilidad local.</p>';
	} else {
		$subj    = sprintf( '[AVISION] Votre Cockpit AVI — accès %s', $bn );
		$opening = '<p>Bienvenue ' . esc_html( $bn ) . ',</p><p>Votre Cockpit AVI est prêt. Vous pouvez piloter votre visibilité locale dès maintenant.</p>';
	}
	$body    = avision_build_b2c_welcome_email_html( $opening, $dashboard_url, $email, '', $activation_url, $lang );
	$headers = array( 'Content-Type: text/html; charset=UTF-8' );
	return function_exists( 'wp_mail' ) && wp_mail( $email, $subj, $body, $headers );
}

/**
 * Alias pratique : email de bienvenue B2C (cockpit + accès).
 *
 * @param string $user_email      Email du compte.
 * @param string $business_name   Nom affiché (entreprise).
 * @param string $password        Mot de passe interne si le compte vient d’être créé (non inclus dans l’email ; lien d’activation à la place).
 * @param string $pid             UUID profil (query pid).
 * @param int    $profile_row_id  ID ligne avi_saas_profiles.
 * @param string $email_lang      fr|en|es.
 * @return bool
 */
function avi_send_welcome_email( $user_email, $business_name, $password, $pid = '', $profile_row_id = 0, $email_lang = 'fr' ) {
	return avision_send_welcome_b2c_email_after_convert( $user_email, $business_name, $pid, (string) $password, (int) $profile_row_id, $email_lang );
}

/**
 * Liste les fiches Google disponibles et met à jour pending_google_locations (retour OAuth sans lieu choisi).
 *
 * @param string $access_token Jeton OAuth.
 * @param int    $profile_id   ID ligne profil.
 * @param string $profiles_table Nom table.
 * @param string $google_location_name Si non vide, ne fait rien.
 * @return void
 */
function avision_convert_profile_sync_pending_google_locations( $access_token, $profile_id, $profiles_table, $google_location_name = '' ) {
	global $wpdb;
	$profile_id = (int) $profile_id;
	if ( $profile_id <= 0 || trim( (string) $google_location_name ) !== '' ) {
		return;
	}
	$access_token = is_string( $access_token ) ? trim( $access_token ) : '';
	if ( $access_token === '' || ! is_string( $profiles_table ) || $profiles_table === '' ) {
		return;
	}
	if ( function_exists( 'avi_ensure_saas_pending_google_locations_column' ) ) {
		avi_ensure_saas_pending_google_locations_column( $profiles_table );
	}
	if ( function_exists( 'avi_oauth_fetch_and_store_google_locations_for_profile' ) ) {
		avi_oauth_fetch_and_store_google_locations_for_profile( $access_token, $profile_id, $profiles_table );
		return;
	}
	if ( function_exists( 'avi_oauth_agency_store_pending_google_locations' ) ) {
		avi_oauth_agency_store_pending_google_locations( $access_token, $profile_id, $profiles_table );
		return;
	}
	if ( ! function_exists( 'avi_google_api_list_business_locations_flat' ) && ! function_exists( 'avi_google_api_list_business_locations_flat_with_meta' ) ) {
		return;
	}
	$col = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'pending_google_locations' ) );
	if ( $col !== 'pending_google_locations' ) {
		return;
	}
	$meta      = array();
	$locations = array();
	if ( function_exists( 'avi_google_api_list_business_locations_flat_with_meta' ) ) {
		$bundle    = avi_google_api_list_business_locations_flat_with_meta( $access_token );
		$locations = isset( $bundle['locations'] ) && is_array( $bundle['locations'] ) ? $bundle['locations'] : array();
		$meta      = isset( $bundle['meta'] ) && is_array( $bundle['meta'] ) ? $bundle['meta'] : array();
	} else {
		$locations = avi_google_api_list_business_locations_flat( $access_token );
	}
	$norm = array();
	foreach ( $locations as $loc ) {
		if ( ! is_array( $loc ) ) {
			continue;
		}
		$fn = isset( $loc['location_full_name'] ) ? trim( (string) $loc['location_full_name'] ) : '';
		if ( $fn === '' || strpos( $fn, 'accounts/' ) !== 0 ) {
			continue;
		}
		$norm[] = array(
			'location_full_name' => $fn,
			'title'              => isset( $loc['title'] ) ? (string) $loc['title'] : 'Sans nom',
			'address'            => isset( $loc['address'] ) ? (string) $loc['address'] : '',
		);
	}
	$payload = function_exists( 'avi_oauth_build_pending_google_locations_json' )
		? avi_oauth_build_pending_google_locations_json( $norm, $meta )
		: wp_json_encode(
			array(
				'locations'  => $norm,
				'fetched_at' => gmdate( 'c' ),
			),
			JSON_UNESCAPED_UNICODE
		);
	$wpdb->update(
		$profiles_table,
		array( 'pending_google_locations' => $payload ),
		array( 'id' => $profile_id ),
		array( '%s' ),
		array( '%d' )
	);
	if ( function_exists( 'avi_oauth_sync_pending_google_locations_user_meta' ) ) {
		avi_oauth_sync_pending_google_locations_user_meta( $profiles_table, $profile_id, $payload );
	}
}

/**
 * Email WordPress dérivé lorsque l'email du rapport est déjà celui du compte administrateur (ID 1),
 * pour ne jamais lier un profil B2C onboarding à l'admin.
 *
 * @param string $email Email du rapport (déjà sanitizé).
 * @param string $rid   Identifiant rapport.
 * @return string Email dérivé unique ou chaîne vide.
 */
function avision_b2c_derived_wp_email_for_admin_collision( $email, $rid ) {
	$email = sanitize_email( (string) $email );
	$rid   = sanitize_text_field( (string) $rid );
	if ( ! is_email( $email ) || $rid === '' ) {
		return '';
	}
	$hash = substr( md5( $rid . '|' . strtolower( $email ) ), 0, 12 );
	$parts = explode( '@', $email, 2 );
	if ( count( $parts ) === 2 && $parts[0] !== '' && $parts[1] !== '' ) {
		$derived = $parts[0] . '+avi_b2c_' . $hash . '@' . $parts[1];
		if ( is_email( $derived ) ) {
			return $derived;
		}
	}
	$host = wp_parse_url( home_url(), PHP_URL_HOST );
	if ( ! is_string( $host ) || $host === '' ) {
		$host = 'localhost';
	}
	$derived = 'avi_b2c_' . $hash . '@' . $host;
	return is_email( $derived ) ? $derived : '';
}

/**
 * Convertit un rapport temporaire (transient avision_{rid}) en profil SaaS persistant.
 *
 * @param string $report_id Identifiant rapport (rid).
 * @return array{profile_id:int,wp_user_id:int,pid:string,email:string}|null
 */
function avision_convert_report_to_profile( $report_id ) {
	$rid = sanitize_text_field( (string) $report_id );
	$can_convert = current_user_can( 'manage_options' );
	if ( ! $can_convert ) {
		return null;
	}
	if ( $rid === '' ) {
		return null;
	}

	$report_data = get_transient( 'avision_' . $rid );
	if ( ! is_array( $report_data ) || empty( $report_data['meta'] ) || ! is_array( $report_data['meta'] ) ) {
		return null;
	}

	$meta = $report_data['meta'];
	$email = isset( $meta['email'] ) ? sanitize_email( (string) $meta['email'] ) : '';
	if ( ! is_email( $email ) ) {
		return null;
	}

	$business_name = isset( $meta['name'] ) ? sanitize_text_field( (string) $meta['name'] ) : '';
	$city          = isset( $meta['city'] ) ? sanitize_text_field( (string) $meta['city'] ) : '';
	$phone         = isset( $meta['phone'] ) ? sanitize_text_field( (string) $meta['phone'] ) : '';
	$category      = isset( $meta['category'] ) ? sanitize_text_field( (string) $meta['category'] ) : '';
	if ( $category === '' && isset( $meta['cat'] ) ) {
		$category = sanitize_text_field( (string) $meta['cat'] );
	}
	$neighborhood = isset( $meta['neighborhood'] ) ? sanitize_text_field( (string) $meta['neighborhood'] ) : '';
	$country      = isset( $meta['country'] ) ? strtoupper( sanitize_text_field( (string) $meta['country'] ) ) : 'FR';
	if ( ! in_array( $country, array( 'FR', 'GB', 'US', 'ES' ), true ) ) {
		$country = 'FR';
	}

	$meta_google_access   = isset( $meta['google_access_token'] ) ? trim( (string) $meta['google_access_token'] ) : '';
	$meta_google_refresh  = isset( $meta['google_refresh_token'] ) ? trim( (string) $meta['google_refresh_token'] ) : '';
	$meta_google_location = isset( $meta['google_location_name'] ) ? trim( (string) $meta['google_location_name'] ) : '';
	if ( $meta_google_location === '' && isset( $meta['google_resource_name'] ) ) {
		$meta_google_location = trim( (string) $meta['google_resource_name'] );
	}
	$meta_expires_in = isset( $meta['expires_in'] ) ? (int) $meta['expires_in'] : 0;
	if ( $meta_expires_in <= 0 && isset( $meta['google_expires_in'] ) ) {
		$meta_expires_in = (int) $meta['google_expires_in'];
	}

	$is_new_wp_user             = false;
	$plain_password_for_welcome = '';
	// Jamais get_current_user_id() : rattachement uniquement à l'email du rapport (ou compte dérivé si collision admin).
	$wp_account_email = $email;
	$wp_user          = get_user_by( 'email', $email );
	if ( $wp_user && (int) $wp_user->ID === 1 ) {
		$derived = avision_b2c_derived_wp_email_for_admin_collision( $email, $rid );
		if ( $derived === '' ) {
			return null;
		}
		$wp_account_email = $derived;
		$wp_user          = get_user_by( 'email', $derived );
	}
	if ( $wp_user && (int) $wp_user->ID === 1 ) {
		return null;
	}
	if ( $wp_user ) {
		$wp_user_id = (int) $wp_user->ID;
	} else {
		$username_base = sanitize_user( current( explode( '@', $wp_account_email ) ), true );
		if ( $username_base === '' ) {
			$username_base = 'user';
		}
		$username = $username_base;
		$suffix   = 1;
		while ( username_exists( $username ) ) {
			$suffix++;
			$username = $username_base . $suffix;
		}
		$plain_password_for_welcome = wp_generate_password( 12, false );
		$created_user               = wp_create_user( $username, $plain_password_for_welcome, $wp_account_email );
		if ( is_wp_error( $created_user ) ) {
			if ( ! function_exists( 'avision_rest_onboarding_is_wp_duplicate_email_error' ) || ! avision_rest_onboarding_is_wp_duplicate_email_error( $created_user ) ) {
				return null;
			}
			$existing_wp = get_user_by( 'email', $wp_account_email );
			if ( ! $existing_wp ) {
				error_log( '[AVI convert_report] E-mail WP déjà utilisé mais get_user_by vide : ' . $wp_account_email );
				return null;
			}
			$wp_user_id = (int) $existing_wp->ID;
			$is_new_wp_user = false;
		} else {
			$wp_user_id     = (int) $created_user;
			$is_new_wp_user = true;
		}
	}
	if ( (int) $wp_user_id === 1 ) {
		$admin_email_opt = sanitize_email( (string) get_option( 'admin_email', '' ) );
		$acct_norm       = strtolower( trim( $wp_account_email ) );
		$admin_norm      = strtolower( trim( (string) $admin_email_opt ) );
		if ( $acct_norm !== $admin_norm ) {
			error_log( '[AVI convert_report] ALERTE : ID WordPress 1 avec email session ≠ admin_email — arrêt.' );
			return null;
		}
		error_log( '[AVI convert_report] ALERTE : parcours B2C refusé sur l\'utilisateur ID 1.' );
		return null;
	}
	if ( (int) $wp_user_id <= 0 ) {
		return null;
	}

	if ( function_exists( 'avision_ensure_b2c_solo_account_meta' ) ) {
		avision_ensure_b2c_solo_account_meta( $wp_user_id );
	}

	global $wpdb;
	$profiles_table = 'wor1515_avi_saas_profiles';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) !== $profiles_table ) {
		$profiles_table = $wpdb->prefix . 'avi_saas_profiles';
	}
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) !== $profiles_table ) {
		return null;
	}

	// avi_saas_profiles.user_id = ID avi_saas_users en MySQL (même base que la ligne profil), pas l’ID WordPress.
	// Ne pas appeler Avision_Database::get_or_create_saas_user ici : avec DATABASE_URL il cible PostgreSQL et renvoie
	// un id sans correspondance dans wor1515_/prefix_avi_saas_users → effet « tout le monde en user_id 1 » côté MySQL.
	$saas_users_table = 'wor1515_avi_saas_users';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) !== $saas_users_table ) {
		$saas_users_table = $wpdb->prefix . 'avi_saas_users';
	}
	$saas_user_id_for_profile = 0;
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) === $saas_users_table ) {
		// Dernière ligne SaaS pour cet e-mail (évite de rattacher au id=1 erroné si doublons / données legacy).
		$ex_su = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM `{$saas_users_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id DESC LIMIT 1",
				$email
			)
		);
		if ( $ex_su > 0 ) {
			$saas_user_id_for_profile = $ex_su;
		} else {
			$ins_ok = $wpdb->insert( $saas_users_table, array( 'email' => $email ), array( '%s' ) );
			if ( $ins_ok && (int) $wpdb->insert_id > 0 ) {
				$saas_user_id_for_profile = (int) $wpdb->insert_id;
			}
		}
		if ( $saas_user_id_for_profile > 0 ) {
			$owner_email = (string) $wpdb->get_var(
				$wpdb->prepare( "SELECT email FROM `{$saas_users_table}` WHERE id = %d LIMIT 1", $saas_user_id_for_profile )
			);
			if ( strtolower( trim( $owner_email ) ) !== strtolower( trim( $email ) ) ) {
				error_log( '[AVI convert_report] ALERTE : user_id SaaS ' . (int) $saas_user_id_for_profile . ' incohérent (email BDD ≠ rapport) — recalcul par email.' );
				$saas_user_id_for_profile = (int) $wpdb->get_var(
					$wpdb->prepare(
						"SELECT id FROM `{$saas_users_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id DESC LIMIT 1",
						$email
					)
				);
				if ( $saas_user_id_for_profile <= 0 ) {
					$ins_ok2 = $wpdb->insert( $saas_users_table, array( 'email' => $email ), array( '%s' ) );
					if ( $ins_ok2 && (int) $wpdb->insert_id > 0 ) {
						$saas_user_id_for_profile = (int) $wpdb->insert_id;
					}
				}
			}
		}
		if ( (int) $saas_user_id_for_profile === 1 ) {
			$row1_email = (string) $wpdb->get_var( "SELECT email FROM `{$saas_users_table}` WHERE id = 1 LIMIT 1" );
			if ( strtolower( trim( $row1_email ) ) !== strtolower( trim( $email ) ) ) {
				error_log( '[AVI convert_report] ALERTE : Tentative d\'attribution user_id SaaS 1 bloquée (ligne 1 ≠ email rapport).' );
				$saas_user_id_for_profile = (int) $wpdb->get_var(
					$wpdb->prepare(
						"SELECT id FROM `{$saas_users_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) AND id <> 1 ORDER BY id DESC LIMIT 1",
						$email
					)
				);
				if ( $saas_user_id_for_profile <= 0 ) {
					$ins_ok3 = $wpdb->insert( $saas_users_table, array( 'email' => $email ), array( '%s' ) );
					if ( $ins_ok3 && (int) $wpdb->insert_id > 0 ) {
						$saas_user_id_for_profile = (int) $wpdb->insert_id;
					} else {
						$saas_user_id_for_profile = 0;
					}
				}
			}
		}
	}
	if ( (int) $saas_user_id_for_profile === 1 && isset( $saas_users_table ) && $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) === $saas_users_table ) {
		$final_row1_email = (string) $wpdb->get_var( "SELECT email FROM `{$saas_users_table}` WHERE id = 1 LIMIT 1" );
		if ( strtolower( trim( $final_row1_email ) ) !== strtolower( trim( $email ) ) ) {
			error_log( '[AVI convert_report] ALERTE : user_id SaaS 1 final incohérent (ligne 1 ≠ email rapport) — correction id SaaS ou INSERT.' );
			$fix_saas_id = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT id FROM `{$saas_users_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) AND id > 1 ORDER BY id DESC LIMIT 1",
					$email
				)
			);
			if ( $fix_saas_id > 0 ) {
				$saas_user_id_for_profile = $fix_saas_id;
			} else {
				$ins_fix_saas = $wpdb->insert( $saas_users_table, array( 'email' => $email ), array( '%s' ) );
				if ( $ins_fix_saas && (int) $wpdb->insert_id > 0 ) {
					$saas_user_id_for_profile = (int) $wpdb->insert_id;
				} else {
					return null;
				}
			}
		}
	}

	// Flux convert rapport → profil : langue toujours 'en' pour ce flux ; user_id profil = avi_saas_users.id (SaaS) ; wp_user_id = wp_users.ID (WordPress).
	$convert_b2c_profile_lang = 'en';
	$has_user_id_col_global   = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'user_id' ) ) === 'user_id' );
	if ( $has_user_id_col_global && $saas_user_id_for_profile <= 0 ) {
		return null;
	}

	$existing_profile_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$profiles_table}` WHERE email = %s LIMIT 1", $email ) );
	if ( $existing_profile_id > 0 ) {
		$has_wp_user_id_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'wp_user_id' ) ) === 'wp_user_id' );
		$has_user_id_col    = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'user_id' ) ) === 'user_id' );
		$has_uuid_col       = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'uuid' ) ) === 'uuid' );
		$has_phone_col      = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'phone' ) ) === 'phone' );
		$has_type_col       = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'type' ) ) === 'type' );
		$has_country_col    = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'country' ) ) === 'country' );
		$has_neigh_col      = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'neighborhood' ) ) === 'neighborhood' );
		$has_lang_col_ex    = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'lang' ) ) === 'lang' );
		$has_gtok_ex        = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'google_access_token' ) ) === 'google_access_token' );
		$has_gref_ex        = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'google_refresh_token' ) ) === 'google_refresh_token' );
		$has_gloc_ex        = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'google_location_name' ) ) === 'google_location_name' );
		$has_gexp_ex        = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'google_token_expires_at' ) ) === 'google_token_expires_at' );
		$has_radar_plan_ex  = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'radar_plan' ) ) === 'radar_plan' );
		$has_max_kw_ex      = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'max_keywords' ) ) === 'max_keywords' );

		$update_data = [
			'business_name' => $business_name !== '' ? $business_name : 'Profil ' . $email,
			'city'          => $city !== '' ? $city : '-',
			'category'      => $category !== '' ? $category : 'Client',
			'email'         => $email,
		];
		$update_format = [ '%s', '%s', '%s', '%s' ];
		if ( $has_wp_user_id_col ) {
			$update_data['wp_user_id'] = $wp_user_id;
			$update_format[] = '%d';
		}
		if ( $has_user_id_col ) {
			$update_data['user_id'] = $saas_user_id_for_profile;
			$update_format[]        = '%d';
		}
		if ( $has_phone_col ) {
			$update_data['phone'] = $phone;
			$update_format[] = '%s';
		}
		if ( $has_type_col ) {
			$update_data['type'] = 'premium';
			$update_format[] = '%s';
		}
		if ( $has_country_col ) {
			$update_data['country'] = $country;
			$update_format[] = '%s';
		}
		if ( $has_neigh_col ) {
			$update_data['neighborhood'] = $neighborhood;
			$update_format[] = '%s';
		}
		if ( $has_lang_col_ex ) {
			$update_data['lang'] = $convert_b2c_profile_lang;
			$update_format[]     = '%s';
		}
		if ( $has_radar_plan_ex ) {
			$update_data['radar_plan'] = 'offert';
			$update_format[]           = '%s';
		}
		if ( $has_max_kw_ex ) {
			$update_data['max_keywords'] = 20;
			$update_format[]             = '%d';
		}
		$has_created_at_ex = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'created_at' ) ) === 'created_at' );
		if ( $has_created_at_ex ) {
			$update_data['created_at'] = current_time( 'mysql' );
			$update_format[]           = '%s';
		}
		$has_updated_at_ex_up = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'updated_at' ) ) === 'updated_at' );
		if ( $has_updated_at_ex_up ) {
			$update_data['updated_at'] = current_time( 'mysql' );
			$update_format[]           = '%s';
		}
		if ( $has_gtok_ex ) {
			$update_data['google_access_token'] = null;
			$update_format[]                    = '%s';
		}
		if ( $has_gref_ex ) {
			$update_data['google_refresh_token'] = null;
			$update_format[]                     = '%s';
		}
		if ( $has_gexp_ex ) {
			$update_data['google_token_expires_at'] = null;
			$update_format[]                      = '%s';
		}
		if ( $has_gloc_ex ) {
			$update_data['google_location_name'] = null;
			$update_format[]                     = '%s';
		}
		$wpdb->update(
			$profiles_table,
			$update_data,
			[ 'id' => $existing_profile_id ],
			$update_format,
			[ '%d' ]
		);

		$row_gl = '';
		if ( $has_gloc_ex ) {
			$row_gl = (string) $wpdb->get_var( $wpdb->prepare( "SELECT google_location_name FROM `{$profiles_table}` WHERE id = %d LIMIT 1", $existing_profile_id ) );
		}
		$row_tok = '';
		avision_convert_profile_sync_pending_google_locations( $row_tok, $existing_profile_id, $profiles_table, $meta_google_location !== '' ? $meta_google_location : $row_gl );

		$existing_pid = '';
		if ( $has_uuid_col ) {
			$existing_pid = (string) $wpdb->get_var( $wpdb->prepare( "SELECT uuid FROM `{$profiles_table}` WHERE id = %d LIMIT 1", $existing_profile_id ) );
			if ( $existing_pid === '' ) {
				$existing_pid = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'pid_', true );
				$wpdb->update(
					$profiles_table,
					[ 'uuid' => $existing_pid ],
					[ 'id' => $existing_profile_id ],
					[ '%s' ],
					[ '%d' ]
				);
			}
		}
		if ( $existing_pid === '' && function_exists( 'avi_get_or_create_profile_uuid' ) ) {
			$existing_pid = (string) avi_get_or_create_profile_uuid( $existing_profile_id );
		}
		if ( function_exists( 'avision_force_immediate_scan' ) ) {
			avision_force_immediate_scan( $existing_profile_id );
		}
		if ( function_exists( 'wp_set_current_user' ) && function_exists( 'wp_set_auth_cookie' ) ) {
			wp_set_current_user( (int) $wp_user_id );
			wp_set_auth_cookie( (int) $wp_user_id, true, '' );
		}
		if ( function_exists( 'error_log' ) && ( ! $is_new_wp_user || $plain_password_for_welcome === '' ) ) {
			error_log(
				'[BREVO] Skip welcome (existing profile branch): is_new_wp_user=' . ( $is_new_wp_user ? '1' : '0' )
				. ' internal_pw=' . ( $plain_password_for_welcome !== '' ? 'set' : 'empty' )
				. ' rid=' . $rid
				. ' — welcome email uniquement si compte WP nouveau (lien d’activation, pas de MDP en clair).'
			);
		}
		if ( $is_new_wp_user && $plain_password_for_welcome !== '' ) {
			if ( function_exists( 'error_log' ) ) {
				error_log( '[BREVO] Envoi welcome (magic link) — nouveau compte WP, rid=' . $rid );
			}
			// Confirmer que la liaison wp_user_id est réellement écrite en base avant l'envoi.
			if ( $has_wp_user_id_col ) {
				$hard = $wpdb->update(
					$profiles_table,
					[ 'wp_user_id' => $wp_user_id ],
					[ 'id' => $existing_profile_id ],
					[ '%d' ],
					[ '%d' ]
				);
				$confirmed = (int) $wpdb->get_var( $wpdb->prepare( "SELECT wp_user_id FROM `{$profiles_table}` WHERE id = %d LIMIT 1", $existing_profile_id ) );
				if ( $confirmed !== (int) $wp_user_id ) {
					error_log( '[BREVO] Annulé : wp_user_id non confirmé sur avi_saas_profiles (id=' . (int) $existing_profile_id . '). hard_update=' . ( is_int( $hard ) ? $hard : -1 ) );
					return null;
				}
			}
			avision_send_welcome_b2c_email_after_convert( $wp_account_email, $business_name, $existing_pid, $plain_password_for_welcome, $existing_profile_id );
		}
		return array(
			'profile_id'             => $existing_profile_id,
			'wp_user_id'             => $wp_user_id,
			'pid'                    => $existing_pid,
			'email'                  => $email,
			'brevo_welcome_pass_len' => ( $is_new_wp_user && $plain_password_for_welcome !== '' ) ? strlen( $plain_password_for_welcome ) : 0,
		);
	}

	$has_wp_user_id_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'wp_user_id' ) ) === 'wp_user_id' );
	$has_user_id_col    = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'user_id' ) ) === 'user_id' );
	$has_uuid_col       = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'uuid' ) ) === 'uuid' );
	$has_phone_col      = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'phone' ) ) === 'phone' );
	$has_type_col       = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'type' ) ) === 'type' );
	$has_country_col    = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'country' ) ) === 'country' );
	$has_neigh_col      = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'neighborhood' ) ) === 'neighborhood' );
	$has_radar_plan_col           = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'radar_plan' ) ) === 'radar_plan' );
	$has_max_keywords_col         = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'max_keywords' ) ) === 'max_keywords' );
	$has_google_access_token_col  = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'google_access_token' ) ) === 'google_access_token' );
	$has_google_refresh_token_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'google_refresh_token' ) ) === 'google_refresh_token' );
	$has_google_location_name_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'google_location_name' ) ) === 'google_location_name' );
	$has_lang_col                 = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'lang' ) ) === 'lang' );
	$has_google_token_expires_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'google_token_expires_at' ) ) === 'google_token_expires_at' );
	$has_created_at_col           = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'created_at' ) ) === 'created_at' );
	$has_updated_at_col           = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'updated_at' ) ) === 'updated_at' );

	$pid = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'pid_', true );

	$insert_data = array(
		'email'         => $email,
		'business_name' => $business_name !== '' ? $business_name : 'Profil ' . $email,
		'city'          => $city !== '' ? $city : '-',
		'category'      => $category !== '' ? $category : 'Client',
	);
	$insert_format = array( '%s', '%s', '%s', '%s' );

	if ( $has_wp_user_id_col ) {
		$insert_data['wp_user_id'] = $wp_user_id;
		$insert_format[]           = '%d';
	}
	if ( $has_user_id_col ) {
		$insert_data['user_id'] = $saas_user_id_for_profile;
		$insert_format[]        = '%d';
	}
	if ( $has_uuid_col ) {
		$insert_data['uuid'] = $pid;
		$insert_format[]     = '%s';
	}
	if ( $has_phone_col ) {
		$insert_data['phone'] = $phone;
		$insert_format[]      = '%s';
	}
	if ( $has_type_col ) {
		$insert_data['type'] = 'premium';
		$insert_format[]     = '%s';
	}
	if ( $has_country_col ) {
		$insert_data['country'] = $country;
		$insert_format[]        = '%s';
	}
	if ( $has_neigh_col ) {
		$insert_data['neighborhood'] = $neighborhood;
		$insert_format[]             = '%s';
	}
	// Profil B2C vierge : plan offert (20 mots-clés), sans liaison Google préexistante.
	if ( $has_radar_plan_col ) {
		$insert_data['radar_plan'] = 'offert';
		$insert_format[]         = '%s';
	}
	if ( $has_max_keywords_col ) {
		$insert_data['max_keywords'] = 20;
		$insert_format[]            = '%d';
	}
	if ( $has_google_access_token_col ) {
		$insert_data['google_access_token'] = null;
		$insert_format[]                    = '%s';
	}
	if ( $has_google_refresh_token_col ) {
		$insert_data['google_refresh_token'] = null;
		$insert_format[]                     = '%s';
	}
	if ( $has_google_location_name_col ) {
		$insert_data['google_location_name'] = null;
		$insert_format[]                     = '%s';
	}
	if ( $has_google_token_expires_col ) {
		$insert_data['google_token_expires_at'] = null;
		$insert_format[]                        = '%s';
	}
	if ( $has_lang_col ) {
		$insert_data['lang'] = $convert_b2c_profile_lang;
		$insert_format[]     = '%s';
	}
	$insert_now_mysql = current_time( 'mysql' );
	if ( $has_created_at_col ) {
		$insert_data['created_at'] = $insert_now_mysql;
		$insert_format[]           = '%s';
	}
	if ( $has_updated_at_col ) {
		$insert_data['updated_at'] = $insert_now_mysql;
		$insert_format[]           = '%s';
	}

	$has_guardian_active_col     = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'guardian_active' ) ) === 'guardian_active' );
	$has_pilot_guardian_mode_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'pilot_guardian_mode' ) ) === 'pilot_guardian_mode' );
	$has_is_sealed_col           = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'is_sealed' ) ) === 'is_sealed' );
	$has_current_level_col       = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'current_level' ) ) === 'current_level' );
	if ( $has_guardian_active_col ) {
		$insert_data['guardian_active'] = 0;
		$insert_format[]              = '%d';
	}
	if ( $has_pilot_guardian_mode_col ) {
		$insert_data['pilot_guardian_mode'] = 0;
		$insert_format[]                  = '%d';
	}
	if ( $has_is_sealed_col ) {
		$insert_data['is_sealed'] = 0;
		$insert_format[]        = '%d';
	}
	if ( $has_current_level_col ) {
		$insert_data['current_level'] = 1;
		$insert_format[]            = '%d';
	}

	$ok = $wpdb->insert( $profiles_table, $insert_data, $insert_format );
	if ( ! $ok ) {
		return null;
	}

	$new_profile_id = (int) $wpdb->insert_id;
	if ( $new_profile_id > 0 && function_exists( 'avision_force_immediate_scan' ) ) {
		avision_force_immediate_scan( $new_profile_id );
	}
	if ( $new_profile_id > 0 ) {
		$ins_tok = $has_google_access_token_col ? ( isset( $insert_data['google_access_token'] ) ? (string) $insert_data['google_access_token'] : '' ) : '';
		$ins_loc = $has_google_location_name_col ? ( isset( $insert_data['google_location_name'] ) ? (string) $insert_data['google_location_name'] : '' ) : '';
		avision_convert_profile_sync_pending_google_locations( $ins_tok, $new_profile_id, $profiles_table, $ins_loc );
	}
	// ANTI-RÉGRESSION — insert_initial_keywords_for_profile :
	// Ce bloc est la SEULE source d'insertion des 3 keywords initiaux (category+city,
	// category+neighborhood, category+proximité). NE JAMAIS dupliquer cet appel.
	// Le 2ème appel à avision_force_immediate_scan (supprimé) déclenchait
	// avision_generate_niche_keywords_matrix → insérait "Meilleur [category] [city]"
	// en doublon. Un seul appel insert_initial_keywords_for_profile suffit.
	// NE JAMAIS réintroduire le doublon avision_force_immediate_scan ici.
	// AUTORISATION ANTHONY REQUISE POUR TOUTE MODIFICATION.
	if ( $new_profile_id > 0 && class_exists( '\Avision\Avision_Database' ) ) {
		$db_radar = new \Avision\Avision_Database();
		$db_radar->init();
		if ( method_exists( $db_radar, 'insert_initial_keywords_for_profile' ) ) {
			$db_radar->insert_initial_keywords_for_profile( $new_profile_id );
		}
	}

	if ( function_exists( 'error_log' ) && ( ! $is_new_wp_user || $plain_password_for_welcome === '' ) ) {
		error_log(
			'[BREVO] Skip welcome (new insert branch): is_new_wp_user=' . ( $is_new_wp_user ? '1' : '0' )
			. ' internal_pw=' . ( $plain_password_for_welcome !== '' ? 'set' : 'empty' )
			. ' rid=' . $rid
			. ' — welcome email uniquement si wp_create_user vient d’être exécuté (lien d’activation, pas de MDP en clair).'
		);
	}
	if ( function_exists( 'wp_set_current_user' ) && function_exists( 'wp_set_auth_cookie' ) ) {
		wp_set_current_user( (int) $wp_user_id );
		wp_set_auth_cookie( (int) $wp_user_id, true, '' );
	}
	if ( $is_new_wp_user && $plain_password_for_welcome !== '' ) {
		if ( function_exists( 'error_log' ) ) {
			error_log( '[BREVO] Envoi welcome (magic link) — nouveau compte WP, rid=' . $rid );
		}
		// Confirmer que la liaison wp_user_id est réellement écrite en base avant l'envoi.
		if ( $has_wp_user_id_col ) {
			$hard2 = $wpdb->update(
				$profiles_table,
				[ 'wp_user_id' => $wp_user_id ],
				[ 'id' => $new_profile_id ],
				[ '%d' ],
				[ '%d' ]
			);
			$confirmed2 = (int) $wpdb->get_var( $wpdb->prepare( "SELECT wp_user_id FROM `{$profiles_table}` WHERE id = %d LIMIT 1", $new_profile_id ) );
			if ( $confirmed2 !== (int) $wp_user_id ) {
				error_log( '[BREVO] Annulé : wp_user_id non confirmé sur avi_saas_profiles (id=' . (int) $new_profile_id . '). hard_update=' . ( is_int( $hard2 ) ? $hard2 : -1 ) );
				return null;
			}
		}
		avision_send_welcome_b2c_email_after_convert( $wp_account_email, $business_name, $pid, $plain_password_for_welcome, $new_profile_id );
	}

	return array(
		'profile_id'             => $new_profile_id,
		'wp_user_id'             => $wp_user_id,
		'pid'                    => $pid,
		'email'                  => $email,
		'brevo_welcome_pass_len' => ( $is_new_wp_user && $plain_password_for_welcome !== '' ) ? strlen( $plain_password_for_welcome ) : 0,
	);
}

/**
 * Force le scan DataForSEO des mots-clés seeds (pending) pour un profil, sans attendre le worker CRON.
 * À utiliser après bypass admin : le fallback thème insère des lignes en pending sans J0 ; la file globale
 * traitait autrefois par id ASC, ce qui pouvait repousser indéfiniment les nouveaux PID.
 *
 * @param int $profile_id avi_saas_profiles.id.
 * @return int Nombre de mots-clés passés dans run_single_keyword_scan (hors insert_initial qui fait déjà un J0 si table vide).
 */
function avision_force_immediate_scan( $profile_id ) {
	$profile_id = (int) $profile_id;
	error_log( '[AVI force_immediate_scan] start profile_id=' . $profile_id );
	if ( $profile_id <= 0 || ! class_exists( '\Avision\Avision_Database' ) ) {
		error_log( '[AVI force_immediate_scan] abort: invalid profile_id or Avision_Database missing' );
		return 0;
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	if ( ! method_exists( $db, 'get_saas_profile_for_sync' ) || ! method_exists( $db, 'run_single_keyword_scan' ) ) {
		error_log( '[AVI force_immediate_scan] abort: get_saas_profile_for_sync or run_single_keyword_scan missing' );
		return 0;
	}
	$profile = $db->get_saas_profile_for_sync( $profile_id );
	if ( empty( $profile ) || ! is_array( $profile ) ) {
		error_log( '[AVI force_immediate_scan] abort: get_saas_profile_for_sync empty for profile_id=' . $profile_id );
		return 0;
	}
	$country_log = isset( $profile['country'] ) ? (string) $profile['country'] : '';
	$biz_log     = isset( $profile['business_name'] ) ? (string) $profile['business_name'] : '';
	error_log( '[AVI force_immediate_scan] profile loaded country=' . $country_log . ' business_name=' . substr( $biz_log, 0, 80 ) );
	// Table vide : insert des 3 seeds + J0 synchrone (DataForSEO) dans la classe.
	// ═══════════════════════════════════════════════════════════════
	// ANTI-RÉGRESSION — Fallback neighborhood depuis pré-inscription (règle #151)
	// avision_force_immediate_scan est le vrai chemin de génération KW.
	// Si neighborhood=NULL (GBP sans sublocality + webhook sans metadata),
	// récupérer depuis wor1515_avi_pre_inscription avant génération KW2.
	// Sans ce fallback : KW2 [category] [neighborhood] jamais généré.
	// NE JAMAIS supprimer sans autorisation Anthony.
	// ═══════════════════════════════════════════════════════════════
	if ( empty( $profile['neighborhood'] ) && ! empty( $profile['email'] ) ) {
		global $wpdb;
		$pre_neighborhood = $wpdb->get_var( $wpdb->prepare(
			"SELECT neighborhood FROM wor1515_avi_pre_inscription
         WHERE email = %s AND neighborhood != ''
         ORDER BY created_at DESC LIMIT 1",
			$profile['email']
		) );
		if ( ! empty( $pre_neighborhood ) ) {
			$profile['neighborhood'] = sanitize_text_field( (string) $pre_neighborhood );
			$wpdb->update(
				$wpdb->prefix . 'avi_saas_profiles',
				array( 'neighborhood' => $profile['neighborhood'] ),
				array( 'id' => $profile_id ),
				array( '%s' ),
				array( '%d' )
			);
			error_log( '[AVI] Neighborhood fallback pré-inscription profile_id=' . $profile_id . ' neighborhood=' . $profile['neighborhood'] );
			// insert_initial_keywords_for_profile lit le profil via get_saas_profile_for_sync — UPDATE suffit.
			if ( method_exists( $db, 'set_profile_override' ) ) {
				$db->set_profile_override( $profile_id, $profile );
			}
		}
	}
	if ( method_exists( $db, 'insert_initial_keywords_for_profile' ) ) {
		$insert_ran = $db->insert_initial_keywords_for_profile( $profile_id );
		error_log( '[AVI force_immediate_scan] insert_initial_keywords_for_profile returned ' . ( $insert_ran ? 'true (insert+J0)' : 'false (keywords already exist or skip)' ) . ' profile_id=' . $profile_id );
	}
	$limit = (int) apply_filters( 'avision_force_immediate_scan_keyword_limit', 3 );
	$limit = max( 1, min( 20, $limit ) );
	if ( ! method_exists( $db, 'get_pending_keywords_for_profile_scan' ) ) {
		error_log( '[AVI force_immediate_scan] abort: get_pending_keywords_for_profile_scan missing' );
		return 0;
	}
	$keywords = $db->get_pending_keywords_for_profile_scan( $profile_id, $limit );
	error_log( '[AVI force_immediate_scan] pending keywords count=' . count( $keywords ) . ' limit=' . $limit . ' profile_id=' . $profile_id );
	if ( empty( $keywords ) && isset( $GLOBALS['wpdb'] ) && is_object( $GLOBALS['wpdb'] ) ) {
		global $wpdb;
		$tbl_kw = $wpdb->prefix . 'avi_saas_keywords';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl_kw ) ) === $tbl_kw ) {
			$snap = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT id, keyword, scan_status, current_rank, rank_init FROM `{$tbl_kw}` WHERE profile_id = %d ORDER BY id ASC LIMIT 15",
					$profile_id
				),
				ARRAY_A
			);
			error_log( '[AVI force_immediate_scan] diagnostic (no pending): keyword rows snapshot=' . wp_json_encode( is_array( $snap ) ? $snap : array() ) );
		}
	}
	$scanned = 0;
	foreach ( $keywords as $kw ) {
		if ( ! is_array( $kw ) ) {
			continue;
		}
		$kid = isset( $kw['id'] ) ? (int) $kw['id'] : 0;
		$kws = isset( $kw['keyword'] ) ? (string) $kw['keyword'] : '';
		$tc  = isset( $kw['target_city'] ) ? (string) $kw['target_city'] : '';
		error_log( '[AVI force_immediate_scan] DataForSEO → run_single_keyword_scan profile_id=' . $profile_id . ' kw_id=' . $kid . ' keyword="' . $kws . '" target_city="' . $tc . '"' );
		$db->run_single_keyword_scan( $kw, $profile );
		error_log( '[AVI force_immediate_scan] run_single_keyword_scan finished kw_id=' . $kid );
		$scanned++;
	}
	error_log( '[AVI force_immediate_scan] end profile_id=' . $profile_id . ' scanned=' . $scanned );
	return $scanned;
}

/**
 * Fusionne les champs onboarding du corps JSON dans les metadata Stripe (écrase / complète la pré-inscription).
 * Aligne company_name pour les webhooks / création profil (GPS 3.2) qui lisent company_name.
 *
 * @param array $metadata Metadata déjà remplie (pre_token, user_type, etc.).
 * @param array $params   Corps JSON ou paramètres de requête.
 * @return array
 */
function avision_stripe_merge_onboarding_fields_into_metadata( array $metadata, array $params ) {
	$string_fields = array( 'business_name', 'category', 'city', 'country', 'neighborhood', 'phone', 'agency_name', 'contact_name' );
	foreach ( $string_fields as $field ) {
		if ( ! array_key_exists( $field, $params ) ) {
			continue;
		}
		$raw = $params[ $field ];
		$val = is_string( $raw ) ? trim( $raw ) : ( is_scalar( $raw ) ? trim( (string) $raw ) : '' );
		if ( $val === '' ) {
			continue;
		}
		$safe = sanitize_text_field( $val );
		if ( function_exists( 'mb_substr' ) ) {
			$safe = mb_substr( $safe, 0, 500 );
		} else {
			$safe = substr( $safe, 0, 500 );
		}
		$metadata[ $field ] = $safe;
	}
	if ( array_key_exists( 'email', $params ) ) {
		$em = sanitize_email( (string) $params['email'] );
		if ( is_email( $em ) ) {
			$metadata['email'] = $em;
		}
	}
	// Langue checkout / e-mails (query ?lang=en ou corps JSON) — alias language.
	$allowed_checkout_langs = array( 'fr', 'en', 'es' );
	foreach ( array( 'lang', 'language' ) as $lang_key ) {
		if ( ! array_key_exists( $lang_key, $params ) ) {
			continue;
		}
		$raw = $params[ $lang_key ];
		$lv  = is_string( $raw ) ? strtolower( trim( $raw ) ) : ( is_scalar( $raw ) ? strtolower( trim( (string) $raw ) ) : '' );
		if ( strpos( $lv, '-' ) !== false ) {
			$lv = strtolower( trim( explode( '-', $lv, 2 )[0] ) );
		}
		if ( ! in_array( $lv, $allowed_checkout_langs, true ) ) {
			continue;
		}
		$metadata['lang'] = $lv;
		break;
	}
	if ( array_key_exists( 'volume_fiches', $params ) && $params['volume_fiches'] !== '' && null !== $params['volume_fiches'] ) {
		$metadata['volume_fiches'] = (string) max( 0, absint( $params['volume_fiches'] ) );
	}
	// Webhook checkout.session : create_saas_profile utilise company_name.
	if ( ! empty( $metadata['business_name'] ) ) {
		$metadata['company_name'] = $metadata['business_name'];
	} elseif ( ! empty( $metadata['agency_name'] ) ) {
		$metadata['company_name'] = $metadata['agency_name'];
	}
	return $metadata;
}

/**
 * Normalise les metadata renvoyées par l’API Stripe (Checkout Session) après json_decode.
 * Alignement business_name / agency_name → company_name pour la création de profil (GPS 3.2 / 3.2.5).
 *
 * @param array $meta Metadata brutes (clés string, valeurs souvent string).
 * @return array
 */
function avision_stripe_normalize_checkout_metadata( array $meta ) {
	$out = array();
	foreach ( $meta as $k => $v ) {
		if ( ! is_string( $k ) || $k === '' ) {
			continue;
		}
		if ( is_scalar( $v ) || null === $v ) {
			$out[ $k ] = trim( (string) $v );
		}
	}
	if ( ! empty( $out['business_name'] ) && empty( $out['company_name'] ) ) {
		$out['company_name'] = $out['business_name'];
	}
	if ( empty( $out['company_name'] ) && ! empty( $out['agency_name'] ) ) {
		$out['company_name'] = $out['agency_name'];
	}
	return $out;
}

/**
 * Langue B2C dérivée des metadata Checkout (clés lang / language, whitelist fr|en|es).
 *
 * @param array $meta Metadata (session Stripe ou checkout_meta).
 * @return string fr|en|es
 */
function avision_stripe_resolve_checkout_lang( array $meta ) {
	$m = avision_stripe_normalize_checkout_metadata( $meta );
	foreach ( array( 'lang', 'language' ) as $k ) {
		if ( empty( $m[ $k ] ) ) {
			continue;
		}
		$lv = strtolower( trim( (string) $m[ $k ] ) );
		if ( strpos( $lv, '-' ) !== false ) {
			$lv = strtolower( trim( explode( '-', $lv, 2 )[0] ) );
		}
		if ( in_array( $lv, array( 'fr', 'en', 'es' ), true ) ) {
			return $lv;
		}
	}
	$cc = isset( $m['country'] ) ? strtoupper( trim( (string) $m['country'] ) ) : '';
	if ( in_array( $cc, array( 'GB', 'US' ), true ) ) {
		return 'en';
	}
	if ( $cc === 'ES' ) {
		return 'es';
	}
	return 'fr';
}

/**
 * Écrit lang / language sur avi_saas_profiles (wor1515_ ou préfixe) à partir des métadonnées Checkout Stripe.
 *
 * @param int   $profile_id ID avi_saas_profiles.
 * @param array $meta       Metadata session (clés lang, language, country).
 * @return void
 */
function avision_stripe_persist_profile_lang_column( $profile_id, array $meta ) {
	$profile_id = (int) $profile_id;
	if ( $profile_id <= 0 || ! isset( $GLOBALS['wpdb'] ) || ! $GLOBALS['wpdb'] ) {
		return;
	}
	$lang = function_exists( 'avision_stripe_resolve_checkout_lang' ) ? avision_stripe_resolve_checkout_lang( $meta ) : 'fr';
	global $wpdb;
	$candidates = array( 'wor1515_avi_saas_profiles', $wpdb->prefix . 'avi_saas_profiles' );
	foreach ( array_unique( $candidates ) as $tbl ) {
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) ) !== $tbl ) {
			continue;
		}
		$exists = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$tbl}` WHERE id = %d LIMIT 1", $profile_id ) );
		if ( $exists !== $profile_id ) {
			continue;
		}
		$set_lang = array();
		$fmt_lang = array();
		foreach ( array( 'lang', 'language' ) as $col ) {
			if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$tbl}` LIKE %s", $col ) ) !== $col ) {
				continue;
			}
			$set_lang[ $col ] = $lang;
			$fmt_lang[]       = '%s';
		}
		if ( ! empty( $set_lang ) ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nom de table validé SHOW TABLES.
			$wpdb->update( $tbl, $set_lang, array( 'id' => $profile_id ), $fmt_lang, array( '%d' ) );
		}
		break;
	}
}

/**
 * Champs profil SaaS extraits des metadata Checkout (retour bridge session_id).
 *
 * @param array $meta Metadata (normalisées ou brutes).
 * @return array{business_name:string,city:string,category:string,neighborhood:string,country:string,phone:string}
 */
function avision_stripe_profile_fields_from_metadata( array $meta ) {
	$m = avision_stripe_normalize_checkout_metadata( $meta );
	$bn      = isset( $m['company_name'] ) ? trim( (string) $m['company_name'] ) : '';
	if ( $bn === '' && ! empty( $m['business_name'] ) ) {
		$bn = trim( (string) $m['business_name'] );
	}
	if ( $bn === '' && ! empty( $m['agency_name'] ) ) {
		$bn = trim( (string) $m['agency_name'] );
	}
	$ct      = isset( $m['city'] ) ? trim( (string) $m['city'] ) : '';
	$cat     = isset( $m['category'] ) ? trim( (string) $m['category'] ) : '';
	$nbh     = isset( $m['neighborhood'] ) ? trim( (string) $m['neighborhood'] ) : '';
	$country = isset( $m['country'] ) ? strtoupper( trim( (string) $m['country'] ) ) : '';
	if ( ! in_array( $country, array( 'FR', 'GB', 'US', 'ES' ), true ) ) {
		$country = 'FR';
	}
	$phone = isset( $m['phone'] ) && (string) $m['phone'] !== '' ? sanitize_text_field( trim( (string) $m['phone'] ) ) : '';
	return array(
		'business_name' => $bn,
		'city'          => $ct,
		'category'      => $cat,
		'neighborhood'  => $nbh,
		'country'       => $country,
		'phone'         => $phone,
	);
}

/**
 * Met à jour city, neighborhood, category, phone, business_name sur avi_saas_profiles (wor1515_ ou préfixe) depuis les champs profil checkout.
 *
 * @param int   $profile_id ID ligne profil.
 * @param array $pf         Retour de avision_stripe_profile_fields_from_metadata().
 * @return void
 */
function avision_stripe_mysql_merge_profile_fields_from_metadata( $profile_id, array $pf ) {
	$profile_id = (int) $profile_id;
	if ( $profile_id <= 0 || ! isset( $GLOBALS['wpdb'] ) || ! $GLOBALS['wpdb'] ) {
		return;
	}
	global $wpdb;
	$city         = isset( $pf['city'] ) ? sanitize_text_field( (string) $pf['city'] ) : '';
	$neighborhood = isset( $pf['neighborhood'] ) ? sanitize_text_field( (string) $pf['neighborhood'] ) : '';
	$category     = isset( $pf['category'] ) ? sanitize_text_field( (string) $pf['category'] ) : '';
	$phone        = isset( $pf['phone'] ) ? sanitize_text_field( (string) $pf['phone'] ) : '';
	$bn           = isset( $pf['business_name'] ) ? sanitize_text_field( (string) $pf['business_name'] ) : '';
	if ( $category === '' ) {
		$category = __( 'Client', 'avision-dashboard' );
	}
	$set = array();
	$fmt = array();
	if ( $city !== '' ) {
		$set['city'] = $city;
		$fmt[]       = '%s';
	}
	if ( $neighborhood !== '' ) {
		$set['neighborhood'] = $neighborhood;
		$fmt[]             = '%s';
	}
	$set['category'] = $category;
	$fmt[]           = '%s';
	if ( $phone !== '' ) {
		$set['phone'] = $phone;
		$fmt[]        = '%s';
	}
	if ( $bn !== '' ) {
		$set['business_name'] = $bn;
		$fmt[]                = '%s';
	}
	if ( empty( $set ) ) {
		return;
	}
	$candidates = array( 'wor1515_avi_saas_profiles', $wpdb->prefix . 'avi_saas_profiles' );
	foreach ( array_unique( $candidates ) as $tbl ) {
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) ) !== $tbl ) {
			continue;
		}
		$exists = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$tbl}` WHERE id = %d LIMIT 1", $profile_id ) );
		if ( $exists !== $profile_id ) {
			continue;
		}
		$set_f = array();
		$fmt_f = array();
		foreach ( $set as $col => $val ) {
			if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$tbl}` LIKE %s", $col ) ) === $col ) {
				$set_f[ $col ] = $val;
				$fmt_f[]       = '%s';
			}
		}
		if ( ! empty( $set_f ) ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- nom de table validé SHOW TABLES.
			$wpdb->update( $tbl, $set_f, array( 'id' => $profile_id ), $fmt_f, array( '%d' ) );
		}
		break;
	}
}

/**
 * Fusionne query + corps JSON pour les routes REST (évite $params = [] si get_json_params est vide mais le body contenait des clés).
 *
 * @param \WP_REST_Request $request Requête.
 * @return array<string,mixed>
 */
function avision_rest_merge_request_params( \WP_REST_Request $request ) {
	$base = $request->get_params();
	if ( ! is_array( $base ) ) {
		$base = array();
	}
	$json = $request->get_json_params();
	if ( ! is_array( $json ) ) {
		$json = array();
	}
	// Le JSON du body prime sur les query args.
	return array_merge( $base, $json );
}

/**
 * Crée une session Stripe Checkout avec metadata (données onboarding + add-ons) et success_url vers connexion.
 * Clé secrète : get_option( 'avision_stripe_secret_key' ) ou constante AVISION_STRIPE_SECRET_KEY.
 */
function avision_rest_stripe_create_checkout_session( \WP_REST_Request $request ) {
	$params = avision_rest_merge_request_params( $request );

	// Champs formulaire onboarding (corps JSON) — get_param + repli $params fusionné (query + body).
	$business_name = sanitize_text_field( (string) $request->get_param( 'business_name' ) );
	if ( $business_name === '' && isset( $params['business_name'] ) ) {
		$business_name = sanitize_text_field( (string) $params['business_name'] );
	}
	$category = sanitize_text_field( (string) $request->get_param( 'category' ) );
	if ( $category === '' && isset( $params['category'] ) ) {
		$category = sanitize_text_field( (string) $params['category'] );
	}
	$city = sanitize_text_field( (string) $request->get_param( 'city' ) );
	if ( $city === '' && isset( $params['city'] ) ) {
		$city = sanitize_text_field( (string) $params['city'] );
	}
	$country = sanitize_text_field( (string) $request->get_param( 'country' ) );
	if ( $country === '' && isset( $params['country'] ) ) {
		$country = sanitize_text_field( (string) $params['country'] );
	}
	$neighborhood = sanitize_text_field( (string) $request->get_param( 'neighborhood' ) );
	if ( $neighborhood === '' && isset( $params['neighborhood'] ) ) {
		$neighborhood = sanitize_text_field( (string) $params['neighborhood'] );
	}
	$phone = sanitize_text_field( (string) $request->get_param( 'phone' ) );
	if ( $phone === '' && isset( $params['phone'] ) ) {
		$phone = sanitize_text_field( (string) $params['phone'] );
	}
	$agency_name = sanitize_text_field( (string) $request->get_param( 'agency_name' ) );
	if ( $agency_name === '' && isset( $params['agency_name'] ) ) {
		$agency_name = sanitize_text_field( (string) $params['agency_name'] );
	}
	$contact_name = sanitize_text_field( (string) $request->get_param( 'contact_name' ) );
	if ( $contact_name === '' && isset( $params['contact_name'] ) ) {
		$contact_name = sanitize_text_field( (string) $params['contact_name'] );
	}
	$volume_fiches_param = $request->get_param( 'volume_fiches' );
	if ( ( $volume_fiches_param === null || $volume_fiches_param === '' ) && isset( $params['volume_fiches'] ) ) {
		$volume_fiches_param = $params['volume_fiches'];
	}
	$volume_fiches = ( $volume_fiches_param !== null && $volume_fiches_param !== '' ) ? (string) max( 0, absint( $volume_fiches_param ) ) : '';

    $pre_token = isset( $params['pre_token'] ) ? sanitize_text_field( $params['pre_token'] ) : $request->get_param( 'pre_token' );
    $rid = isset( $params['rid'] ) ? sanitize_text_field( $params['rid'] ) : $request->get_param( 'rid' );
    if ( empty( $rid ) ) {
        $rid = isset( $params['report_id'] ) ? sanitize_text_field( $params['report_id'] ) : $request->get_param( 'report_id' );
    }
    $client_email_param = isset( $params['email'] ) ? sanitize_email( (string) $params['email'] ) : sanitize_email( (string) $request->get_param( 'email' ) );
    if ( $pre_token === '' && $rid !== '' && function_exists( 'avision_ensure_demo_guest_pre_token_from_rid' ) ) {
        $guest_tok = avision_ensure_demo_guest_pre_token_from_rid( $rid, is_email( $client_email_param ) ? $client_email_param : '' );
        if ( is_string( $guest_tok ) && $guest_tok !== '' ) {
            $pre_token = $guest_tok;
        } else {
            return new \WP_REST_Response(
                [ 'success' => false, 'error' => 'audit_session_expired', 'message' => __( 'Session d’audit expirée ou introuvable. Relancez une analyse.', 'avision-dashboard' ) ],
                400
            );
        }
    }
    $addon_guardian = isset( $params['addon_guardian'] ) ? $params['addon_guardian'] : $request->get_param( 'addon_guardian' );
    $addon_radar    = isset( $params['addon_radar'] ) ? $params['addon_radar'] : $request->get_param( 'addon_radar' );
    $upgrade_type   = isset( $params['upgrade_type'] ) ? sanitize_text_field( $params['upgrade_type'] ) : sanitize_text_field( (string) $request->get_param( 'upgrade_type' ) );
    $profile_id     = isset( $params['profile_id'] ) ? absint( $params['profile_id'] ) : absint( $request->get_param( 'profile_id' ) );
    $upgrade_cfg    = function_exists( 'avision_get_upgrade_checkout_config' ) ? avision_get_upgrade_checkout_config( $upgrade_type ) : null;

    $metadata = [];
    if ( ! empty( $pre_token ) && function_exists( 'avision_get_pre_inscription_by_token' ) ) {
        $pre = avision_get_pre_inscription_by_token( $pre_token );
        if ( $pre ) {
            $metadata['user_type'] = strtoupper( $pre['type'] );
            $metadata['email']      = isset( $pre['email'] ) ? $pre['email'] : '';
            if ( $pre['type'] === 'b2c' ) {
                $metadata['business_name'] = isset( $pre['business_name'] ) ? (string) $pre['business_name'] : '';
                $metadata['company_name']  = $metadata['business_name'];
                $metadata['city']          = isset( $pre['city'] ) ? $pre['city'] : '';
                $metadata['country']       = isset( $pre['country'] ) ? $pre['country'] : '';
                $metadata['neighborhood']  = isset( $pre['neighborhood'] ) ? $pre['neighborhood'] : '';
                $metadata['category']      = isset( $pre['category'] ) ? $pre['category'] : '';
                if ( isset( $pre['phone'] ) && (string) $pre['phone'] !== '' ) {
                    $metadata['phone'] = (string) $pre['phone'];
                }
            } else {
                $metadata['company_name'] = isset( $pre['agency_name'] ) ? $pre['agency_name'] : '';
                $metadata['contact_name'] = isset( $pre['contact_name'] ) ? $pre['contact_name'] : '';
                $metadata['volume_fiches'] = isset( $pre['volume_fiches'] ) ? (string) $pre['volume_fiches'] : '';
            }
        }
    }
    $metadata = avision_stripe_merge_onboarding_fields_into_metadata( $metadata, $params );

    // Surcharge explicite depuis get_param (priorité sur pré-inscription vide ou $params mal fusionné).
    if ( $business_name !== '' ) {
        $metadata['business_name'] = $business_name;
        $metadata['company_name']  = $business_name;
    }
    if ( $category !== '' ) {
        $metadata['category'] = $category;
    }
    if ( $city !== '' ) {
        $metadata['city'] = $city;
    }
    if ( $country !== '' ) {
        $metadata['country'] = $country;
    }
    if ( $neighborhood !== '' ) {
        $metadata['neighborhood'] = $neighborhood;
    }
    if ( $phone !== '' ) {
        $metadata['phone'] = $phone;
    }
    if ( $agency_name !== '' ) {
        $metadata['agency_name']  = $agency_name;
        $metadata['company_name'] = $agency_name;
    }
    if ( $contact_name !== '' ) {
        $metadata['contact_name'] = $contact_name;
    }
    if ( $volume_fiches !== '' ) {
        $metadata['volume_fiches'] = $volume_fiches;
    }

    $metadata['addon_guardian'] = ( $addon_guardian === true || $addon_guardian === 'true' || $addon_guardian === '1' ) ? 'true' : 'false';
    $metadata['addon_radar']   = ( $addon_radar === true || $addon_radar === 'true' || $addon_radar === '1' ) ? 'true' : 'false';
    if ( ! empty( $upgrade_type ) ) {
        $metadata['upgrade_type'] = $upgrade_type;
    }
    if ( $profile_id > 0 ) {
        $metadata['profile_id'] = (string) $profile_id;
    }
    if ( ! empty( $pre_token ) ) {
        $metadata['pre_token'] = $pre_token;
    }
    if ( ! empty( $rid ) ) {
        $metadata['report_id'] = $rid;
        $metadata['type']      = 'b2c_onboarding';
    }

    $metadata = apply_filters( 'avision_stripe_checkout_metadata', $metadata, $request );

    if ( function_exists( 'avision_stripe_secret_key_configured' ) && ! avision_stripe_secret_key_configured() ) {
        return new \WP_REST_Response( [ 'success' => false, 'error' => 'stripe_not_configured' ], 503 );
    }
	$secret_key = defined( 'AVISION_STRIPE_SECRET_KEY' ) && is_string( AVISION_STRIPE_SECRET_KEY ) && trim( AVISION_STRIPE_SECRET_KEY ) !== ''
		? trim( AVISION_STRIPE_SECRET_KEY )
		: ( is_string( get_option( 'avision_stripe_secret_key', '' ) ) ? trim( (string) get_option( 'avision_stripe_secret_key', '' ) ) : '' );

	$landing_slug = 'premium-dashboard';
	if ( ! empty( $metadata['user_type'] ) && strtoupper( trim( (string) $metadata['user_type'] ) ) === 'B2B' ) {
		$landing_slug = 'agency-dashboard';
	}
    $default_success = function_exists( 'avision_stripe_build_default_success_url' )
        ? avision_stripe_build_default_success_url( $pre_token, $landing_slug )
        : trailingslashit( home_url( '/premium-dashboard/' ) ) . '?status=success&session_id={CHECKOUT_SESSION_ID}';
    $success_url = apply_filters( 'avision_stripe_checkout_success_url', $default_success, $request, $pre_token );
	$success_url = avision_stripe_ensure_success_url_session_placeholder( $success_url );
    $cancel_url  = apply_filters( 'avision_stripe_checkout_cancel_url', home_url( '/' ), $request );

    $unit_amount = (int) apply_filters( 'avision_stripe_unit_amount', 9900, $request );
    $product_name = apply_filters( 'avision_stripe_line_product_name', 'AVI — Accès Plateforme', $request );
    $product_desc = apply_filters( 'avision_stripe_line_description', 'Visibilité locale & Mode Gardien', $request );
    $checkout_mode = 'payment';
    if ( is_array( $upgrade_cfg ) ) {
        $unit_amount = (int) ( $upgrade_cfg['unit_amount'] ?? $unit_amount );
        $product_name = isset( $upgrade_cfg['name'] ) ? (string) $upgrade_cfg['name'] : $product_name;
        $product_desc = isset( $upgrade_cfg['description'] ) ? (string) $upgrade_cfg['description'] : $product_desc;
        $checkout_mode = isset( $upgrade_cfg['mode'] ) ? (string) $upgrade_cfg['mode'] : 'subscription';
    }

    $price_id = '';
    if ( is_array( $upgrade_cfg ) && ! empty( $upgrade_cfg['price_id'] ) ) {
        $price_id = (string) $upgrade_cfg['price_id'];
    } elseif ( function_exists( 'avision_stripe_get_price_id' ) ) {
        $price_id = avision_stripe_get_price_id();
    }
    $line_items = array();
    if ( is_string( $price_id ) && $price_id !== '' && strpos( $price_id, 'price_' ) === 0 ) {
        $line_items = array(
            array(
                'price'    => $price_id,
                'quantity' => 1,
            ),
        );
    } else {
        $line_items = array(
            array(
                'price_data' => array(
                    'currency'     => 'eur',
                    'product_data' => array(
                        'name'        => $product_name,
                        'description' => $product_desc,
                    ),
                    'unit_amount' => $unit_amount,
                ),
                'quantity' => 1,
            ),
        );
        if ( $checkout_mode === 'subscription' ) {
            $line_items[0]['price_data']['recurring'] = array( 'interval' => 'month' );
        }
    }

	// Métadonnées session Stripe (checkout) : alignement explicite des champs audit / profil (écrasent toute entrée vide résiduelle).
	$metadata['city']         = isset( $metadata['city'] ) ? (string) $metadata['city'] : '';
	$metadata['neighborhood'] = isset( $metadata['neighborhood'] ) ? (string) $metadata['neighborhood'] : '';
	$metadata['category']    = isset( $metadata['category'] ) ? (string) $metadata['category'] : '';
	$metadata['phone']       = isset( $metadata['phone'] ) ? (string) $metadata['phone'] : '';
	if ( $city !== '' ) {
		$metadata['city'] = $city;
	}
	if ( $neighborhood !== '' ) {
		$metadata['neighborhood'] = $neighborhood;
	}
	if ( $category !== '' ) {
		$metadata['category'] = $category;
	}
	if ( $phone !== '' ) {
		$metadata['phone'] = $phone;
	}

    $body = array(
        'mode'                 => $checkout_mode,
        'success_url'          => $success_url,
        'cancel_url'           => $cancel_url,
        'payment_method_types' => array( 'card' ),
        'line_items'           => $line_items,
        'metadata'             => $metadata,
    );
    if ( ! empty( $pre_token ) ) {
        $body['client_reference_id'] = substr( $pre_token, 0, 255 );
    }
    if ( ! empty( $metadata['email'] ) && is_email( $metadata['email'] ) ) {
        $body['customer_email'] = $metadata['email'];
    }

    $response = wp_remote_post(
        'https://api.stripe.com/v1/checkout/sessions',
        [
            'headers' => [
                'Authorization' => 'Bearer ' . $secret_key,
                'Content-Type'  => 'application/x-www-form-urlencoded',
            ],
            'body'    => _avision_stripe_checkout_body( $body ),
            'timeout' => 15,
        ]
    );

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

    if ( is_wp_error( $response ) || $code < 200 || $code >= 300 ) {
        $err = ( is_array( $data ) && isset( $data['error']['message'] ) ) ? $data['error']['message'] : 'stripe_error';
        return new \WP_REST_Response( [ 'success' => false, 'error' => $err ], $code >= 400 ? $code : 502 );
    }
    if ( empty( $data['url'] ) ) {
        return new \WP_REST_Response( [ 'success' => false, 'error' => 'no_checkout_url' ], 502 );
    }
    return new \WP_REST_Response( [ 'success' => true, 'url' => $data['url'] ], 200 );
}

/**
 * Encode le body pour Stripe Checkout Session (format attendu par l'API Stripe).
 * Stripe accepte application/json pour certaines versions ; on utilise form-encoded pour compatibilité.
 *
 * @param array $body Tableau associatif (metadata, line_items, success_url, etc.).
 * @return string Body encodé pour wp_remote_post.
 */
function _avision_stripe_checkout_body( $body ) {
    $flat = [];
    $flat['success_url'] = isset( $body['success_url'] ) ? $body['success_url'] : '';
    $flat['cancel_url']  = isset( $body['cancel_url'] ) ? $body['cancel_url'] : '';
    $flat['mode']        = isset( $body['mode'] ) ? $body['mode'] : 'payment';
    $flat['payment_method_types[0]'] = 'card';
    $flat['allow_promotion_codes'] = 'true';
    if ( ! empty( $body['customer_email'] ) ) {
        $flat['customer_email'] = $body['customer_email'];
    }
    if ( ! empty( $body['client_reference_id'] ) ) {
        $flat['client_reference_id'] = $body['client_reference_id'];
    }
    $li0 = isset( $body['line_items'][0] ) ? $body['line_items'][0] : array();
    if ( ! empty( $li0['price'] ) && is_string( $li0['price'] ) && strpos( $li0['price'], 'price_' ) === 0 ) {
        $flat['line_items[0][price]']    = $li0['price'];
        $flat['line_items[0][quantity]'] = isset( $li0['quantity'] ) ? (int) $li0['quantity'] : 1;
    } else {
        $flat['line_items[0][price_data][currency]'] = 'eur';
        $flat['line_items[0][price_data][product_data][name]'] = isset( $body['line_items'][0]['price_data']['product_data']['name'] ) ? $body['line_items'][0]['price_data']['product_data']['name'] : 'AVI — Accès Plateforme';
        $flat['line_items[0][price_data][product_data][description]'] = isset( $body['line_items'][0]['price_data']['product_data']['description'] ) ? $body['line_items'][0]['price_data']['product_data']['description'] : '';
        $flat['line_items[0][price_data][unit_amount]'] = isset( $body['line_items'][0]['price_data']['unit_amount'] ) ? $body['line_items'][0]['price_data']['unit_amount'] : 9900;
        if ( ! empty( $body['line_items'][0]['price_data']['recurring']['interval'] ) ) {
            $flat['line_items[0][price_data][recurring][interval]'] = $body['line_items'][0]['price_data']['recurring']['interval'];
        }
        $flat['line_items[0][quantity]'] = isset( $body['line_items'][0]['quantity'] ) ? (int) $body['line_items'][0]['quantity'] : 1;
    }
    foreach ( isset( $body['metadata'] ) ? $body['metadata'] : [] as $k => $v ) {
        $flat[ 'metadata[' . $k . ']' ] = is_string( $v ) ? $v : (string) $v;
    }
    return http_build_query( $flat );
}

/**
 * Résout l’UUID profil (pid URL) pour une ligne avi_saas_profiles.id.
 *
 * @param int $profile_id ID interne profil.
 * @return string UUID normalisé ou chaîne vide.
 */
function avision_resolve_profile_uuid_for_checkout( $profile_id ) {
	$profile_id = (int) $profile_id;
	if ( $profile_id <= 0 ) {
		return '';
	}
	global $wpdb;
	if ( ! isset( $wpdb ) || ! $wpdb ) {
		return '';
	}
	$candidates = array( 'wor1515_avi_saas_profiles', $wpdb->prefix . 'avi_saas_profiles' );
	foreach ( array_unique( $candidates ) as $tbl ) {
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) ) !== $tbl ) {
			continue;
		}
		if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$tbl}` LIKE %s", 'uuid' ) ) !== 'uuid' ) {
			continue;
		}
		$u = (string) $wpdb->get_var( $wpdb->prepare( "SELECT uuid FROM `{$tbl}` WHERE id = %d LIMIT 1", $profile_id ) );
		if ( $u !== '' && preg_match( '/^[a-f0-9\-]{32,36}$/i', $u ) ) {
			return strtolower( trim( $u ) );
		}
	}
	if ( function_exists( 'avi_get_or_create_profile_uuid' ) ) {
		$u = (string) avi_get_or_create_profile_uuid( $profile_id );
		if ( $u !== '' && preg_match( '/^[a-f0-9\-]{32,36}$/i', $u ) ) {
			return strtolower( trim( $u ) );
		}
	}
	return '';
}

/**
 * Dernier recours MySQL : ligne avi_saas_profiles pour l’e-mail Stripe si la voie Avision_Database (PDO) échoue ou décale d’une autre BDD.
 *
 * @param string $email E-mail payeur.
 * @param array  $meta  Métadonnées checkout (company_name, city, category, country, neighborhood).
 * @return int ID profil ou 0.
 */
function avision_stripe_last_resort_mysql_profile_for_email( $email, array $meta = array() ) {
	if ( ! is_email( $email ) || ! isset( $GLOBALS['wpdb'] ) || ! $GLOBALS['wpdb'] ) {
		return 0;
	}
	$meta = avision_stripe_normalize_checkout_metadata( $meta );
	$pf   = avision_stripe_profile_fields_from_metadata( $meta );
	global $wpdb;

	$profiles_table = 'wor1515_avi_saas_profiles';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) !== $profiles_table ) {
		$profiles_table = $wpdb->prefix . 'avi_saas_profiles';
	}
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) !== $profiles_table ) {
		return 0;
	}

	$has_email_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'email' ) ) === 'email' );
	if ( $has_email_col ) {
		$existing = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM `{$profiles_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id DESC LIMIT 1",
				$email
			)
		);
		if ( $existing > 0 ) {
			return $existing;
		}
	} else {
		$has_user_id_join = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'user_id' ) ) === 'user_id' );
		$saas_users_table = 'wor1515_avi_saas_users';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) !== $saas_users_table ) {
			$saas_users_table = $wpdb->prefix . 'avi_saas_users';
		}
		if ( $has_user_id_join && $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) === $saas_users_table ) {
			$existing = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT p.id FROM `{$profiles_table}` p INNER JOIN `{$saas_users_table}` u ON p.user_id = u.id WHERE LOWER(TRIM(u.email)) = LOWER(TRIM(%s)) ORDER BY p.id DESC LIMIT 1",
					$email
				)
			);
			if ( $existing > 0 ) {
				return $existing;
			}
		}
	}

	$saas_users_table = 'wor1515_avi_saas_users';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) !== $saas_users_table ) {
		$saas_users_table = $wpdb->prefix . 'avi_saas_users';
	}
	$saas_user_id = 0;
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $saas_users_table ) ) === $saas_users_table
		&& $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$saas_users_table}` LIKE %s", 'email' ) ) === 'email' ) {
		$saas_user_id = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM `{$saas_users_table}` WHERE LOWER(TRIM(email)) = LOWER(TRIM(%s)) ORDER BY id DESC LIMIT 1",
				$email
			)
		);
		if ( $saas_user_id <= 0 ) {
			$ins_u = $wpdb->insert( $saas_users_table, array( 'email' => $email ), array( '%s' ) );
			if ( $ins_u ) {
				$saas_user_id = (int) $wpdb->insert_id;
			}
		}
	}

	$has_user_id_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'user_id' ) ) === 'user_id' );
	if ( $has_user_id_col && $saas_user_id <= 0 ) {
		if ( function_exists( 'error_log' ) ) {
			error_log( '[AVI STRIPE] Last resort : impossible d’obtenir avi_saas_users.id pour ' . $email );
		}
		return 0;
	}

	$bn      = $pf['business_name'];
	$ct      = $pf['city'];
	$cat     = $pf['category'];
	$nbh     = $pf['neighborhood'];
	$country = $pf['country'];

	$uuid = function_exists( 'wp_generate_uuid4' )
		? wp_generate_uuid4()
		: sprintf(
			'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
			wp_rand( 0, 0xffff ),
			wp_rand( 0, 0xffff ),
			wp_rand( 0, 0xffff ),
			wp_rand( 0, 0x0fff ) | 0x4000,
			wp_rand( 0, 0x3fff ) | 0x8000,
			wp_rand( 0, 0xffff ),
			wp_rand( 0, 0xffff ),
			wp_rand( 0, 0xffff )
		);

	$insert_data   = array();
	$insert_format = array();

	if ( $has_email_col ) {
		$insert_data['email'] = $email;
		$insert_format[]      = '%s';
	}
	if ( $has_user_id_col ) {
		$insert_data['user_id'] = $saas_user_id;
		$insert_format[]        = '%d';
	}

	$lang_for_row = function_exists( 'avision_stripe_resolve_checkout_lang' ) ? avision_stripe_resolve_checkout_lang( $meta ) : 'fr';
	$optional_string = array(
		'business_name' => $bn !== '' ? $bn : __( 'Profil', 'avision-dashboard' ),
		'city'          => $ct !== '' ? $ct : '-',
		'category'      => $cat !== '' ? $cat : __( 'Client', 'avision-dashboard' ),
		'neighborhood'  => $nbh !== '' ? $nbh : '',
		'country'       => $country,
		'type'          => 'premium',
		'uuid'          => $uuid,
		'status'        => 'active',
		'lang'          => $lang_for_row,
		'language'      => $lang_for_row,
		'radar_plan'    => 'offert',
	);
	foreach ( $optional_string as $col => $val ) {
		if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", $col ) ) !== $col ) {
			continue;
		}
		$insert_data[ $col ] = $val;
		$insert_format[]     = '%s';
	}

	$optional_int = array(
		'current_level'       => 1,
		'score'               => 0,
		'guardian_active'     => 0,
		'pilot_guardian_mode' => 0,
		'is_sealed'           => 0,
		'max_keywords'        => 20,
		'is_first_sync'       => 1,
	);
	foreach ( $optional_int as $col => $val ) {
		if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", $col ) ) !== $col ) {
			continue;
		}
		$insert_data[ $col ] = (int) $val;
		$insert_format[]     = '%d';
	}

	if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'phone' ) ) === 'phone' ) {
		$insert_data['phone'] = $pf['phone'] !== '' ? sanitize_text_field( $pf['phone'] ) : '';
		$insert_format[]      = '%s';
	}

	$now = current_time( 'mysql' );
	foreach ( array( 'created_at', 'updated_at' ) as $ts_col ) {
		if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", $ts_col ) ) === $ts_col ) {
			$insert_data[ $ts_col ] = $now;
			$insert_format[]        = '%s';
		}
	}

	if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'wp_user_id' ) ) === 'wp_user_id' ) {
		$wp_uid_ins = ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() && function_exists( 'get_current_user_id' ) ) ? (int) get_current_user_id() : 0;
		$insert_data['wp_user_id'] = $wp_uid_ins;
		$insert_format[]           = '%d';
	}

	$ok = $wpdb->insert( $profiles_table, $insert_data, $insert_format );
	if ( ! $ok || (int) $wpdb->insert_id <= 0 ) {
		if ( function_exists( 'error_log' ) ) {
			error_log( '[AVI STRIPE] Last resort INSERT profil échoué : ' . $wpdb->last_error );
		}
		return 0;
	}

	$new_id = (int) $wpdb->insert_id;
	if ( $new_id > 0 && function_exists( 'avision_stripe_mysql_merge_profile_fields_from_metadata' ) ) {
		avision_stripe_mysql_merge_profile_fields_from_metadata( $new_id, $pf );
	}
	if ( $new_id > 0 && function_exists( 'avision_force_immediate_scan' ) ) {
		avision_force_immediate_scan( $new_id );
	}

	return $new_id;
}

/**
 * Retour Stripe Hosted Checkout sur le cockpit agence (chemin HTTP ou requête WP).
 *
 * @return bool
 */
function avision_stripe_is_agency_checkout_landing() {
	if ( isset( $_SERVER['REQUEST_URI'] ) ) {
		$uri  = function_exists( 'wp_unslash' ) ? wp_unslash( (string) $_SERVER['REQUEST_URI'] ) : (string) $_SERVER['REQUEST_URI'];
		$path = function_exists( 'wp_parse_url' ) ? wp_parse_url( $uri, PHP_URL_PATH ) : parse_url( $uri, PHP_URL_PATH );
		$path = is_string( $path ) ? $path : '';
		if ( $path !== '' && (
			strpos( $path, '/agency-dashboard' ) !== false ||
			strpos( $path, '/btob-onboarding' ) !== false
		) ) {
			return true;
		}
	}
	if ( function_exists( 'avision_is_agency_dashboard_query' ) && avision_is_agency_dashboard_query() ) {
		return true;
	}
	return false;
}

/**
 * Valide une session Checkout Stripe payée, crée la session SaaS, construit l’URL cockpit avec ?pid=.
 *
 * @param string $session_id ID session cs_….
 * @return array{success:bool,error?:string,redirect?:string,sid?:string|null,pid?:string,user_type?:string}
 */
function avision_complete_stripe_checkout_return( $session_id ) {
	$session_id = is_string( $session_id ) ? trim( sanitize_text_field( $session_id ) ) : '';
	$get_sid    = '';
	if ( isset( $_GET['session_id'] ) && is_string( $_GET['session_id'] ) && function_exists( 'wp_unslash' ) ) {
		$get_sid = sanitize_text_field( wp_unslash( $_GET['session_id'] ) );
	}
	if ( $session_id === '' || strpos( $session_id, 'cs_' ) !== 0 ) {
		return array( 'success' => false, 'error' => 'invalid_session_id' );
	}
	$secret_key = defined( 'AVISION_STRIPE_SECRET_KEY' ) && is_string( AVISION_STRIPE_SECRET_KEY ) && trim( AVISION_STRIPE_SECRET_KEY ) !== ''
		? trim( AVISION_STRIPE_SECRET_KEY )
		: ( is_string( get_option( 'avision_stripe_secret_key', '' ) ) ? trim( (string) get_option( 'avision_stripe_secret_key', '' ) ) : '' );
	if ( empty( $secret_key ) ) {
		return array( 'success' => false, 'error' => 'stripe_not_configured' );
	}
	// Session complète + client (email de secours si customer_details vide).
	$session_api_url = 'https://api.stripe.com/v1/checkout/sessions/' . rawurlencode( $session_id ) . '?expand%5B%5D=customer';
	$res             = wp_remote_get(
		$session_api_url,
		array(
			'headers' => array( 'Authorization' => 'Bearer ' . $secret_key ),
			'timeout' => 20,
		)
	);
	$code = wp_remote_retrieve_response_code( $res );
	$body = wp_remote_retrieve_body( $res );
	$data = json_decode( $body, true );
	if ( is_wp_error( $res ) || $code !== 200 || ! is_array( $data ) ) {
		return array( 'success' => false, 'error' => 'stripe_session_invalid' );
	}
	$payment_status = isset( $data['payment_status'] ) ? $data['payment_status'] : '';
	if ( $payment_status !== 'paid' ) {
		return array( 'success' => false, 'error' => 'payment_not_completed' );
	}
	$meta = isset( $data['metadata'] ) && is_array( $data['metadata'] ) ? $data['metadata'] : array();
	$meta = avision_stripe_normalize_checkout_metadata( $meta );
	$email = '';
	if ( isset( $data['customer_details'] ) && is_array( $data['customer_details'] ) && ! empty( $data['customer_details']['email'] ) ) {
		$email = (string) $data['customer_details']['email'];
	} elseif ( isset( $data['customer_email'] ) && $data['customer_email'] !== '' ) {
		$email = (string) $data['customer_email'];
	} elseif ( isset( $data['customer'] ) && is_array( $data['customer'] ) && ! empty( $data['customer']['email'] ) ) {
		$email = (string) $data['customer']['email'];
	} elseif ( ! empty( $meta['email'] ) ) {
		$email = (string) $meta['email'];
	}
	$email = is_email( $email ) ? $email : '';
	if ( $email === '' && function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
		$cu = wp_get_current_user();
		if ( $cu && $cu->ID && is_email( $cu->user_email ) ) {
			$email = $cu->user_email;
		}
	}
	if ( $email === '' ) {
		return array( 'success' => false, 'error' => 'no_email' );
	}
	$pf = avision_stripe_profile_fields_from_metadata( $meta );
	$user_type = isset( $meta['user_type'] ) ? strtoupper( trim( (string) $meta['user_type'] ) ) : 'B2C';
	// Tunnel B2B : success_url /agency-dashboard/ — forcer B2B même si metadata Stripe incomplète.
	if ( function_exists( 'avision_stripe_is_agency_checkout_landing' ) && avision_stripe_is_agency_checkout_landing() ) {
		$user_type = 'B2B';
	}
	$redirect_path = ( $user_type === 'B2B' ) ? '/agency-dashboard' : '/premium-dashboard';

	$sid                       = null;
	$profile_row_id            = 0;
	$is_new_profile_checkout   = false;
	global $wpdb;
	if ( isset( $wpdb ) && $wpdb ) {
		$tbl_chk = 'wor1515_avi_saas_profiles';
		$tbl_ok  = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl_chk ) ) === $tbl_chk;
		if ( ! $tbl_ok ) {
			$tbl_chk = $wpdb->prefix . 'avi_saas_profiles';
			$tbl_ok  = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl_chk ) ) === $tbl_chk;
		}
		if ( ! $tbl_ok && function_exists( 'error_log' ) ) {
			error_log( '[DB ERROR] wor1515_avi_saas_profiles / prefix avi_saas_profiles introuvable — PID impossible sans table profils.' );
		}
	}
	if ( class_exists( 'Avision\Avision_Database' ) ) {
		$db = new \Avision\Avision_Database();
		$db->init();
		$saas_user_id = $db->get_or_create_saas_user( $email );
		if ( $saas_user_id ) {
			$first = $db->get_first_saas_profile_by_user_id( $saas_user_id );
			if ( $first && ! empty( $first['id'] ) ) {
				$profile_row_id = (int) $first['id'];
			} elseif ( method_exists( $db, 'create_saas_profile' ) ) {
				$is_new_profile_checkout = true;
				$bn_final  = $pf['business_name'] !== '' ? $pf['business_name'] : __( 'Profil', 'avision-dashboard' );
				$ct_final  = $pf['city'] !== '' ? $pf['city'] : '-';
				$cat_final = $pf['category'] !== '' ? $pf['category'] : __( 'Client', 'avision-dashboard' );
				$ph_final  = isset( $pf['phone'] ) && (string) $pf['phone'] !== '' ? sanitize_text_field( (string) $pf['phone'] ) : '';
				$profile   = $db->create_saas_profile(
					$saas_user_id,
					$bn_final,
					$ct_final,
					$cat_final,
					null,
					null,
					$pf['neighborhood'],
					$pf['country'],
					$ph_final
				);
				if ( is_array( $profile ) && ! empty( $profile['id'] ) ) {
					$profile_row_id = (int) $profile['id'];
					$new_uuid = isset( $profile['uuid'] ) && is_string( $profile['uuid'] ) ? trim( $profile['uuid'] ) : '';
					if ( $new_uuid === '' && function_exists( 'avision_resolve_profile_uuid_for_checkout' ) ) {
						$new_uuid = (string) avision_resolve_profile_uuid_for_checkout( $profile_row_id );
					}
				} elseif ( $is_new_profile_checkout && function_exists( 'error_log' ) ) {
					$db_err = isset( $wpdb ) && $wpdb && $wpdb->last_error ? $wpdb->last_error : '(create_saas_profile sans id — voir PDO/classe DB)';
					error_log( '[AVI STRIPE] Création profil SaaS échouée : ' . $db_err );
				}
			}
			if ( $profile_row_id > 0 ) {
				$session_row = $db->create_saas_session( $saas_user_id, $profile_row_id, 604800 );
				if ( $session_row && ! empty( $session_row['token'] ) ) {
					$sid = $session_row['token'];
				}
			}
		}
	}

	if ( $profile_row_id <= 0 && is_email( $email ) ) {
		$profile_row_id = avision_stripe_last_resort_mysql_profile_for_email( $email, $meta );
		if ( $profile_row_id > 0 ) {
			$is_new_profile_checkout = true;
			if ( $sid === null && class_exists( 'Avision\Avision_Database' ) ) {
				$db_sess = new \Avision\Avision_Database();
				if ( method_exists( $db_sess, 'init' ) ) {
					$db_sess->init();
				}
				$suid_sess = method_exists( $db_sess, 'get_or_create_saas_user' ) ? $db_sess->get_or_create_saas_user( $email ) : null;
				if ( $suid_sess && method_exists( $db_sess, 'create_saas_session' ) ) {
					$session_row_lr = $db_sess->create_saas_session( (int) $suid_sess, $profile_row_id, 604800 );
					if ( $session_row_lr && ! empty( $session_row_lr['token'] ) ) {
						$sid = $session_row_lr['token'];
					}
				}
			}
		}
	}

	if ( $profile_row_id > 0 && function_exists( 'avision_stripe_persist_profile_lang_column' ) ) {
		avision_stripe_persist_profile_lang_column( $profile_row_id, $meta );
	}
	if ( $profile_row_id > 0 && function_exists( 'avision_stripe_mysql_merge_profile_fields_from_metadata' ) ) {
		avision_stripe_mysql_merge_profile_fields_from_metadata( $profile_row_id, $pf );
	}

	// Premium B2C : seeds Radar + scan immédiat pour ne pas arriver sur un cockpit vide.
	if ( $profile_row_id > 0 && $redirect_path === '/premium-dashboard' && function_exists( 'avision_force_immediate_scan' ) ) {
		avision_force_immediate_scan( $profile_row_id );
	}

	$pid_uuid = $profile_row_id > 0 ? avision_resolve_profile_uuid_for_checkout( $profile_row_id ) : '';

	// Email facturation Stripe : alimenter google_email (CRON / identité compte) si colonne présente et champ encore vide.
	if ( $profile_row_id > 0 && is_email( $email ) && isset( $GLOBALS['wpdb'] ) ) {
		global $wpdb;
		$pt = 'wor1515_avi_saas_profiles';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pt ) ) !== $pt ) {
			$pt = $wpdb->prefix . 'avi_saas_profiles';
		}
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pt ) ) === $pt ) {
			if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$pt}` LIKE %s", 'email' ) ) === 'email' ) {
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE `{$pt}` SET email = %s WHERE id = %d AND (email IS NULL OR TRIM(email) = '')",
						$email,
						$profile_row_id
					)
				);
				if ( ! empty( $wpdb->last_error ) && function_exists( 'error_log' ) ) {
					error_log( '[DB ERROR] ' . $wpdb->last_error );
				}
			}
			if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$pt}` LIKE %s", 'google_email' ) ) === 'google_email' ) {
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE `{$pt}` SET google_email = %s WHERE id = %d AND (google_email IS NULL OR TRIM(google_email) = '')",
						$email,
						$profile_row_id
					)
				);
				if ( ! empty( $wpdb->last_error ) && function_exists( 'error_log' ) ) {
					error_log( '[DB ERROR] ' . $wpdb->last_error );
				}
			}
		}
	}

	$redirect_url = trailingslashit( home_url( $redirect_path ) );
	$redirect_url = add_query_arg( 'status', 'success', $redirect_url );
	if ( $pid_uuid !== '' ) {
		$redirect_url = add_query_arg( 'pid', $pid_uuid, $redirect_url );
	}
	if ( $sid ) {
		$redirect_url = add_query_arg( 'sid', $sid, $redirect_url );
	}
	// Euréka / welcome : première entrée cockpit après création profil Checkout (premium ou agency).
	if ( $is_new_profile_checkout && ( $redirect_path === '/premium-dashboard' || $redirect_path === '/agency-dashboard' ) ) {
		$redirect_url = add_query_arg( 'first_login', '1', $redirect_url );
	}

	return array(
		'success'       => true,
		'redirect'      => $redirect_url,
		'sid'           => $sid,
		'pid'           => $pid_uuid,
		'user_type'     => ( $user_type === 'B2B' ) ? 'b2b' : 'b2c',
		'email'         => $email,
		'profile_id'    => $profile_row_id,
		'first_login'   => (bool) $is_new_profile_checkout,
		'checkout_meta' => $meta,
	);
}

/**
 * Reconstruit l’URL cockpit après paiement (pid UUID, sid SaaS, first_login).
 *
 * @param array $out         Retour enrichi (user_type, sid, first_login, pid optionnel).
 * @param int   $profile_id  ID profil SaaS final (prioritaire sur $out['profile_id']).
 * @return string
 */
function avision_stripe_build_post_checkout_redirect_url( array $out, $profile_id = null ) {
	$profile_id = $profile_id !== null ? (int) $profile_id : (int) ( $out['profile_id'] ?? 0 );
	$sid        = isset( $out['sid'] ) ? $out['sid'] : null;
	$first_login = ! empty( $out['first_login'] );
	$ut         = isset( $out['user_type'] ) ? strtolower( (string) $out['user_type'] ) : 'b2c';
	if ( function_exists( 'avision_stripe_is_agency_checkout_landing' ) && avision_stripe_is_agency_checkout_landing() ) {
		$ut = 'b2b';
	}
	$redirect_path = ( $ut === 'b2b' ) ? '/agency-dashboard' : '/premium-dashboard';
	$pid_uuid = '';
	if ( $profile_id > 0 && function_exists( 'avision_resolve_profile_uuid_for_checkout' ) ) {
		$pid_uuid = (string) avision_resolve_profile_uuid_for_checkout( $profile_id );
	}
	if ( $pid_uuid === '' && ! empty( $out['pid'] ) && is_string( $out['pid'] ) ) {
		$pid_uuid = trim( $out['pid'] );
	}
	$redirect_url = trailingslashit( home_url( $redirect_path ) );
	$redirect_url = add_query_arg( 'status', 'success', $redirect_url );
	if ( $pid_uuid !== '' ) {
		$redirect_url = add_query_arg( 'pid', $pid_uuid, $redirect_url );
	}
	if ( $sid ) {
		$redirect_url = add_query_arg( 'sid', $sid, $redirect_url );
	}
	if ( $first_login ) {
		$redirect_url = add_query_arg( 'first_login', '1', $redirect_url );
	}
	return $redirect_url;
}

/**
 * Après avision_complete_stripe_checkout_return : création utilisateur WP si besoin, cookie auth, liaison wp_user_id ↔ profil SaaS.
 *
 * @param array $out Retour avec success, email, profile_id.
 * @return array{wp_user_id:int,profile_id:int,error:string,welcome_plain_password:string}
 */
function avision_stripe_apply_wp_login_and_profile_link( array $out ) {
	$wp_user_id              = 0;
	$profile_id              = isset( $out['profile_id'] ) ? (int) $out['profile_id'] : 0;
	$error                   = '';
	$welcome_plain_password  = '';
	$email                   = isset( $out['email'] ) ? sanitize_email( (string) $out['email'] ) : '';
	if ( ! is_email( $email ) ) {
		return array(
			'wp_user_id'             => 0,
			'profile_id'             => $profile_id,
			'error'                  => 'invalid_email',
			'welcome_plain_password' => '',
		);
	}

	$wp_user = function_exists( 'get_user_by' ) ? get_user_by( 'email', $email ) : null;
	$wp_user_id = ( $wp_user && ! empty( $wp_user->ID ) ) ? (int) $wp_user->ID : 0;
	if ( $wp_user_id <= 0 && function_exists( 'wp_create_user' ) && function_exists( 'username_exists' ) ) {
		$username_base = sanitize_user( current( explode( '@', $email ) ), true );
		if ( $username_base === '' ) {
			$username_base = 'user';
		}
		$username = $username_base;
		$suffix   = 1;
		while ( username_exists( $username ) ) {
			$suffix++;
			$username = $username_base . $suffix;
		}
		$welcome_plain_password = wp_generate_password( 32, true, true );
		$created                  = wp_create_user( $username, $welcome_plain_password, $email );
		if ( ! is_wp_error( $created ) ) {
			$wp_user_id = (int) $created;
		} else {
			$welcome_plain_password = '';
			$error                    = $created instanceof \WP_Error ? $created->get_error_message() : 'wp_create_user_failed';
			$existing                 = function_exists( 'get_user_by' ) ? get_user_by( 'email', $email ) : null;
			$wp_user_id               = ( $existing && ! empty( $existing->ID ) ) ? (int) $existing->ID : 0;
		}
	}
	if ( $wp_user_id > 0 ) {
		$stripe_ut = isset( $out['user_type'] ) ? strtolower( (string) $out['user_type'] ) : 'b2c';
		if ( $stripe_ut === 'b2b' && function_exists( 'avision_ensure_b2b_agency_account_meta' ) ) {
			avision_ensure_b2b_agency_account_meta( $wp_user_id );
		} elseif ( function_exists( 'avision_ensure_b2c_solo_account_meta' ) ) {
			avision_ensure_b2c_solo_account_meta( $wp_user_id );
		}
	}

	// Cookie d’authentification le plus tôt possible pour que les fetch JS (ex. first-sync) voient is_user_logged_in.
	if ( $wp_user_id > 0 && function_exists( 'wp_set_current_user' ) && function_exists( 'wp_set_auth_cookie' ) ) {
		wp_set_current_user( $wp_user_id );
		wp_set_auth_cookie( $wp_user_id, true, '' );
	}

	if ( $profile_id <= 0 && class_exists( 'Avision\Avision_Database' ) ) {
		try {
			$db = new \Avision\Avision_Database();
			if ( method_exists( $db, 'init' ) ) {
				$db->init();
			}
			if ( method_exists( $db, 'get_or_create_saas_user' ) ) {
				$saas_user_id = (int) $db->get_or_create_saas_user( $email );
				if ( $saas_user_id > 0 && method_exists( $db, 'get_first_saas_profile_by_user_id' ) ) {
					$first = $db->get_first_saas_profile_by_user_id( $saas_user_id );
					if ( is_array( $first ) && ! empty( $first['id'] ) ) {
						$profile_id = (int) $first['id'];
					} elseif ( method_exists( $db, 'create_saas_profile' ) ) {
						$cm       = isset( $out['checkout_meta'] ) && is_array( $out['checkout_meta'] ) ? $out['checkout_meta'] : array();
						$pf_retry = avision_stripe_profile_fields_from_metadata( $cm );
						$ph_retry = isset( $pf_retry['phone'] ) && (string) $pf_retry['phone'] !== '' ? sanitize_text_field( (string) $pf_retry['phone'] ) : '';
						$profile  = $db->create_saas_profile(
							$saas_user_id,
							$pf_retry['business_name'] !== '' ? $pf_retry['business_name'] : __( 'Profil', 'avision-dashboard' ),
							$pf_retry['city'] !== '' ? $pf_retry['city'] : '-',
							$pf_retry['category'] !== '' ? $pf_retry['category'] : __( 'Client', 'avision-dashboard' ),
							null,
							null,
							$pf_retry['neighborhood'],
							$pf_retry['country'],
							$ph_retry
						);
						if ( is_array( $profile ) && ! empty( $profile['id'] ) ) {
							$profile_id = (int) $profile['id'];
						}
					}
				}
			}
		} catch ( \Throwable $eProvision ) {
			$error = 'provision_' . get_class( $eProvision );
			if ( function_exists( 'error_log' ) ) {
				error_log( '[AVI STRIPE] Provisioning SaaS forcé: exception ' . get_class( $eProvision ) );
			}
		}
	}

	if ( $profile_id > 0 && function_exists( 'avision_stripe_persist_profile_lang_column' ) && isset( $out['checkout_meta'] ) && is_array( $out['checkout_meta'] ) ) {
		avision_stripe_persist_profile_lang_column( $profile_id, $out['checkout_meta'] );
	}

	if ( $profile_id > 0 && isset( $GLOBALS['wpdb'] ) && $GLOBALS['wpdb'] && $wp_user_id > 0 ) {
		global $wpdb;
		$profiles_table = 'wor1515_avi_saas_profiles';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) !== $profiles_table ) {
			$profiles_table = $wpdb->prefix . 'avi_saas_profiles';
		}
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) === $profiles_table ) {
			$has_wp_user_id_col = ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'wp_user_id' ) ) === 'wp_user_id' );
			if ( $has_wp_user_id_col ) {
				$wpdb->update(
					$profiles_table,
					array( 'wp_user_id' => $wp_user_id ),
					array( 'id' => $profile_id ),
					array( '%d' ),
					array( '%d' )
				);
				$confirmed_wp_uid = (int) $wpdb->get_var( $wpdb->prepare( "SELECT wp_user_id FROM `{$profiles_table}` WHERE id = %d LIMIT 1", $profile_id ) );
				if ( $confirmed_wp_uid !== (int) $wp_user_id ) {
					if ( function_exists( 'error_log' ) ) {
						error_log( '[AVI STRIPE] Liaison wp_user_id non confirmée sur profil id=' . (int) $profile_id . ' table=' . $profiles_table );
					}
				}
			} elseif ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$profiles_table}` LIKE %s", 'user_id' ) ) === 'user_id' ) {
				$wpdb->update(
					$profiles_table,
					array( 'user_id' => $wp_user_id ),
					array( 'id' => $profile_id ),
					array( '%d' ),
					array( '%d' )
				);
			}
		}
	}

	return array(
		'wp_user_id'             => $wp_user_id,
		'profile_id'             => $profile_id,
		'error'                  => $error,
		'welcome_plain_password' => $welcome_plain_password,
	);
}

/**
 * Envoie l’email de bienvenue après premier profil créé via le retour Stripe (évite les doublons au refresh).
 *
 * @param array $out  Retour de avision_complete_stripe_checkout_return (first_login, user_type, email, checkout_meta).
 * @param array $link Retour de avision_stripe_apply_wp_login_and_profile_link (profile_id, welcome_plain_password).
 * @return void
 */
function avision_trigger_stripe_checkout_welcome_email( array $out, array $link ) {
	if ( empty( $out['first_login'] ) ) {
		return;
	}
	$ut = isset( $out['user_type'] ) ? strtolower( (string) $out['user_type'] ) : 'b2c';
	if ( $ut !== 'b2c' ) {
		return;
	}
	$profile_id = isset( $link['profile_id'] ) ? (int) $link['profile_id'] : 0;
	$email        = isset( $out['email'] ) ? sanitize_email( (string) $out['email'] ) : '';
	if ( $profile_id <= 0 || ! is_email( $email ) ) {
		return;
	}
	$tkey = 'avi_welcome_sent_stripe_' . $profile_id;
	if ( function_exists( 'get_transient' ) && get_transient( $tkey ) ) {
		return;
	}
	$cm = isset( $out['checkout_meta'] ) && is_array( $out['checkout_meta'] ) ? $out['checkout_meta'] : array();
	$pf = avision_stripe_profile_fields_from_metadata( $cm );
	$bn = $pf['business_name'] !== '' ? $pf['business_name'] : '';
	$pid_uuid = function_exists( 'avision_resolve_profile_uuid_for_checkout' ) ? (string) avision_resolve_profile_uuid_for_checkout( $profile_id ) : '';
	$pass     = isset( $link['welcome_plain_password'] ) ? (string) $link['welcome_plain_password'] : '';
	$email_lang = function_exists( 'avision_stripe_resolve_checkout_lang' ) ? avision_stripe_resolve_checkout_lang( $cm ) : 'fr';
	$sent     = false;
	if ( function_exists( 'avi_send_welcome_email' ) ) {
		$sent = avi_send_welcome_email( $email, $bn, $pass, $pid_uuid, $profile_id, $email_lang );
	} elseif ( function_exists( 'avision_send_welcome_b2c_email_after_convert' ) ) {
		$sent = avision_send_welcome_b2c_email_after_convert( $email, $bn, $pid_uuid, $pass, $profile_id, $email_lang );
	}
	if ( $sent && function_exists( 'set_transient' ) ) {
		set_transient( $tkey, '1', DAY_IN_SECONDS );
	}
	if ( function_exists( 'error_log' ) ) {
		error_log( '[BREVO] WELCOME post-checkout Stripe — profile_id=' . $profile_id . ' sent=' . ( $sent ? '1' : '0' ) );
	}
}

/**
 * Point d’entrée page : ?session_id=cs_… après Hosted Checkout — profil + user WP + cookie SaaS + redirect propre.
 *
 * @return bool True si une redirection a été envoyée.
 */
function avision_handle_stripe_checkout_landing() {
	if ( empty( $_GET['session_id'] ) || ! is_string( $_GET['session_id'] ) ) {
		return false;
	}
	$session_id = sanitize_text_field( wp_unslash( $_GET['session_id'] ) );
	if ( strpos( $session_id, 'cs_' ) !== 0 ) {
		return false;
	}
	$out = avision_complete_stripe_checkout_return( $session_id );
	if ( empty( $out['success'] ) ) {
		return false;
	}

	global $wpdb;
	$link = avision_stripe_apply_wp_login_and_profile_link( $out );
	$out['profile_id'] = $link['profile_id'];

	if ( $link['profile_id'] <= 0 && ! empty( $out['email'] ) && is_email( (string) $out['email'] ) ) {
		$forced_lr = avision_stripe_last_resort_mysql_profile_for_email( (string) $out['email'], array() );
		if ( $forced_lr > 0 ) {
			$out['profile_id'] = $forced_lr;
			$link              = avision_stripe_apply_wp_login_and_profile_link( $out );
			$out['profile_id'] = $link['profile_id'];
		}
	}

	if ( $link['profile_id'] <= 0 ) {
		if ( function_exists( 'error_log' ) ) {
			$db_msg = ( isset( $wpdb ) && $wpdb && $wpdb->last_error ) ? $wpdb->last_error : ( $link['error'] !== '' ? $link['error'] : 'profil introuvable' );
			error_log( '[AVI STRIPE] Landing : profil toujours absent — ' . $db_msg );
		}
		$pending_base    = ( function_exists( 'avision_stripe_is_agency_checkout_landing' ) && avision_stripe_is_agency_checkout_landing() )
			? trailingslashit( home_url( '/agency-dashboard/' ) )
			: trailingslashit( home_url( '/premium-dashboard/' ) );
		$out['redirect'] = $pending_base . '?status=success&stripe_profile_pending=1';
	} elseif ( $link['wp_user_id'] <= 0 ) {
		if ( function_exists( 'error_log' ) ) {
			error_log( '[AVI STRIPE] Landing : compte WP absent err=' . $link['error'] );
		}
		$out['redirect'] = trailingslashit( home_url( '/connexion/' ) ) . '?stripe_user_pending=1&status=success';
	} else {
		$out['redirect'] = avision_stripe_build_post_checkout_redirect_url( $out, $link['profile_id'] );
	}

	if ( $link['profile_id'] > 0 && $link['wp_user_id'] > 0 && function_exists( 'avision_trigger_stripe_checkout_welcome_email' ) ) {
		avision_trigger_stripe_checkout_welcome_email( $out, $link );
	}
	if ( ! empty( $out['first_login'] ) && isset( $link['wp_user_id'] ) && (int) $link['wp_user_id'] > 0 && function_exists( 'avision_flag_premium_first_login_welcome' ) ) {
		avision_flag_premium_first_login_welcome( (int) $link['wp_user_id'] );
	}

	if ( isset( $wpdb ) && $wpdb && method_exists( $wpdb, 'flush' ) ) {
		$wpdb->flush();
	}
	if ( function_exists( 'usleep' ) ) {
		usleep( 100000 );
	}

	if ( ! empty( $out['sid'] ) && ! headers_sent() ) {
		$secure = function_exists( 'is_ssl' ) && is_ssl();
		setcookie( 'avision_session_token', $out['sid'], time() + 604800, '/', '', $secure, true );
		$_COOKIE['avision_session_token'] = $out['sid'];
	}
	if ( function_exists( 'wp_safe_redirect' ) && ! headers_sent() ) {
		wp_safe_redirect( $out['redirect'] );
		return true;
	}
	return false;
}

/**
 * Pont de succès Stripe → WordPress (auto-login) + liaison profil.
 *
 * - Détecte ?session_id=cs_… (retour Stripe Hosted Checkout)
 * - Vérifie via Stripe API (via avision_complete_stripe_checkout_return)
 * - Connecte (ou crée) l’utilisateur WP par email
 * - Lie wp_user_id au profil SaaS si possible
 * - Nettoie l’URL : redirige vers /premium-dashboard/ ou /agency-dashboard/ selon le tunnel (sans session_id).
 */
function avision_stripe_success_auto_login_bridge() {
	if ( is_admin() ) {
		return;
	}
	if ( empty( $_GET['session_id'] ) || ! is_string( $_GET['session_id'] ) ) {
		return;
	}
	$session_id = sanitize_text_field( wp_unslash( $_GET['session_id'] ) );
	if ( $session_id === '' || strpos( $session_id, 'cs_' ) !== 0 ) {
		return;
	}
	// Anti-double-run (refresh/back) : 10 minutes.
	$lock_key = 'avi_stripe_bridge_' . md5( $session_id );
	if ( function_exists( 'get_transient' ) && get_transient( $lock_key ) ) {
		return;
	}
	if ( function_exists( 'set_transient' ) ) {
		set_transient( $lock_key, 1, 10 * MINUTE_IN_SECONDS );
	}

	$out = avision_complete_stripe_checkout_return( $session_id );
	if ( empty( $out['success'] ) ) {
		return;
	}
	$email = isset( $out['email'] ) ? sanitize_email( (string) $out['email'] ) : '';
	if ( ! is_email( $email ) ) {
		return;
	}

	$link        = avision_stripe_apply_wp_login_and_profile_link( $out );
	$profile_id  = $link['profile_id'];
	$out['profile_id'] = $profile_id;

	if ( $profile_id <= 0 || $link['wp_user_id'] <= 0 ) {
		if ( function_exists( 'error_log' ) ) {
			error_log( '[AVI STRIPE] Bridge incomplet — profile_id=' . $profile_id . ' wp_user_id=' . $link['wp_user_id'] . ' err=' . $link['error'] );
		}
		return;
	}

	if ( function_exists( 'avision_trigger_stripe_checkout_welcome_email' ) ) {
		avision_trigger_stripe_checkout_welcome_email( $out, $link );
	}
	if ( ! empty( $out['first_login'] ) && isset( $link['wp_user_id'] ) && (int) $link['wp_user_id'] > 0 && function_exists( 'avision_flag_premium_first_login_welcome' ) ) {
		avision_flag_premium_first_login_welcome( (int) $link['wp_user_id'] );
	}

	global $wpdb;
	if ( isset( $wpdb ) && $wpdb && method_exists( $wpdb, 'flush' ) ) {
		$wpdb->flush();
	}
	if ( function_exists( 'usleep' ) ) {
		usleep( 100000 );
	}

	// 4) Redirection propre vers cockpit (pid + status), sans session_id — aligné B2B/B2C.
	$target = avision_stripe_build_post_checkout_redirect_url( $out, $profile_id );
	$pid    = '';
	if ( $profile_id > 0 && function_exists( 'avision_resolve_profile_uuid_for_checkout' ) ) {
		$pid = (string) avision_resolve_profile_uuid_for_checkout( $profile_id );
	}
	if ( $pid === '' && ! empty( $out['pid'] ) ) {
		$pid = trim( (string) $out['pid'] );
	}
	if ( $pid === '' ) {
		if ( function_exists( 'error_log' ) ) {
			error_log( '[AVI STRIPE] Bridge : pid UUID vide — pas de redirection.' );
		}
		return;
	}
	if ( function_exists( 'wp_safe_redirect' ) && function_exists( 'home_url' ) && ! headers_sent() ) {
		wp_safe_redirect( $target, 302 );
		exit;
	}
}
add_action( 'init', 'avision_stripe_success_auto_login_bridge', 5 );

/**
 * Vérifie la session Stripe (retour après paiement) et retourne l'URL de redirection + sid pour connexion.
 * Utilisé par la page connexion (login-logic.js) si success_url pointe encore vers /connexion/.
 */
function avision_rest_connexion_verify( \WP_REST_Request $request ) {
	$session_id = $request->get_param( 'session_id' );
	if ( empty( $session_id ) || strpos( (string) $session_id, 'cs_' ) !== 0 ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'invalid_session_id' ], 400 );
	}
	$out = avision_complete_stripe_checkout_return( (string) $session_id );
	if ( empty( $out['success'] ) ) {
		$err = isset( $out['error'] ) ? (string) $out['error'] : 'stripe_error';
		$map = array(
			'stripe_not_configured'   => 503,
			'invalid_session_id'      => 400,
			'payment_not_completed'   => 400,
			'no_email'                => 400,
			'stripe_session_invalid'  => 502,
		);
		$status = isset( $map[ $err ] ) ? $map[ $err ] : 400;
		return new \WP_REST_Response( [ 'success' => false, 'error' => $err ], $status );
	}
	return new \WP_REST_Response( [
		'success'   => true,
		'redirect'  => $out['redirect'],
		'user_type' => isset( $out['user_type'] ) ? $out['user_type'] : 'b2c',
		'sid'       => isset( $out['sid'] ) ? $out['sid'] : null,
		'pid'       => isset( $out['pid'] ) ? $out['pid'] : '',
	], 200 );
}

/**
 * Retourne l'ID profil (avi_saas_profiles.id) à partir de l'UUID (paramètre pid en URL).
 *
 * @param string $uuid UUID du profil (36 caractères).
 * @return int 0 si non trouvé.
 */
function avision_get_profile_id_by_uuid( $uuid ) {
	$uuid = is_string( $uuid ) ? trim( $uuid ) : '';
	if ( $uuid === '' || ! preg_match( '/^[a-f0-9\-]{32,36}$/i', $uuid ) ) {
		return 0;
	}
	global $wpdb;
	$table = $wpdb->prefix . 'avi_saas_profiles';
	$id    = (int) $wpdb->get_var( $wpdb->prepare(
		"SELECT id FROM `{$table}` WHERE uuid = %s LIMIT 1",
		$uuid
	) );
	return $id;
}

/**
 * REST : statut CRON pour une fiche (dernier scan, stats Gardien, derniers logs, derniers posts).
 * GET /wp-json/avision/v1/cron-status?profile_id=123 ou ?pid=uuid
 */
function avision_rest_cron_status( \WP_REST_Request $request ) {
	$profile_id = (int) $request->get_param( 'profile_id' );
	if ( $profile_id <= 0 ) {
		$pid = $request->get_param( 'pid' );
		if ( $pid && function_exists( 'avision_get_profile_id_by_uuid' ) ) {
			$profile_id = avision_get_profile_id_by_uuid( $pid );
		}
	}
	if ( $profile_id <= 0 ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'profile_id ou pid invalide' ], 400 );
	}
	global $wpdb;
	$t_profiles = $wpdb->prefix . 'avi_saas_profiles';
	$t_posts    = $wpdb->prefix . 'avi_saas_posts';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t_profiles ) ) !== $t_profiles ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'Table profils manquante. Installez les tables cœeur (AVI → Tables cœur).' ], 503 );
	}
	$schema = defined( 'DB_NAME' ) ? DB_NAME : '';
	if ( $schema !== '' ) {
		$col = $wpdb->get_results( $wpdb->prepare(
			"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'guardian_active'",
			$schema,
			$t_profiles
		) );
		if ( empty( $col ) ) {
			$wpdb->query( "ALTER TABLE `{$t_profiles}` ADD COLUMN guardian_active SMALLINT DEFAULT 0" );
		}
	}
	$profile = $wpdb->get_row( $wpdb->prepare(
		"SELECT id, user_id, business_name, last_radar_scan, radar_plan, COALESCE(guardian_active, 0) AS guardian_active FROM `{$t_profiles}` WHERE id = %d LIMIT 1",
		$profile_id
	), ARRAY_A );
	if ( ! $profile ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'Profil introuvable (id=' . $profile_id . ').' ], 404 );
	}
	if ( ! current_user_can( 'manage_options' ) && function_exists( 'avi_current_user_can_access_profile' ) && ! avi_current_user_can_access_profile( $profile_id ) ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'accès non autorisé à cette fiche' ], 403 );
	}
	$user_id = (int) $profile['user_id'];
	$guardian_active = ( (int) ( $profile['guardian_active'] ?? 0 ) ) === 1;
	$guardian_stats = [];
	$logs = [];
	if ( class_exists( 'Avision\Avision_Guardian_Watchers' ) ) {
		$guardian_stats = \Avision\Avision_Guardian_Watchers::get_guardian_stats( $user_id );
		$logs           = \Avision\Avision_Guardian_Watchers::get_guardian_logs( $user_id, 20 );
	}
	$posts = [];
	if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $t_posts ) ) === $t_posts ) {
		$posts = $wpdb->get_results( $wpdb->prepare(
			"SELECT id, title, status, created_at FROM `{$t_posts}` WHERE profile_id = %d ORDER BY created_at DESC LIMIT 10",
			$profile_id
		), ARRAY_A );
		if ( ! is_array( $posts ) ) {
			$posts = [];
		}
	}
	return new \WP_REST_Response( [
		'success'          => true,
		'profile_id'       => $profile_id,
		'user_id'          => $user_id,
		'business_name'    => isset( $profile['business_name'] ) ? $profile['business_name'] : '',
		'last_radar_scan'  => isset( $profile['last_radar_scan'] ) ? $profile['last_radar_scan'] : null,
		'radar_plan'       => isset( $profile['radar_plan'] ) ? $profile['radar_plan'] : 'offert',
		'guardian_active'  => $guardian_active,
		'guardian_stats'   => $guardian_stats,
		'logs'             => $logs,
		'posts'            => $posts,
	], 200 );
}

/**
 * REST : lancer l'orchestrateur CRON pour une fiche (admin uniquement).
 * POST /wp-json/avision/v1/cron-run-profile avec body { "profile_id": 123 }.
 * Vérification manage_options AVANT tout chargement ; erreur exacte dans wp-content/debug.log.
 */
function avision_rest_cron_run_profile( \WP_REST_Request $request ) {
	// Dépendances média WP — chemins absolus (ABSPATH)
	if ( defined( 'ABSPATH' ) ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';
		require_once ABSPATH . 'wp-admin/includes/media.php';
	}
	if ( ! function_exists( 'current_user_can' ) || ! current_user_can( 'manage_options' ) ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'Non autorisé (manage_options requis)' ], 403 );
	}

	try {
		$json_params = method_exists( $request, 'get_json_params' ) ? $request->get_json_params() : null;
		$data        = is_array( $json_params ) ? $json_params : [];
		$profile_id  = isset( $data['profile_id'] ) ? (int) $data['profile_id'] : (int) $request->get_param( 'profile_id' );
		if ( $profile_id <= 0 && ! empty( $data['pid'] ) && is_string( $data['pid'] ) && function_exists( 'avision_get_profile_id_by_uuid' ) ) {
			$profile_id = (int) avision_get_profile_id_by_uuid( trim( $data['pid'] ) );
		}
		if ( $profile_id <= 0 ) {
			return new \WP_REST_Response( [ 'success' => false, 'error' => 'profile_id ou pid invalide/manquant (body JSON: profile_id ou pid UUID)' ], 400 );
		}
		$includes_dir = defined( 'AVISION_INCLUDES_DIR' ) ? AVISION_INCLUDES_DIR : ( defined( 'AVISION_PATH' ) ? AVISION_PATH . 'includes/' : '' );
		if ( $includes_dir === '' ) {
			return new \WP_REST_Response( [ 'success' => false, 'error' => 'AVISION_INCLUDES_DIR/AVISION_PATH non définis' ], 500 );
		}
		$cron_file = $includes_dir . 'cron-task-manager.php';
		if ( ! file_exists( $cron_file ) ) {
			return new \WP_REST_Response( [ 'success' => false, 'error' => 'Fichier cron-task-manager.php introuvable.', 'debug_hint' => 'Chemin testé: ' . $cron_file ], 500 );
		}
		require_once $cron_file;
		$scan_engine = $includes_dir . 'class-avision-scan-engine.php';
		if ( file_exists( $scan_engine ) ) {
			require_once $scan_engine;
		}
		$manager = new \Avision\Cron_Task_Manager();
		if ( ! method_exists( $manager, 'run_for_profile' ) ) {
			return new \WP_REST_Response( [ 'success' => false, 'error' => 'run_for_profile non disponible' ], 503 );
		}
		$result = $manager->run_for_profile( $profile_id );
		return new \WP_REST_Response( [
			'success' => true,
			'message' => 'CRON exécuté pour cette fiche.',
			'result'  => $result,
		], 200 );
	} catch ( \Throwable $e ) {
		$msg  = $e->getMessage();
		$file = $e->getFile();
		$line = $e->getLine();
		error_log( 'AVI ERROR: ' . $msg . ' in ' . $file . ':' . $line );
		$log_line  = sprintf( "[%s] AVI cron-run-profile ERROR: %s in %s:%d\n", gmdate( 'Y-m-d H:i:s' ), $msg, $file, $line );
		$debug_log = ( defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ( defined( 'ABSPATH' ) ? rtrim( ABSPATH, '/\\' ) . 'wp-content' : '' ) ) . '/debug.log';
		if ( $debug_log !== '/debug.log' && ( is_writable( $debug_log ) || ( ! file_exists( $debug_log ) && is_writable( dirname( $debug_log ) ) ) ) ) {
			@file_put_contents( $debug_log, $log_line, FILE_APPEND | LOCK_EX );
		}
		return new \WP_REST_Response( [
			'success'    => false,
			'error'     => 'Erreur lors de l\'exécution du CRON.',
			'message'    => ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? $msg : null,
			'debug_hint' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? ( $msg . ' in ' . basename( $file ) . ':' . $line ) : 'Consultez wp-content/debug.log ou logs/cron-task.log. Activez WP_DEBUG pour le détail ici.',
		], 500 );
	}
}

/**
 * REST : lance le scan de démonstration (même flux que admin.php?page=avi-install-core-tables&avi_demo_scan=1).
 * Injecte des logs dans avi_security_logs pour l'utilisateur courant et, si profile_id est fourni, un post de test (image + texte) dans "Intervention en cours".
 * Admin uniquement.
 *
 * @param \WP_REST_Request $request
 * @return \WP_REST_Response
 */
function avision_rest_demo_scan( \WP_REST_Request $request ) {
	if ( ! current_user_can( 'manage_options' ) ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'Non autorisé (manage_options requis)' ], 403 );
	}
	if ( ! class_exists( 'Avision\AVI_Tester' ) || ! \Avision\AVI_Tester::can_run_demo_scan() ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'Scan de démo non disponible' ], 503 );
	}
	$user_id = get_current_user_id();
	$json    = method_exists( $request, 'get_json_params' ) ? $request->get_json_params() : null;
	$data    = is_array( $json ) ? $json : [];
	$profile_id = isset( $data['profile_id'] ) ? (int) $data['profile_id'] : (int) $request->get_param( 'profile_id' );

	try {
		$tester = new \Avision\AVI_Tester();
		$res    = $tester->simulate_full_protection( $user_id, $profile_id );
		return new \WP_REST_Response( [
			'success'    => true,
			'message'    => isset( $res['message'] ) ? $res['message'] : 'Scan de démo exécuté.',
			'user_id'    => $user_id,
			'demo_post'  => ! empty( $res['demo_post'] ),
		], 200 );
	} catch ( \Throwable $e ) {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'error_log' ) ) {
			error_log( '[AVI demo-scan] ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
		}
		return new \WP_REST_Response( [
			'success' => false,
			'error'   => 'Erreur lors du scan de démo.',
			'message' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? $e->getMessage() : null,
		], 200 );
	}
}

/**
 * REST : lance le First Sync (Google + scan 3 mots-clés Radar + calcul score + rank_history).
 * POST /wp-json/avision/v1/first-sync avec body { "profile_id": 123 }.
 * Répond immédiatement avec {"status": "sync_started"} pour éviter le 502 (timeout).
 * La sync réelle s'exécute après envoi de la réponse (shutdown), en arrière-plan.
 *
 * @param \WP_REST_Request $request
 * @return \WP_REST_Response
 */
function avision_rest_first_sync( \WP_REST_Request $request ) {
    if ( ! session_id() ) {
        @session_start();
    }

    $profile_id = (int) $request->get_param( 'profile_id' );
    if ( $profile_id <= 0 ) {
        return new \WP_REST_Response( [ 'success' => false, 'error' => 'profile_id invalide' ], 400 );
    }

    ignore_user_abort( true );
    error_log( '[Avision First Sync] REST: returning sync_started immediately for profile_id=' . $profile_id );

    global $wpdb;
    $score_from_db = 0;
    if ( isset( $wpdb ) && $wpdb ) {
        $profiles_table = $wpdb->prefix . 'avi_saas_profiles';
        $raw_score      = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COALESCE(score, 0) FROM `{$profiles_table}` WHERE id = %d LIMIT 1",
                $profile_id
            )
        );
        if ( $raw_score !== null && $raw_score !== '' ) {
            $score_from_db = max( 0, min( 100, (int) $raw_score ) );
        }
    }

    return new \WP_REST_Response( [
        'status'        => 'sync_started',
        'success'       => true,
        'initial_score' => $score_from_db,
        'score'         => $score_from_db,
    ], 200 );
}

/**
 * Réponse REST radar-keywords vide (table absente, erreur SQL douce, ou aucune ligne).
 *
 * @param int         $profile_id
 * @param string|null $error      Message optionnel si WP_DEBUG.
 * @return \WP_REST_Response
 */
function avision_radar_keywords_empty_response( $profile_id, $error = null ) {
	$body = [
		'keywords' => [],
		'total'    => 0,
	];
	if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $error !== null && $error !== '' ) {
		$body['error']            = $error;
		$body['debug_profile_id'] = (int) $profile_id;
	} else {
		$body['debug_profile_id'] = (int) $profile_id;
	}
	return new \WP_REST_Response( $body, 200 );
}

/**
 * REST : mots-clés du Radar (même format que le backend Python).
 * GET /wp-json/avision/v1/radar-keywords?profile_id=X&limit=75
 * Utilise $wpdb pour lire/écrire avi_saas_keywords (BDD du site). Si vide et profil valide (category+city), crée les 3 mots-clés initiaux puis re-lit.
 * Zéro fallback statique : résultat vide = JSON { keywords: [], total: 0 } via WP_REST_Response (pas de die / exit).
 *
 * @param \WP_REST_Request $request
 * @return \WP_REST_Response
 */
function avision_rest_radar_keywords( \WP_REST_Request $request ) {
	global $wpdb;
	$profile_id = (int) $request->get_param( 'profile_id' );
	$limit_raw  = $request->get_param( 'limit' );
	// limit=0 ou absent : tous les mots-clés du profil (pas de plafond artificiel — distribution Radar fidèle à la BDD).
	$unlimited = ( $limit_raw === null || $limit_raw === '' || (int) $limit_raw === 0 );
	$limit     = $unlimited ? 0 : max( 1, min( 10000, (int) $limit_raw ) );
	$force     = (bool) $request->get_param( 'force' );
	if ( $profile_id <= 0 || ! isset( $wpdb ) || ! $wpdb ) {
		return avision_radar_keywords_empty_response( $profile_id > 0 ? $profile_id : 0, null );
	}
	$pre     = ! empty( $wpdb->prefix ) ? $wpdb->prefix : '';
	$tbl_kw  = $pre . 'avi_saas_keywords';
	$tbl_pro = $pre . 'avi_saas_profiles';

	// Table absente ou inaccessible : JSON vide (évite HTML d’erreur SQL).
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl_kw ) ) !== $tbl_kw ) {
		return avision_radar_keywords_empty_response( $profile_id, 'Table mots-clés indisponible' );
	}

	$total_in_db = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$tbl_kw}` WHERE profile_id = %d", $profile_id ) );

	$get_rows = function() use ( $wpdb, $profile_id, $limit, $tbl_kw, $unlimited ) {
		if ( $unlimited ) {
			$sql = $wpdb->prepare(
				"SELECT id, keyword, COALESCE(rank_init, previous_rank, 0) AS rank_init, current_rank, scan_status FROM `" . $tbl_kw . "` WHERE profile_id = %d ORDER BY is_strategic DESC, id ASC",
				$profile_id
			);
		} else {
			$sql = $wpdb->prepare(
				"SELECT id, keyword, COALESCE(rank_init, previous_rank, 0) AS rank_init, current_rank, scan_status FROM `" . $tbl_kw . "` WHERE profile_id = %d ORDER BY is_strategic DESC, id ASC LIMIT %d",
				$profile_id,
				$limit
			);
		}
		$rows = $wpdb->get_results( $sql, ARRAY_A );
		if ( ! is_array( $rows ) || ( ! empty( $wpdb->last_error ) && strpos( $wpdb->last_error, 'rank_init' ) !== false ) ) {
			if ( $unlimited ) {
				$sql = $wpdb->prepare(
					"SELECT id, keyword, COALESCE(previous_rank, 0) AS rank_init, current_rank, scan_status FROM `" . $tbl_kw . "` WHERE profile_id = %d ORDER BY is_strategic DESC, id ASC",
					$profile_id
				);
			} else {
				$sql = $wpdb->prepare(
					"SELECT id, keyword, COALESCE(previous_rank, 0) AS rank_init, current_rank, scan_status FROM `" . $tbl_kw . "` WHERE profile_id = %d ORDER BY is_strategic DESC, id ASC LIMIT %d",
					$profile_id,
					$limit
				);
			}
			$rows = $wpdb->get_results( $sql, ARRAY_A );
		}
		return is_array( $rows ) ? $rows : [];
	};

	$rows = $get_rows();
	if ( ! empty( $wpdb->last_error ) ) {
		return avision_radar_keywords_empty_response( $profile_id, 'Erreur lecture mots-clés' );
	}
	if ( empty( $rows ) || $force ) {
		$profile = $wpdb->get_row( $wpdb->prepare( "SELECT category, city, neighborhood FROM `" . $tbl_pro . "` WHERE id = %d LIMIT 1", $profile_id ), ARRAY_A );
		if ( ! empty( $wpdb->last_error ) ) {
			return avision_radar_keywords_empty_response( $profile_id, 'Erreur lecture profil' );
		}
		$cat   = $profile ? trim( (string) ( isset( $profile['category'] ) ? $profile['category'] : '' ) ) : '';
		$city  = $profile ? trim( (string) ( isset( $profile['city'] ) ? $profile['city'] : '' ) ) : '';
		$neigh = $profile ? trim( (string) ( isset( $profile['neighborhood'] ) ? $profile['neighborhood'] : '' ) ) : '';
		if ( $cat === '-' ) {
			$cat = '';
		}
		if ( $city === '-' ) {
			$city = '';
		}
		if ( $neigh === '-' ) {
			$neigh = '';
		}
		$can_generate = $profile && $cat !== '' && $city !== '';
		if ( $can_generate && function_exists( 'avi_generate_initial_keywords' ) ) {
			avi_generate_initial_keywords( $profile_id );
			$rows = $get_rows();
			if ( ! empty( $wpdb->last_error ) ) {
				return avision_radar_keywords_empty_response( $profile_id, 'Erreur après génération initiale' );
			}
		}
	}

	$result = [];
	foreach ( $rows as $r ) {
		$rank_init = isset( $r['rank_init'] ) ? (int) $r['rank_init'] : 0;
		$rank_curr = isset( $r['current_rank'] ) ? $r['current_rank'] : null;
		if ( $rank_curr !== null && in_array( (int) $rank_curr, [ 1, 2, 3 ], true ) ) {
			$rank_curr = 'TOP 3';
		}
		$keyword_str = isset( $r['keyword'] ) ? trim( (string) $r['keyword'] ) : '';
		$result[] = [
			'keyword'   => $keyword_str,
			'rankInit'  => $rank_init,
			'rank_init' => $rank_init,
			'rankCurr'  => $rank_curr,
			'volume'    => 0,
		];
	}

	return new \WP_REST_Response( [
		'keywords'         => $result,
		'total'            => $total_in_db,
		'total_fetched'    => count( $result ),
		'debug_profile_id' => $profile_id,
	], 200 );
}

/**
 * REST : sauvegarde stratégie Radar (sync liste complète → avi_saas_keywords).
 * POST /wp-json/avision/v1/radar-keywords-save
 * Body: { profile_id: number, keywords: string[] }
 * UPSERT : conserve scan_status / last_scan_at des lignes existantes ; supprime les retirés ; insère les nouveaux en pending.
 *
 * @param \WP_REST_Request $request
 * @return \WP_REST_Response
 */
function avision_rest_radar_keywords_save( \WP_REST_Request $request ) {
	$profile_id = (int) $request->get_param( 'profile_id' );
	$keywords   = $request->get_param( 'keywords' );

	if ( $profile_id <= 0 || ! is_array( $keywords ) ) {
		return new \WP_REST_Response(
			[
				'success' => false,
				'error'   => 'invalid_params',
			],
			400
		);
	}

	$user_id = get_current_user_id();
	if ( ! $user_id ) {
		return new \WP_REST_Response(
			[
				'success' => false,
				'error'   => 'not_authenticated',
			],
			401
		);
	}

	global $wpdb;
	if ( ! isset( $wpdb ) || ! $wpdb ) {
		return new \WP_REST_Response(
			[
				'success' => false,
				'error'   => 'db_unavailable',
			],
			503
		);
	}

	$pre = ! empty( $wpdb->prefix ) ? $wpdb->prefix : '';
	$tbl = $pre . 'avi_saas_keywords';

	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) ) !== $tbl ) {
		return new \WP_REST_Response(
			[
				'success' => false,
				'error'   => 'table_unavailable',
			],
			503
		);
	}

	$incoming = array_values(
		array_filter(
			array_map(
				static function ( $k ) {
					return sanitize_text_field( (string) $k );
				},
				$keywords
			),
			static function ( $k ) {
				return $k !== '';
			}
		)
	);
	$incoming_lower = array_map( 'strtolower', $incoming );

	$existing = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT id, keyword, scan_status, is_strategic FROM `{$tbl}` WHERE profile_id = %d",
			$profile_id
		),
		ARRAY_A
	);
	$existing      = is_array( $existing ) ? $existing : [];
	$existing_map  = [];
	foreach ( $existing as $row ) {
		$key = strtolower( trim( (string) ( $row['keyword'] ?? '' ) ) );
		if ( $key !== '' ) {
			$existing_map[ $key ] = $row;
		}
	}

	foreach ( $existing_map as $key => $row ) {
		if ( ! in_array( $key, $incoming_lower, true ) ) {
			$wpdb->delete( $tbl, [ 'id' => (int) $row['id'] ], [ '%d' ] );
		}
	}

	foreach ( $incoming as $kw ) {
		$key = strtolower( $kw );
		if ( ! isset( $existing_map[ $key ] ) ) {
			$wpdb->insert(
				$tbl,
				[
					'profile_id'   => $profile_id,
					'keyword'      => $kw,
					'is_strategic' => 1,
					'scan_status'  => 'pending',
					'last_scan_at' => null,
				],
				[ '%d', '%s', '%d', '%s', '%s' ]
			);
		} elseif ( empty( $existing_map[ $key ]['is_strategic'] ) ) {
			$wpdb->update(
				$tbl,
				[ 'is_strategic' => 1 ],
				[ 'id' => (int) $existing_map[ $key ]['id'] ],
				[ '%d' ],
				[ '%d' ]
			);
		}
	}

	$count = (int) $wpdb->get_var(
		$wpdb->prepare(
			"SELECT COUNT(*) FROM `{$tbl}` WHERE profile_id = %d",
			$profile_id
		)
	);

	return new \WP_REST_Response(
		[
			'success' => true,
			'saved'   => count( $incoming ),
			'total'   => $count,
		],
		200
	);
}

/**
 * REST : ajout en lot des mots-clés choisis par l'utilisateur (Queue Manager).
 * POST /wp-json/avision/v1/add-keywords-batch
 * Body: { profile_id: number, keywords: string[] }
 * Respecte la limite du plan (offert 20, hebdo 75, elite 100) et marque les nouveaux en pending_scan.
 *
 * @param \WP_REST_Request $request
 * @return \WP_REST_Response
 */
function avision_rest_add_keywords_batch( \WP_REST_Request $request ) {
	$profile_id = (int) $request->get_param( 'profile_id' );
	$keywords   = $request->get_param( 'keywords' );
	if ( ! is_array( $keywords ) ) {
		$keywords = [];
	}
	$keywords = array_map( 'sanitize_text_field', $keywords );

	if ( $profile_id <= 0 ) {
		return new \WP_REST_Response( [ 'error' => 'profile_id requis', 'inserted' => 0, 'limit' => 20, 'current_total' => 0 ], 400 );
	}

	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return new \WP_REST_Response( [ 'error' => 'Database unavailable', 'inserted' => 0, 'limit' => 20, 'current_total' => 0 ], 503 );
	}

	$db     = new \Avision\Avision_Database();
	$db->init();
	$result = $db->add_suggested_keywords_to_profile( $profile_id, $keywords );
	if ( empty( $result ) ) {
		$result = [ 'inserted' => 0, 'limit' => 20, 'current_total' => 0 ];
	}
	return new \WP_REST_Response( array_merge( [ 'success' => (int) ( $result['inserted'] ?? 0 ) > 0 ], $result ), 200 );
}

/**
 * REST : suppression d'un mot-clé du Radar.
 * POST /wp-json/avision/v1/delete-keyword
 * Body: { profile_id: number, keyword: string }
 */
function avision_rest_delete_keyword( \WP_REST_Request $request ) {
	$profile_id = (int) $request->get_param( 'profile_id' );
	$keyword    = trim( (string) $request->get_param( 'keyword' ) );

	if ( $profile_id <= 0 || $keyword === '' ) {
		return new \WP_REST_Response( [ 'success' => false, 'error' => 'profile_id et keyword requis' ], 400 );
	}

	global $wpdb;
	$pre   = ! empty( $wpdb->prefix ) ? $wpdb->prefix : '';
	$table = $pre . 'avi_saas_keywords';
	$deleted = $wpdb->delete( $table, [
		'profile_id' => $profile_id,
		'keyword'    => $keyword,
	], [ '%d', '%s' ] );

	if ( function_exists( 'error_log' ) ) {
		error_log( '[AVI] delete-keyword: profile=' . $profile_id . ' keyword="' . $keyword . '" deleted=' . ( $deleted ? $deleted : '0' ) );
	}

	return new \WP_REST_Response( [ 'success' => $deleted > 0, 'deleted' => (int) $deleted ], 200 );
}

/**
 * Génère et insère les 3 mots-clés initiaux du Radar (First Sync) pour un profil fraîchement créé.
 * Kw1 : category + city ; Kw2 : category + neighborhood (si présent) ; Kw3 : category + proximité locale (near me / à proximité / cerca de mi).
 * À appeler juste après l'insertion du profil (onboarding).
 *
 * @param int $profile_id ID du profil (avi_saas_profiles.id)
 * @return bool True si au moins un mot-clé a été inséré
 */
function avi_generate_initial_keywords($profile_id) {
    $profile_id = (int) $profile_id;
    if ($profile_id <= 0) return false;
    if (!class_exists('Avision\Avision_Database')) return false;
    $db = new \Avision\Avision_Database();
    $db->init();
    if (!method_exists($db, 'insert_initial_keywords_for_profile')) return false;
    return $db->insert_initial_keywords_for_profile($profile_id);
}

/**
 * Récupère les positions des 15 mots-clés pour plusieurs audits (une seule requête SQL).
 * Pour le dashboard agence (Ranking Heatmap).
 *
 * @param int[] $audit_ids
 * @return array [ audit_id => ['has_top3' => bool, 'keywords' => [ 1..15 => ['keyword'=>'...', 'position'=>int|null] ] ] ]
 */
function avision_get_keyword_ranks_for_audits(array $audit_ids) {
	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return array();
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	if ( ! method_exists( $db, 'get_keyword_ranks_for_audits' ) ) {
		return array();
	}
	return $db->get_keyword_ranks_for_audits( $audit_ids );
}

/**
 * Crée ou récupère le profil "ADMIN TEST STORE" et une session réelle pour un administrator.
 * Permet de sauvegarder la progression des niveaux. Force current_level à 1 pour commencer le parcours.
 *
 * @return array|null ['user_id' => int, 'profile_id' => int, 'profile' => array, 'is_premium' => true] ou null si erreur
 */
function avision_get_or_create_admin_test_session() {
	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return null;
	}
	$wp_user_id = function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0;
	$admin_email = 'admin-test-store@avision.local';
	if ( $wp_user_id && function_exists( 'get_userdata' ) ) {
		$user_data = get_userdata( $wp_user_id );
		if ( $user_data && ! empty( $user_data->user_email ) ) {
			$admin_email = trim( $user_data->user_email );
		}
	}
	if ( $admin_email === '' ) {
		$admin_email = 'admin-test-store@avision.local';
	}

	$db = new \Avision\Avision_Database();
	$db->init();
	if ( ! $db->get_connection() ) {
		return null;
	}

	$user_id = $db->get_or_create_saas_user( $admin_email );
	if ( ! $user_id ) {
		return null;
	}

	$profile = $db->get_saas_profile_by_user_and_business_name( $user_id, 'ADMIN TEST STORE' );
	if ( ! $profile ) {
		$created = $db->create_saas_profile( $user_id, 'ADMIN TEST STORE', '', '', null, 0, '' );
		if ( ! $created ) {
			return null;
		}
		if ( function_exists( 'avi_generate_initial_keywords' ) ) {
			avi_generate_initial_keywords( $created['id'] );
		}
		$profile = $db->get_saas_profile_by_id( $created['id'] );
		if ( ! $profile ) {
			return null;
		}
	} else {
		$db->update_saas_profile_current_level( $profile['id'], 1 );
		$profile = $db->get_saas_profile_by_id( $profile['id'] );
		if ( $profile ) {
			$profile['current_level'] = 1;
		}
	}

	$session_data = $db->create_saas_session( $user_id, $profile['id'], 604800 );
	if ( ! $session_data || empty( $session_data['token'] ) ) {
		return null;
	}

	if ( function_exists( 'setcookie' ) ) {
		@setcookie( 'avision_session_token', $session_data['token'], time() + 604800, '/', '', function_exists( 'is_ssl' ) && is_ssl(), true );
	}

	return array(
		'user_id'    => (int) $user_id,
		'profile_id' => (int) $profile['id'],
		'profile'    => $profile,
		'is_premium' => true,
	);
}

/**
 * Retourne une session virtuelle pour administrateur (profile_id = 0) quand la BDD est indisponible.
 * Évite toute redirection vers la home : le dashboard s'affiche avec des données de test.
 *
 * @return array
 */
function avision_admin_virtual_session() {
	$wp_user_id = function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0;
	return array(
		'user_id'    => $wp_user_id,
		'profile_id' => 0,
		'profile'    => array(
			'id'            => 0,
			'business_name' => 'ADMIN TEST STORE',
			'city'          => '',
			'category'      => '',
			'current_level' => 1,
			'score'         => 0,
		),
		'is_premium' => true,
	);
}

/**
 * Valide la session dashboard (cookie avision_session_token ou paramètre sid).
 * À appeler au début des pages premium-dashboard et demo-dashboard.
 * Règle admin prioritaire : si current_user_can('administrator'), on ne renvoie jamais null.
 * On tente d'abord de créer/récupérer le profil "ADMIN TEST STORE" en BDD ; si échec, on renvoie une session virtuelle (profile_id = 0).
 *
 * @return array|null ['user_id' => int, 'profile_id' => int, 'profile' => array, 'is_premium' => bool] ou null si non authentifié
 */
function avision_validate_session() {
	$is_administrator = function_exists( 'current_user_can' ) && current_user_can( 'administrator' );

	if ( $is_administrator ) {
		$admin_session = avision_get_or_create_admin_test_session();
		if ( $admin_session !== null ) {
			return $admin_session;
		}
		return avision_admin_virtual_session();
	}

	$token = '';
	if ( ! empty( $_COOKIE['avision_session_token'] ) ) {
		$token = sanitize_text_field( wp_unslash( $_COOKIE['avision_session_token'] ) );
	} elseif ( ! empty( $_GET['sid'] ) ) {
		$token = sanitize_text_field( wp_unslash( $_GET['sid'] ) );
	}
	if ( $token === '' && ! empty( $_GET['pre'] ) && function_exists( 'avision_get_pre_inscription_by_token' ) ) {
		$pre_tok = sanitize_text_field( wp_unslash( $_GET['pre'] ) );
		$pre_row = avision_get_pre_inscription_by_token( $pre_tok );
		if ( is_array( $pre_row ) && isset( $pre_row['type'] ) && $pre_row['type'] === 'b2c' ) {
			return array(
				'user_id'              => 0,
				'profile_id'           => 0,
				'profile'              => array(
					'id'            => 0,
					'business_name' => isset( $pre_row['business_name'] ) ? $pre_row['business_name'] : '',
					'city'          => isset( $pre_row['city'] ) ? $pre_row['city'] : '',
					'neighborhood'  => isset( $pre_row['neighborhood'] ) ? $pre_row['neighborhood'] : '',
					'category'      => isset( $pre_row['category'] ) ? $pre_row['category'] : '',
					'country'       => isset( $pre_row['country'] ) ? $pre_row['country'] : 'FR',
					'current_level' => 1,
					'score'         => isset( $pre_row['score'] ) ? (int) $pre_row['score'] : 0,
				),
				'is_premium'           => false,
				'pre_inscription_only' => true,
				'pre_token'            => $pre_tok,
			);
		}
	}

	if ( $token === '' ) {
		return null;
	}

	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return null;
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	$session = $db->get_saas_session_by_token( $token );
	if ( ! $session ) {
		return null;
	}
	if ( ! empty( $_GET['sid'] ) && function_exists( 'setcookie' ) ) {
		@setcookie( 'avision_session_token', $token, time() + 604800, '/', '', is_ssl(), true );
	}
	$profile = $db->get_saas_profile_by_id( $session['profile_id'] );
	if ( ! $profile ) {
		return null;
	}
	$result = array(
		'user_id'    => $session['user_id'],
		'profile_id' => $session['profile_id'],
		'profile'    => $profile,
	);
	$result['is_premium'] = ! empty( $profile['is_premium'] );
	return $result;
}

/**
 * Enregistre le script bridge des 21 niveaux (pour le thème premium-dashboard).
 * La config est exposée via avision_enqueue_levels_config() et le filtre avision_levels_config.
 */
function avision_register_levels_config_script() {
    if (!function_exists('wp_register_script')) {
        return;
    }
    $bridge_path = plugin_dir_path(__FILE__) . 'assets/js/levels-config-bridge.js';
    $bridge_ver  = file_exists($bridge_path) ? (string) filemtime($bridge_path) : (defined('AVISION_VERSION') ? AVISION_VERSION : '9.0.3');
    wp_register_script(
        'avision-levels-config',
        plugin_dir_url(__FILE__) . 'assets/js/levels-config-bridge.js',
        array(),
        $bridge_ver,
        true
    );
}

/**
 * Enqueue + localize la config des 21 niveaux pour le front (premium-dashboard).
 * À appeler depuis le thème avant d’enqueuer premium-dashboard-js.
 */
function avision_enqueue_levels_config() {
    if (!class_exists('Avision\Avision_Levels_Config') || !function_exists('wp_enqueue_script')) {
        return;
    }
    $config = apply_filters('avision_levels_config', \Avision\Avision_Levels_Config::get_js_config());
    wp_enqueue_script('avision-levels-config');
    wp_localize_script('avision-levels-config', 'avisionLevelsConfig', $config);
}

/**
 * Charge une ligne profil par UUID (wor1515 + table préfixée).
 *
 * @param string $uuid UUID normalisé (minuscules).
 * @return array<string,mixed>|null ARRAY_A ou null.
 */
function avision_premium_dashboard_get_saas_profile_row_by_uuid( $uuid ) {
	$uuid = is_string( $uuid ) ? strtolower( trim( $uuid ) ) : '';
	if ( $uuid === '' || ! preg_match( '/^[a-f0-9\-]{32,36}$/i', $uuid ) ) {
		return null;
	}
	global $wpdb;
	if ( ! isset( $wpdb ) || ! $wpdb ) {
		return null;
	}
	$tables = array();
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', 'wor1515_avi_saas_profiles' ) ) === 'wor1515_avi_saas_profiles' ) {
		$tables[] = 'wor1515_avi_saas_profiles';
	}
	$pt = $wpdb->prefix . 'avi_saas_profiles';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pt ) ) === $pt ) {
		$tables[] = $pt;
	}
	$tables = array_values( array_unique( $tables ) );
	foreach ( $tables as $tbl ) {
		if ( $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$tbl}` LIKE %s", 'uuid' ) ) !== 'uuid' ) {
			continue;
		}
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$tbl}` WHERE uuid = %s LIMIT 1", $uuid ), ARRAY_A );
		if ( is_array( $row ) && ! empty( $row['id'] ) ) {
			return $row;
		}
	}
	return null;
}

/**
 * Extrait google_email / email d’une ligne profil pour le JS (pill GBP).
 *
 * @param array<string,mixed> $row
 * @return string
 */
function avision_premium_dashboard_email_from_profile_row( array $row ) {
	if ( ! function_exists( 'is_email' ) ) {
		return '';
	}
	$g = isset( $row['google_email'] ) ? trim( (string) $row['google_email'] ) : '';
	if ( $g !== '' && is_email( $g ) ) {
		return sanitize_email( $g );
	}
	$e = isset( $row['email'] ) ? trim( (string) $row['email'] ) : '';
	if ( $e !== '' && is_email( $e ) ) {
		return sanitize_email( $e );
	}
	if ( $g !== '' && function_exists( 'sanitize_text_field' ) ) {
		return sanitize_text_field( $g );
	}
	if ( $e !== '' && function_exists( 'sanitize_text_field' ) ) {
		return sanitize_text_field( $e );
	}
	return '';
}

/**
 * Email cockpit / pill (JS) : priorité google_email puis colonne email sur avi_saas_profiles — pas l’email du compte WordPress.
 * @param int $profile_id avi_saas_profiles.id
 * @return string Chaîne vide ou email sanitizé
 */
function avision_premium_dashboard_profile_email_for_js( $profile_id ) {
	$profile_id = (int) $profile_id;
	if ( $profile_id <= 0 || ! function_exists( 'is_email' ) ) {
		return '';
	}
	global $wpdb;
	if ( ! isset( $wpdb ) || ! $wpdb ) {
		return '';
	}
	$table = $wpdb->prefix . 'avi_saas_profiles';
	if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
		return '';
	}
	$has_google_email_col = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$table}` LIKE %s", 'google_email' ) ) === 'google_email';
	$select_cols          = $has_google_email_col ? '`google_email`, `email`' : '`email`';
	// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- whitelist : google_email + email ou email seul.
	$row = $wpdb->get_row( $wpdb->prepare( "SELECT {$select_cols} FROM `{$table}` WHERE id = %d LIMIT 1", $profile_id ), ARRAY_A );
	if ( ! is_array( $row ) ) {
		return '';
	}
	return avision_premium_dashboard_email_from_profile_row( $row );
}

/**
 * Réputation locale GBP : note moyenne (étoiles) et nombre total d’avis (Business Information v1).
 * Appelle en interne Avision_Database::fetch_gbp_local_reputation (GET location + readMask profile).
 *
 * @param string $access_token Token OAuth.
 * @param string $location_id  Ressource location (accounts/…/locations/…).
 * @param int    $profile_id   ID profil SaaS (logs uniquement).
 * @return array{rating: float|null, review_count: int|null}|null Null si classe BDD indisponible.
 */
function avision_gbp_fetch_local_reputation( $access_token, $location_id, $profile_id = 0 ) {
	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return null;
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	return $db->fetch_gbp_local_reputation( (string) $access_token, (string) $location_id, (int) $profile_id );
}

/**
 * Stub — 5 métriques standards Performance Google (Business Profile Performance API).
 * À brancher sur le futur calcul Visibility / KPI (getDailyMetricsTimeSeries).
 * Métriques cibles : vues Search, vues Maps, appels, clics site, demandes d’itinéraire.
 *
 * Enums Google attendus : BUSINESS_IMPRESSIONS_MOBILE_SEARCH, BUSINESS_IMPRESSIONS_MOBILE_MAPS,
 * CALL_CLICKS, WEBSITE_CLICKS, BUSINESS_DIRECTION_REQUESTS (voir doc API pour variantes desktop).
 *
 * @param string $access_token Token OAuth.
 * @param string $location_id  Ressource location.
 * @param int    $profile_id   ID profil SaaS (logs).
 * @return array{
 *   impressions_search: int|null,
 *   impressions_maps: int|null,
 *   call_clicks: int|null,
 *   website_clicks: int|null,
 *   direction_requests: int|null
 * }|null Stub : retour structure vide (null par clé) jusqu’à implémentation.
 */
function avision_gbp_stub_fetch_five_performance_metrics( $access_token, $location_id, $profile_id = 0 ) {
	$profile_id = (int) $profile_id;
	return array(
		'impressions_search' => null,
		'impressions_maps'   => null,
		'call_clicks'        => null,
		'website_clicks'     => null,
		'direction_requests' => null,
	);
}

/**
 * Façade KPI — Compétiteurs DFS (Maps live + extraction leaders). Ne modifie pas la BDD.
 *
 * @return array{leaders: array, maps_items_count: int, wp_error: string|null}|null
 */
function avision_dfs_kpi_fetch_competitors_leaders( $profile_id, $sector_keyword, $target_city, $country, $business_name, $limit = 3 ) {
	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return null;
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	return $db->dfs_fetch_competitors_leaders_for_kpi( (int) $profile_id, (string) $sector_keyword, (string) $target_city, (string) $country, (string) $business_name, (int) $limit );
}

/**
 * Façade KPI — Stub « Your search keywords » (DFS Labs keyword_suggestions/live).
 *
 * @return array{suggestions: array<int, string>}|null
 */
function avision_dfs_kpi_stub_your_search_keyword_suggestions( $seed_keyword, $location_name, $location_code, $language_code, $profile_id = 0 ) {
	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return null;
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	return $db->dfs_stub_fetch_your_search_keyword_suggestions_for_kpi( (string) $seed_keyword, (string) $location_name, (int) $location_code, (string) $language_code, (int) $profile_id );
}

/**
 * Façade KPI — Growth Radar (lignes avi_saas_keywords alimentées par les scans DFS).
 *
 * @return array{rows: array, count: int}|null
 */
function avision_dfs_kpi_fetch_growth_radar_rows( $profile_id, $limit = 100 ) {
	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return null;
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	return $db->dfs_fetch_growth_radar_keyword_rows_for_kpi( (int) $profile_id, (int) $limit );
}

if ( defined( 'ABSPATH' ) && defined( 'AVISION_PLUGIN_DIR' ) && is_readable( AVISION_PLUGIN_DIR . 'includes/avision-api-diagnostics.php' ) ) {
	require_once AVISION_PLUGIN_DIR . 'includes/avision-api-diagnostics.php';
}

/**
 * Calcule le Score de Visibilité (30–42) et l'estimation Net Worth à partir des données profil + snapshot.
 * Base 30 + 3 (site) + 3 (téléphone) + 3 (≥10 avis) + 3 (catégorie précise). Plus de bonus horaires.
 *
 * @param array $profile_data Clés : has_website (bool), has_phone (bool), category_precise (bool),
 *                            review_count (int), rating (float), position_rank (int|null).
 * @return array ['score' => int 30–42, 'net_worth' => float]
 */
function avi_calculate_visibility_score( $profile_data ) {
	$base = 30;
	$score = $base;
	if ( ! empty( $profile_data['has_website'] ) ) {
		$score += 3;
	}
	if ( ! empty( $profile_data['has_phone'] ) ) {
		$score += 3;
	}
	$review_count = isset( $profile_data['review_count'] ) ? (int) $profile_data['review_count'] : 0;
	if ( $review_count >= 10 ) {
		$score += 3;
	}
	if ( ! empty( $profile_data['category_precise'] ) ) {
		$score += 3;
	}
	$score = min( 42, max( 30, $score ) );

	$rating       = isset( $profile_data['rating'] ) ? (float) $profile_data['rating'] : 0.0;
	$position     = isset( $profile_data['position_rank'] ) ? $profile_data['position_rank'] : null;
	if ( $position === 'Hors Radar' || $position === '' ) {
		$position = null;
	}
	$position = is_numeric( $position ) ? (int) $position : null;

	// Facteur de visibilité selon la position (1–3 = 1.0, 4–10 = 0.7, >10 ou Hors Radar = 0.2)
	if ( $position === null || $position > 10 ) {
		$visibility_factor = 0.2;
	} elseif ( $position <= 3 ) {
		$visibility_factor = 1.0;
	} else {
		$visibility_factor = 0.7;
	}

	$raw_value = ( $review_count * $rating ) * $visibility_factor;
	$net_worth = max( 50.0, round( $raw_value, 2 ) );

	return array(
		'score'     => $score,
		'net_worth' => $net_worth,
	);
}

/**
 * Lance le calcul Score + Net Worth, enregistre le premier point dans avi_saas_rank_history,
 * met à jour le score du profil et passe is_first_sync à 0. À appeler après avi_run_initial_sync().
 *
 * @param int $profile_id ID du profil (avi_saas_profiles.id).
 * @return array|false ['score' => int, 'net_worth' => float, 'saved' => true] ou false.
 */
function avi_run_visibility_calculation( $profile_id ) {
	$profile_id = (int) $profile_id;
	if ( $profile_id <= 0 ) {
		return false;
	}
	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return false;
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	return $db->run_visibility_calculation_and_save( $profile_id );
}

/**
 * Lance la synchronisation initiale (First Sync) pour un profil : Google + DataForSEO Secteur,
 * enregistrement des 3 leaders et mise à jour de last_metrics_snapshot.
 * À appeler lorsque is_first_sync = 1 (ex. après connexion OAuth ou depuis le backend).
 *
 * @param int $profile_id ID du profil (avi_saas_profiles.id).
 * @return array|false ['success' => true, 'position' => int|string, 'leaders' => array, 'google' => array] ou false.
 */
function avi_run_initial_sync( $profile_id ) {
	$profile_id = (int) $profile_id;
	if ( $profile_id <= 0 ) {
		return false;
	}
	if ( ! class_exists( 'Avision\Avision_Database' ) ) {
		return false;
	}
	$db = new \Avision\Avision_Database();
	$db->init();
	return $db->run_initial_sync( $profile_id );
}

// Administrateur WordPress : accès premium et Mode Gardien sans transaction (pour tester "Connecter ma fiche")
add_filter( 'avision_has_guardian_mode', function( $default ) {
	if ( function_exists( 'current_user_can' ) && current_user_can( 'administrator' ) ) {
		return true;
	}
	return $default;
} );
add_filter( 'avision_has_keyword_top3_premium', function( $default ) {
	if ( function_exists( 'current_user_can' ) && current_user_can( 'administrator' ) ) {
		return true;
	}
	return $default;
} );

// Initialize in WordPress or standalone
if (function_exists('add_action')) {
    add_action('plugins_loaded', 'avision_init');
    add_action('rest_api_init', 'avision_register_rest_routes');
    add_action('rest_api_init', 'avision_register_google_updates_proxy_route');
    add_action('rest_api_init', 'avision_register_banners_route');
    add_action('wp_enqueue_scripts', 'avision_register_levels_config_script', 5);

    // Tables "Cœur d'AVI" : création à l'activation du plugin
    if ( function_exists( 'avi_install_core_tables' ) || file_exists( AVISION_PATH . 'install-avi-core-tables.php' ) ) {
        register_activation_hook( __FILE__, 'avi_install_core_tables' );
    }

    // CRON Task Manager : dé-planification propre à la désactivation
    register_deactivation_hook( __FILE__, [ 'Avision\Cron_Task_Manager', 'unschedule' ] );
} else {
    avision_init();
}
