<?php
/**
 * API & Cron Monitor — pings APIs, crons AVI, boucliers Gardien, live tests admin.
 *
 * @package Avision_Dashboard
 */

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

/**
 * Monitoring Control Center (onglet api_monitor).
 */
class Avision_Api_Cron_Monitor {

	const PROFILE_LIVE_TEST_ID    = 349;
	const PROFILE_LIVE_TEST_EMAIL = 'appsumo-btoc@avision.org';
	const PROFILE_LIVE_TEST_UUID  = 'bccc90f7-4f8e-11f1-8a83-fa163e75843f';
	const PROFILE_LIVE_TEST_USER_ID = 110;

	const OPTION_PING_LAST     = 'avision_api_ping_last';
	const OPTION_PING_HISTORY  = 'avision_api_ping_history';
	const OPTION_CRON_LAST_RUN = 'avision_cron_last_run_log';
	const MAX_PROFILE_CAPACITY = 80;
	const HISTORY_MAX          = 20;

	/**
	 * @return void
	 */
	public static function init_crons() {
		add_filter( 'cron_schedules', array( __CLASS__, 'add_five_minute_schedule' ) );
		if ( ! wp_next_scheduled( 'avision_api_ping_cron' ) ) {
			wp_schedule_event( time(), 'every_five_minutes', 'avision_api_ping_cron' );
		}
		add_action( 'avision_api_ping_cron', array( __CLASS__, 'run_api_ping' ) );
	}

	/**
	 * @param array $schedules Schedules WP.
	 * @return array
	 */
	public static function add_five_minute_schedule( $schedules ) {
		if ( ! isset( $schedules['every_five_minutes'] ) ) {
			$schedules['every_five_minutes'] = array(
				'interval' => 300,
				'display'  => 'Every 5 Minutes',
			);
		}
		return $schedules;
	}

	/**
	 * Ping les 9 APIs et persiste le résultat.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function run_api_ping() {
		$results = array();
		$apis    = array(
			'gbp_api'        => array( self::class, 'ping_gbp_api' ),
			'google_places'  => array( self::class, 'ping_google_places' ),
			'maps_embed'     => array( self::class, 'ping_maps_embed' ),
			'dataforseo'     => array( self::class, 'ping_dataforseo' ),
			'openrouter'     => array( self::class, 'ping_openrouter' ),
			'qdrant'         => array( self::class, 'ping_qdrant' ),
			'brevo'          => array( self::class, 'ping_brevo' ),
			'stripe'         => array( self::class, 'ping_stripe' ),
			'google_oauth'   => array( self::class, 'ping_google_oauth_profile' ),
		);

		foreach ( $apis as $name => $callable ) {
			$row = call_user_func( $callable );
			$results[ $name ] = $row;
			if ( isset( $row['status'] ) && $row['status'] === 'fail' ) {
				self::maybe_send_api_alert( $name, 'fail', $row );
			}
		}

		update_option( self::OPTION_PING_LAST, $results, false );

		$history = get_option( self::OPTION_PING_HISTORY, array() );
		if ( ! is_array( $history ) ) {
			$history = array();
		}
		array_unshift(
			$history,
			array(
				'checked_at' => time(),
				'results'    => $results,
			)
		);
		$history = array_slice( $history, 0, self::HISTORY_MAX );
		update_option( self::OPTION_PING_HISTORY, $history, false );

		return $results;
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_row( $status, $code, $latency_ms, $extra = array() ) {
		$row = array(
			'status'      => $status,
			'latency_ms'  => (int) $latency_ms,
			'code'        => (int) $code,
			'checked_at'  => time(),
		);
		return array_merge( $row, is_array( $extra ) ? $extra : array() );
	}

	/**
	 * @param string $url URL.
	 * @param array  $args wp_remote_* args.
	 * @return array{code: int, body: string, error: string}
	 */
	private static function http_request( $url, $args = array() ) {
		$start = microtime( true );
		$resp  = wp_remote_request( $url, $args );
		$ms    = (int) round( ( microtime( true ) - $start ) * 1000 );
		if ( is_wp_error( $resp ) ) {
			return array(
				'code'       => 0,
				'body'       => '',
				'error'      => $resp->get_error_message(),
				'latency_ms' => $ms,
			);
		}
		return array(
			'code'       => (int) wp_remote_retrieve_response_code( $resp ),
			'body'       => (string) wp_remote_retrieve_body( $resp ),
			'error'      => '',
			'latency_ms' => $ms,
		);
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_gbp_api() {
		$r = self::http_request(
			'https://mybusinessbusinessinformation.googleapis.com/',
			array( 'method' => 'GET', 'timeout' => 10 )
		);
		$code = $r['code'];
		if ( $code === 0 ) {
			$status = 'fail';
		} elseif ( $code === 404 ) {
			$status = 'warn';
		} elseif ( $code === 200 || $code === 401 || $code === 403 ) {
			$status = 'ok';
		} else {
			$status = 'warn';
		}
		return self::ping_row( $status, $code, $r['latency_ms'] );
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_google_places() {
		$key = defined( 'AVI_GOOGLE_PLACES_API_KEY' ) ? trim( (string) AVI_GOOGLE_PLACES_API_KEY ) : '';
		if ( $key === '' ) {
			return self::ping_row( 'warn', 0, 0, array( 'note' => 'AVI_GOOGLE_PLACES_API_KEY missing' ) );
		}
		$r = self::http_request(
			'https://places.googleapis.com/v1/places/test',
			array(
				'method'  => 'GET',
				'timeout' => 10,
				'headers' => array(
					'X-Goog-Api-Key'   => $key,
					'X-Goog-FieldMask' => 'id',
				),
			)
		);
		$ok = ( $r['code'] === 404 || $r['code'] === 200 || $r['code'] === 400 );
		return self::ping_row( $ok ? 'ok' : ( $r['code'] === 0 ? 'fail' : 'warn' ), $r['code'], $r['latency_ms'] );
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_maps_embed() {
		$key = defined( 'AVI_GOOGLE_PLACES_API_KEY' ) ? trim( (string) AVI_GOOGLE_PLACES_API_KEY ) : '';
		if ( $key === '' ) {
			return self::ping_row( 'warn', 0, 0, array( 'note' => 'embed key missing' ) );
		}
		$url = 'https://www.google.com/maps/embed/v1/place?key=' . rawurlencode( $key ) . '&q=test';
		$r   = self::http_request( $url, array( 'method' => 'GET', 'timeout' => 10 ) );
		$code = $r['code'];
		if ( $code === 0 ) {
			$status = 'fail';
		} elseif ( $code === 200 ) {
			$status = 'ok';
		} elseif ( $code === 403 ) {
			$status = 'warn';
		} else {
			$status = 'fail';
		}
		return self::ping_row( $status, $code, $r['latency_ms'] );
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_dataforseo() {
		$login    = '';
		$password = '';
		if ( class_exists( 'Avision\Avision_Scan_Engine' ) ) {
			$creds = \Avision\Avision_Scan_Engine::get_dataforseo_credentials();
			if ( is_array( $creds ) ) {
				$login    = isset( $creds['login'] ) ? (string) $creds['login'] : '';
				$password = isset( $creds['password'] ) ? (string) $creds['password'] : '';
			}
		}
		if ( $login === '' || $password === '' ) {
			$login    = (string) get_option( 'dataforseo_login', '' );
			$password = (string) get_option( 'dataforseo_password', '' );
		}
		if ( $login === '' || $password === '' ) {
			return self::ping_row( 'warn', 0, 0, array( 'note' => 'DataForSEO credentials missing' ) );
		}
		$r = self::http_request(
			'https://api.dataforseo.com/v3/serp/google/maps/live/regular',
			array(
				'method'  => 'POST',
				'timeout' => 15,
				'headers' => array(
					'Authorization' => 'Basic ' . base64_encode( $login . ':' . $password ),
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode(
					array(
						array(
							'keyword'       => 'test',
							'language_code' => 'en',
							'device'        => 'desktop',
							'os'            => 'windows',
							'depth'         => 10,
						),
					)
				),
			)
		);
		$status = 'fail';
		if ( $r['code'] === 200 ) {
			$status = 'ok';
		} elseif ( $r['code'] === 401 ) {
			$status = 'warn';
		}
		return self::ping_row( $status, $r['code'], $r['latency_ms'] );
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_openrouter() {
		$key = '';
		if ( defined( 'AVISION_OPENROUTER_API_KEY' ) && AVISION_OPENROUTER_API_KEY !== '' ) {
			$key = (string) AVISION_OPENROUTER_API_KEY;
		} elseif ( defined( 'OPENROUTER_API_KEY' ) ) {
			$key = (string) OPENROUTER_API_KEY;
		}
		if ( $key === '' ) {
			return self::ping_row( 'warn', 0, 0, array( 'note' => 'OpenRouter key missing' ) );
		}
		$r = self::http_request(
			'https://openrouter.ai/api/v1/models',
			array(
				'method'  => 'GET',
				'timeout' => 12,
				'headers' => array( 'Authorization' => 'Bearer ' . $key ),
			)
		);
		return self::ping_row( $r['code'] === 200 ? 'ok' : 'fail', $r['code'], $r['latency_ms'] );
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_qdrant() {
		$r = self::http_request(
			'http://localhost:6333/collections/avi_knowledge',
			array(
				'method'  => 'GET',
				'timeout' => 8,
				'headers' => array( 'api-key' => '!!AvisionMemory2026!!' ),
			)
		);
		$code = $r['code'];
		if ( $code === 0 ) {
			$status = 'fail';
		} elseif ( $code === 200 ) {
			$status = 'ok';
		} elseif ( $code === 401 ) {
			$status = 'warn';
		} else {
			$status = 'fail';
		}
		return self::ping_row( $status, $code, $r['latency_ms'] );
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_brevo() {
		$key = defined( 'AVISION_BREVO_API_KEY' ) ? trim( (string) AVISION_BREVO_API_KEY ) : '';
		if ( $key === '' ) {
			return self::ping_row( 'warn', 0, 0, array( 'note' => 'AVISION_BREVO_API_KEY missing' ) );
		}
		$r = self::http_request(
			'https://api.brevo.com/v3/account',
			array(
				'method'  => 'GET',
				'timeout' => 10,
				'headers' => array( 'api-key' => $key ),
			)
		);
		return self::ping_row( $r['code'] === 200 ? 'ok' : 'fail', $r['code'], $r['latency_ms'] );
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_stripe() {
		$key = defined( 'AVISION_STRIPE_SECRET_KEY' ) ? trim( (string) AVISION_STRIPE_SECRET_KEY ) : '';
		if ( $key === '' ) {
			$key = (string) get_option( 'avision_stripe_secret_key', '' );
		}
		if ( $key === '' ) {
			return self::ping_row( 'warn', 0, 0, array( 'note' => 'Stripe secret missing' ) );
		}
		$r = self::http_request(
			'https://api.stripe.com/v1/balance',
			array(
				'method'  => 'GET',
				'timeout' => 10,
				'headers' => array( 'Authorization' => 'Bearer ' . $key ),
			)
		);
		return self::ping_row( $r['code'] === 200 ? 'ok' : 'fail', $r['code'], $r['latency_ms'] );
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function ping_google_oauth_profile() {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! $wpdb ) {
			return self::ping_row( 'fail', 0, 0 );
		}
		// Check global OAuth : compter profils avec token valide (pas un profil spécifique)
		$table = $wpdb->prefix . 'avi_saas_profiles';
		$now   = current_time( 'mysql' );
		$count_valid = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM `{$table}` WHERE google_access_token IS NOT NULL AND google_access_token != '' AND (google_token_expires_at IS NULL OR google_token_expires_at > %s)",
				$now
			)
		);
		$count_total = (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM `{$table}` WHERE google_access_token IS NOT NULL AND google_access_token != ''"
		);
		if ( $count_total === 0 ) {
			return self::ping_row( 'warn', 0, 0, array( 'note' => 'no connected profiles' ) );
		}
		if ( $count_valid === 0 ) {
			return self::ping_row( 'warn', 0, 0, array( 'note' => 'all tokens expired', 'total' => $count_total ) );
		}
		return self::ping_row( 'ok', 200, 0, array( 'valid' => $count_valid, 'total' => $count_total ) );
	}

	/**
	 * @param string               $api_name Nom API.
	 * @param string               $status   fail|warn.
	 * @param array<string, mixed> $details  Détails.
	 * @return void
	 */
	public static function maybe_send_api_alert( $api_name, $status, $details = array() ) {
		if ( $status !== 'fail' || ! function_exists( 'wp_mail' ) ) {
			return;
		}
		$subject = '[AVI API ALERT] ' . sanitize_text_field( (string) $api_name ) . ' — FAIL';
		$body    = "API: {$api_name}\nStatus: {$status}\nTime: " . gmdate( 'Y-m-d H:i:s' ) . " UTC\n";
		$body   .= "Profile test: " . self::PROFILE_LIVE_TEST_ID . ' (' . self::PROFILE_LIVE_TEST_EMAIL . ")\n\n";
		$body   .= wp_json_encode( $details, JSON_PRETTY_PRINT );
		wp_mail( self::PROFILE_LIVE_TEST_EMAIL, $subject, $body );
		wp_mail( 'contact@avision.org', $subject, $body );
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	public static function get_all_crons_status() {
		$crons = _get_cron_array();
		if ( ! is_array( $crons ) ) {
			$crons = array();
		}
		$last_log = get_option( self::OPTION_CRON_LAST_RUN, array() );
		if ( ! is_array( $last_log ) ) {
			$last_log = array();
		}
		$schedules = wp_get_schedules();
		$out       = array();
		$seen      = array();

		foreach ( $crons as $ts => $hooks ) {
			if ( ! is_array( $hooks ) ) {
				continue;
			}
			foreach ( $hooks as $hook => $events ) {
				if ( ! self::is_avi_hook( $hook ) || isset( $seen[ $hook ] ) ) {
					continue;
				}
				$seen[ $hook ] = true;
				$interval_name = '';
				$interval_sec  = 0;
				if ( is_array( $events ) ) {
					foreach ( $events as $event ) {
						if ( isset( $event['schedule'] ) ) {
							$interval_name = (string) $event['schedule'];
							if ( isset( $schedules[ $interval_name ]['interval'] ) ) {
								$interval_sec = (int) $schedules[ $interval_name ]['interval'];
							}
						}
						break;
					}
				}
				$next_run = (int) $ts;
				$status   = 'unknown';
				$now      = time();
				if ( $next_run > 0 ) {
					if ( $next_run < $now - ( 2 * max( 60, $interval_sec ) ) ) {
						$status = 'late';
					} else {
						$status = 'on_time';
					}
				}
				$out[] = array(
					'hook'              => $hook,
					'interval'          => $interval_name,
					'interval_seconds'  => $interval_sec,
					'next_run'          => $next_run,
					'next_run_human'    => $next_run ? gmdate( 'Y-m-d H:i:s', $next_run ) . ' UTC' : '—',
					'last_run'          => isset( $last_log[ $hook ] ) ? (int) $last_log[ $hook ] : 0,
					'last_run_human'    => isset( $last_log[ $hook ] ) ? gmdate( 'Y-m-d H:i:s', (int) $last_log[ $hook ] ) . ' UTC' : '—',
					'profiles_count'    => self::count_profiles_for_cron( $hook ),
					'bouclier_associe'  => self::shield_for_cron_hook( $hook ),
					'statut'            => $status,
				);
			}
		}

		usort(
			$out,
			function ( $a, $b ) {
				return strcmp( $a['hook'], $b['hook'] );
			}
		);
		return $out;
	}

	/**
	 * @param string $hook Hook.
	 * @return bool
	 */
	public static function is_avi_hook_public( $hook ) {
		$h = strtolower( (string) $hook );
		return ( 0 === strpos( $h, 'avi_' ) || 0 === strpos( $h, 'avision_' ) );
	}

	/**
	 * @param string $hook Hook.
	 * @return bool
	 */
	private static function is_avi_hook( $hook ) {
		return self::is_avi_hook_public( $hook );
	}

	/**
	 * @param string $hook Hook cron.
	 * @return int
	 */
	private static function count_profiles_for_cron( $hook ) {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! $wpdb ) {
			return 0;
		}
		$table = $wpdb->prefix . 'avi_saas_profiles';
		if ( false !== strpos( $hook, 'keyword' ) ) {
			$kw = $wpdb->prefix . 'avi_saas_keywords';
			return (int) $wpdb->get_var( "SELECT COUNT(DISTINCT profile_id) FROM `{$kw}`" );
		}
		if ( false !== strpos( $hook, 'guardian' ) || false !== strpos( $hook, 'scan_orchestrator' ) ) {
			return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}` WHERE COALESCE(guardian_active,0) = 1" );
		}
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
	}

	/**
	 * @param string $hook Hook.
	 * @return string
	 */
	private static function shield_for_cron_hook( $hook ) {
		$map = array(
			'avision_hourly_scan_orchestrator' => 'NAP / Spy / Sentinel',
			'avision_keyword_scan_queue'       => 'Sentinel (Radar)',
			'avision_api_ping_cron'            => '—',
		);
		return isset( $map[ $hook ] ) ? $map[ $hook ] : '—';
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	public static function get_guardian_shields_status() {
		global $wpdb;
		$profiles_active = 0;
		if ( isset( $wpdb ) && $wpdb ) {
			$table           = $wpdb->prefix . 'avi_saas_profiles';
			$profiles_active = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}` WHERE COALESCE(guardian_active,0) = 1" );
		}
		$last_log = get_option( self::OPTION_CRON_LAST_RUN, array() );
		$orch_ts  = is_array( $last_log ) && isset( $last_log['avision_hourly_scan_orchestrator'] )
			? (int) $last_log['avision_hourly_scan_orchestrator']
			: 0;

		$shields = array(
			array( 'id' => 'nap_shield', 'nom' => 'NAP Shield', 'api_used' => 'Google Business Profile', 'cron_hook' => 'avision_hourly_scan_orchestrator', 'frequency' => 'hourly', 'bouclier' => 'Veilleurs V1' ),
			array( 'id' => 'spy_alert', 'nom' => 'Spy Alert', 'api_used' => 'Google Places / GBP', 'cron_hook' => 'avision_hourly_scan_orchestrator', 'frequency' => 'hourly', 'bouclier' => 'Veilleurs V1' ),
			array( 'id' => 'sentinel_top3', 'nom' => 'Sentinel Top 3', 'api_used' => 'DataForSEO', 'cron_hook' => 'avision_keyword_scan_queue', 'frequency' => 'every_minute', 'bouclier' => 'Veilleurs V1' ),
			array( 'id' => 'proof_extractor', 'nom' => 'Proof Extractor', 'api_used' => 'GBP Reviews + OpenRouter', 'cron_hook' => 'avision_hourly_scan_orchestrator', 'frequency' => 'hourly', 'bouclier' => 'Créateurs V2' ),
			array( 'id' => 'hybrid_responses', 'nom' => 'Hybrid Responses', 'api_used' => 'GBP Reviews API', 'cron_hook' => 'avision_hourly_scan_orchestrator', 'frequency' => 'hourly', 'bouclier' => 'Créateurs V2' ),
			array( 'id' => 'armored_reputation', 'nom' => 'Armored Reputation', 'api_used' => 'GBP Reviews API', 'cron_hook' => 'avision_hourly_scan_orchestrator', 'frequency' => 'hourly', 'bouclier' => 'Créateurs V2' ),
			array( 'id' => 'image_cleaner', 'nom' => 'Image Cleaner', 'api_used' => 'GBP Media', 'cron_hook' => 'avision_hourly_scan_orchestrator', 'frequency' => 'hourly', 'bouclier' => 'Mainteneurs V3' ),
			array( 'id' => 'drip_feed', 'nom' => 'Gallery Drip-Feed', 'api_used' => 'GBP Media', 'cron_hook' => 'avision_hourly_scan_orchestrator', 'frequency' => 'hourly', 'bouclier' => 'Mainteneurs V3' ),
			array( 'id' => 'infinite_posts', 'nom' => 'Infinite Posts', 'api_used' => 'GBP Posts', 'cron_hook' => 'avision_hourly_scan_orchestrator', 'frequency' => 'hourly', 'bouclier' => 'Mainteneurs V3' ),
		);

		foreach ( $shields as $i => $s ) {
			$shields[ $i ]['last_run']         = $orch_ts;
			$shields[ $i ]['last_run_human']   = $orch_ts ? gmdate( 'Y-m-d H:i:s', $orch_ts ) . ' UTC' : '—';
			$shields[ $i ]['profiles_active'] = $profiles_active;
			$shields[ $i ]['statut']          = $profiles_active > 0 ? 'ok' : 'idle';
			if ( $orch_ts > 0 && $orch_ts < time() - 7200 ) {
				$shields[ $i ]['statut'] = 'error';
			}
		}
		return $shields;
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	public static function get_onboarding_checks_for_profile( $profile_id ) {
		global $wpdb;
		$profile_id = max( 1, (int) $profile_id );
		$row      = array();
		if ( isset( $wpdb ) && $wpdb ) {
			$table = $wpdb->prefix . 'avi_saas_profiles';
			$row   = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT business_name, category, neighborhood, city, phone, last_metrics_snapshot, google_data_cache
					 FROM `{$table}` WHERE id = %d LIMIT 1",
					$profile_id
				),
				ARRAY_A
			);
		}
		if ( ! is_array( $row ) ) {
			$row = array();
		}
		$website = '';
		if ( ! empty( $row['last_metrics_snapshot'] ) ) {
			$snap = json_decode( (string) $row['last_metrics_snapshot'], true );
			if ( is_array( $snap ) && ! empty( $snap['website'] ) ) {
				$website = (string) $snap['website'];
			}
		}
		if ( $website === '' && ! empty( $row['google_data_cache'] ) ) {
			$cache = json_decode( (string) $row['google_data_cache'], true );
			if ( is_array( $cache ) && isset( $cache['nap']['websiteUri'] ) ) {
				$website = (string) $cache['nap']['websiteUri'];
			}
		}

		$bn = isset( $row['business_name'] ) ? trim( (string) $row['business_name'] ) : '';
		$cat = isset( $row['category'] ) ? trim( (string) $row['category'] ) : '';
		$loc = trim( ( isset( $row['neighborhood'] ) ? (string) $row['neighborhood'] : '' ) . ' ' . ( isset( $row['city'] ) ? (string) $row['city'] : '' ) );
		$phone = isset( $row['phone'] ) ? trim( (string) $row['phone'] ) : '';

		$checks = array(
			array(
				'key'           => 'business_name',
				'label'         => 'Nom établissement',
				'api'           => 'Profil SaaS',
				'value'         => $bn,
				'score_impact'  => 3,
				'pass'          => ( $bn !== '' && ! in_array( strtolower( $bn ), array( 'profil', 'business' ), true ) ),
			),
			array(
				'key'           => 'category',
				'label'         => 'Catégorie',
				'api'           => 'GBP / Profil',
				'value'         => $cat,
				'score_impact'  => 5,
				'pass'          => ( $cat !== '' && ! in_array( strtolower( $cat ), array( 'client', 'client' ), true ) ),
			),
			array(
				'key'           => 'location',
				'label'         => 'Quartier ou ville',
				'api'           => 'Profil SaaS',
				'value'         => trim( $loc ),
				'score_impact'  => 2,
				'pass'          => ( trim( $loc ) !== '' ),
			),
			array(
				'key'           => 'phone',
				'label'         => 'Téléphone',
				'api'           => 'GBP NAP',
				'value'         => $phone,
				'score_impact'  => 2,
				'pass'          => ( $phone !== '' ),
			),
			array(
				'key'           => 'website',
				'label'         => 'Site web',
				'api'           => 'GBP NAP',
				'value'         => $website,
				'score_impact'  => 2,
				'pass'          => ( $website !== '' ),
			),
		);

		$passed = 0;
		foreach ( $checks as $c ) {
			if ( ! empty( $c['pass'] ) ) {
				$passed++;
			}
		}

		return array(
			'profile_id' => $profile_id,
			'checks'     => $checks,
			'passed'     => $passed,
			'total'      => count( $checks ),
		);
	}

	/**
	 * @param string $test_name  Test.
	 * @param int    $profile_id Profil.
	 * @return array<string, mixed>
	 */
	public static function run_live_test( $test_name, $profile_id ) {
		$test_name  = sanitize_key( (string) $test_name );
		$profile_id = max( 1, (int) $profile_id );
		$start      = microtime( true );

		switch ( $test_name ) {
			case 'gbp_scan':
				$result = self::live_test_gbp_scan( $profile_id );
				break;
			case 'reviews':
				$result = self::live_test_reviews( $profile_id );
				break;
			case 'keywords':
				$result = self::live_test_keywords( $profile_id );
				break;
			case 'chat':
				$result = self::live_test_chat( $profile_id );
				break;
			case 'proof_extractor':
				$result = self::live_test_proof_extractor( $profile_id );
				break;
			case 'maps_iframe':
				$result = self::live_test_maps_iframe( $profile_id );
				break;
			case 'email_brevo':
				$result = self::live_test_email_brevo( $profile_id );
				break;
			case 'oauth_status':
				$result = self::live_test_oauth_status( $profile_id );
				break;
			case 'competitors':
				$result = self::live_test_competitors( $profile_id );
				break;
			default:
				$result = array( 'error' => 'Unknown test: ' . $test_name );
		}

		$result['test_name']   = $test_name;
		$result['profile_id']  = $profile_id;
		$result['latency_ms']  = (int) round( ( microtime( true ) - $start ) * 1000 );
		$result['status']      = empty( $result['error'] ) ? 'ok' : 'fail';
		return $result;
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_gbp_scan( $profile_id ) {
		$url = rest_url( 'avision/v1/first-sync' );
		$r   = wp_remote_post(
			$url,
			array(
				'timeout' => 30,
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode(
					array(
						'profile_id' => $profile_id,
						'force'      => true,
					)
				),
				'cookies' => $_COOKIE,
			)
		);
		if ( is_wp_error( $r ) ) {
			return array( 'error' => $r->get_error_message() );
		}
		$body = json_decode( wp_remote_retrieve_body( $r ), true );
		global $wpdb;
		$nap_fields = array();
		if ( isset( $wpdb ) && $wpdb ) {
			$table = $wpdb->prefix . 'avi_saas_profiles';
			$raw   = $wpdb->get_var( $wpdb->prepare( "SELECT google_data_cache FROM `{$table}` WHERE id = %d", $profile_id ) );
			if ( $raw ) {
				$cache = json_decode( (string) $raw, true );
				$nap   = is_array( $cache ) && isset( $cache['nap'] ) ? $cache['nap'] : array();
				$nap_fields = array(
					'title'       => isset( $nap['title'] ) ? $nap['title'] : '',
					'category'    => isset( $nap['categories']['primaryCategory']['displayName'] ) ? $nap['categories']['primaryCategory']['displayName'] : '',
					'phone'       => isset( $nap['phoneNumbers']['primaryPhone'] ) ? $nap['phoneNumbers']['primaryPhone'] : '',
					'website'     => isset( $nap['websiteUri'] ) ? $nap['websiteUri'] : '',
					'description' => isset( $nap['profile']['description'] ) ? substr( (string) $nap['profile']['description'], 0, 120 ) : '',
				);
			}
		}
		return array(
			'http_code'  => (int) wp_remote_retrieve_response_code( $r ),
			'response'   => $body,
			'gbp_fields' => $nap_fields,
		);
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_reviews( $profile_id ) {
		global $wpdb;
		$place_id = '';
		if ( isset( $wpdb ) && $wpdb ) {
			$table    = $wpdb->prefix . 'avi_saas_profiles';
			$place_id = (string) $wpdb->get_var( $wpdb->prepare( "SELECT google_place_id FROM `{$table}` WHERE id = %d", $profile_id ) );
		}
		$key = defined( 'AVI_GOOGLE_PLACES_API_KEY' ) ? trim( (string) AVI_GOOGLE_PLACES_API_KEY ) : '';
		if ( $place_id === '' || $key === '' ) {
			return array( 'error' => 'place_id or API key missing' );
		}
		$resource = $place_id;
		if ( strpos( $resource, 'places/' ) === 0 ) {
			$resource = substr( $resource, 7 );
		}
		$r = self::http_request(
			'https://places.googleapis.com/v1/places/' . rawurlencode( $resource ),
			array(
				'method'  => 'GET',
				'timeout' => 15,
				'headers' => array(
					'X-Goog-Api-Key'   => $key,
					'X-Goog-FieldMask' => 'rating,userRatingCount,reviews',
				),
			)
		);
		$data = json_decode( $r['body'], true );
		$revs = array();
		if ( is_array( $data ) && ! empty( $data['reviews'] ) && is_array( $data['reviews'] ) ) {
			$revs = array_slice( $data['reviews'], 0, 3 );
		}
		return array(
			'rating'            => isset( $data['rating'] ) ? $data['rating'] : null,
			'userRatingCount'   => isset( $data['userRatingCount'] ) ? $data['userRatingCount'] : null,
			'reviews_sample'    => $revs,
			'http_code'         => $r['code'],
		);
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_keywords( $profile_id ) {
		global $wpdb;
		$rows = array();
		if ( isset( $wpdb ) && $wpdb ) {
			$table = $wpdb->prefix . 'avi_saas_keywords';
			$rows  = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT keyword, current_rank, last_scan_at FROM `{$table}` WHERE profile_id = %d ORDER BY id ASC LIMIT 3",
					$profile_id
				),
				ARRAY_A
			);
		}
		return array( 'keywords' => is_array( $rows ) ? $rows : array() );
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_chat( $profile_id ) {
		$base = defined( 'AVISION_BACKEND_INTERNAL_URL' ) ? rtrim( (string) AVISION_BACKEND_INTERNAL_URL, '/' ) : 'http://127.0.0.1:8001';
		$r    = wp_remote_post(
			$base . '/api/v1/chat',
			array(
				'timeout' => 30,
				'headers' => array(
					'Content-Type'     => 'application/json',
					'X-WP-User-ID'     => (string) self::PROFILE_LIVE_TEST_USER_ID,
					'X-Business-ID'    => (string) $profile_id,
					'X-Profile-UUID'   => self::PROFILE_LIVE_TEST_UUID,
				),
				'body'    => wp_json_encode(
					array(
						'user_input'      => 'How do I improve my score?',
						'user_type'       => 'b2c',
						'language'        => 'en',
						'dashboard_type'  => 'b2c',
						'business_name'   => 'Anthony Verrons',
						'city'            => 'Pau',
						'current_score'   => 36,
						'active_level_id' => 1,
					)
				),
			)
		);
		if ( is_wp_error( $r ) ) {
			return array( 'error' => $r->get_error_message() );
		}
		$data = json_decode( wp_remote_retrieve_body( $r ), true );
		return array(
			'reply'      => is_array( $data ) && isset( $data['reply'] ) ? $data['reply'] : ( is_array( $data ) && isset( $data['message'] ) ? $data['message'] : wp_remote_retrieve_body( $r ) ),
			'model_used' => is_array( $data ) && isset( $data['model_used'] ) ? $data['model_used'] : '',
			'rag_used'   => is_array( $data ) && isset( $data['rag_used'] ) ? $data['rag_used'] : null,
			'http_code'  => (int) wp_remote_retrieve_response_code( $r ),
		);
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_proof_extractor( $profile_id ) {
		$rev = self::live_test_reviews( $profile_id );
		if ( ! empty( $rev['error'] ) ) {
			return $rev;
		}
		$text = '';
		if ( ! empty( $rev['reviews_sample'][0] ) ) {
			$sample = $rev['reviews_sample'][0];
			if ( is_array( $sample ) && isset( $sample['text']['text'] ) ) {
				$text = (string) $sample['text']['text'];
			}
		}
		if ( $text === '' ) {
			return array( 'error' => 'No 5-star review text found', 'dry_run' => true );
		}
		$base = defined( 'AVISION_BACKEND_INTERNAL_URL' ) ? rtrim( (string) AVISION_BACKEND_INTERNAL_URL, '/' ) : 'http://127.0.0.1:8001';
		$r    = wp_remote_post(
			$base . '/api/v1/chat',
			array(
				'timeout' => 30,
				'headers' => array(
					'Content-Type'     => 'application/json',
					'X-WP-User-ID'     => (string) self::PROFILE_LIVE_TEST_USER_ID,
					'X-Profile-UUID'   => self::PROFILE_LIVE_TEST_UUID,
				),
				'body'    => wp_json_encode(
					array(
						'user_input' => 'Generate a GBP post based on this 5-star review: ' . $text,
						'user_type'  => 'b2c',
						'language'   => 'en',
					)
				),
			)
		);
		if ( is_wp_error( $r ) ) {
			return array( 'error' => $r->get_error_message(), 'dry_run' => true );
		}
		$data = json_decode( wp_remote_retrieve_body( $r ), true );
		return array(
			'dry_run'          => true,
			'review_excerpt'   => substr( $text, 0, 200 ),
			'generated_post'   => is_array( $data ) && isset( $data['reply'] ) ? $data['reply'] : wp_remote_retrieve_body( $r ),
			'http_code'        => (int) wp_remote_retrieve_response_code( $r ),
		);
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_maps_iframe( $profile_id ) {
		global $wpdb;
		$place_id = '';
		if ( isset( $wpdb ) && $wpdb ) {
			$table    = $wpdb->prefix . 'avi_saas_profiles';
			$place_id = (string) $wpdb->get_var( $wpdb->prepare( "SELECT google_place_id FROM `{$table}` WHERE id = %d", $profile_id ) );
		}
		$key = defined( 'AVI_GOOGLE_PLACES_API_KEY' ) ? trim( (string) AVI_GOOGLE_PLACES_API_KEY ) : '';
		if ( $key === '' ) {
			return array( 'error' => 'API key missing' );
		}
		$q    = rawurlencode( 'Anthony Verrons Pau' );
		$src  = 'https://www.google.com/maps/embed/v1/place?key=' . rawurlencode( $key ) . '&q=' . $q;
		$html = '<iframe width="400" height="300" style="border:0" loading="lazy" src="' . esc_url( $src ) . '"></iframe>';
		return array(
			'place_id'    => $place_id,
			'embed_url'   => $src,
			'iframe_html' => $html,
		);
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_email_brevo( $profile_id ) {
		$to     = self::PROFILE_LIVE_TEST_EMAIL;
		$sent   = wp_mail(
			$to,
			'[AVI Live Test] Email Brevo OK — ' . current_time( 'mysql' ),
			'Test email depuis AVI Control Center Live Tests. Si vous recevez cet email, Brevo fonctionne correctement.'
		);
		return array(
			'sent'       => (bool) $sent,
			'to'         => $to,
			'profile_id' => (int) $profile_id,
		);
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_oauth_status( $profile_id ) {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! $wpdb ) {
			return array( 'error' => 'no db' );
		}
		$table     = $wpdb->prefix . 'avi_saas_profiles';
		$gbp_loc   = 'google_location_' . 'name';
		$row       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT google_access_token, google_token_expires_at, `{$gbp_loc}` FROM `{$table}` WHERE id = %d",
				$profile_id
			),
			ARRAY_A
		);
		if ( ! is_array( $row ) ) {
			return array( 'token_valid' => false, 'expires_in_seconds' => 0 );
		}
		$exp_ts = isset( $row['google_token_expires_at'] ) ? strtotime( (string) $row['google_token_expires_at'] ) : 0;
		$valid  = ! empty( $row['google_access_token'] ) && ( $exp_ts === 0 || $exp_ts > time() );
		return array(
			'token_valid'        => $valid,
			'expires_in_seconds' => $exp_ts > 0 ? max( 0, $exp_ts - time() ) : null,
			'gbp_oauth_location' => isset( $row[ $gbp_loc ] ) ? $row[ $gbp_loc ] : '',
		);
	}

	/**
	 * @param int $profile_id Profil.
	 * @return array<string, mixed>
	 */
	private static function live_test_competitors( $profile_id ) {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! $wpdb ) {
			return array( 'error' => 'no db' );
		}

		$comp_table = $wpdb->prefix . 'avi_saas_competitors';
		$existing   = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT competitor_name, main_category, current_rank, rating, reviews_count FROM `{$comp_table}` WHERE profile_id = %d ORDER BY current_rank ASC LIMIT 3",
				$profile_id
			),
			ARRAY_A
		);
		if ( is_array( $existing ) && count( $existing ) >= 3 ) {
			$comp = array();
			foreach ( $existing as $row ) {
				if ( ! is_array( $row ) ) {
					continue;
				}
				$comp[] = array(
					'name'          => isset( $row['competitor_name'] ) ? (string) $row['competitor_name'] : '',
					'address'       => '',
					'rating'        => isset( $row['rating'] ) ? $row['rating'] : null,
					'maps_position' => isset( $row['current_rank'] ) ? $row['current_rank'] : null,
				);
			}
			return array(
				'source'         => 'database',
				'fallback_level' => 1,
				'keyword_used'   => 'from DB',
				'competitors'    => $comp,
			);
		}

		$table = $wpdb->prefix . 'avi_saas_profiles';
		$row   = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT category, city, neighborhood, business_name, country FROM `{$table}` WHERE id = %d",
				$profile_id
			),
			ARRAY_A
		);
		if ( ! is_array( $row ) ) {
			return array( 'error' => 'profile not found' );
		}
		$category = trim( (string) ( $row['category'] ?? '' ) );
		$city     = trim( (string) ( $row['city'] ?? '' ) );
		$hood     = trim( (string) ( $row['neighborhood'] ?? '' ) );
		$name     = trim( (string) ( $row['business_name'] ?? '' ) );
		$country  = trim( (string) ( $row['country'] ?? 'FR' ) );
		$generic  = $category !== '' ? strtok( $category, ' ' ) : '';

		$levels = array(
			1 => trim( $category . ' ' . $hood ),
			2 => trim( $category . ' ' . $city ),
			3 => trim( $generic . ' ' . $hood ),
			4 => trim( $generic . ' ' . $city ),
		);

		$raw_sample = '';
		foreach ( $levels as $lvl => $kw ) {
			if ( $kw === '' ) {
				continue;
			}
			$dfs_http = self::dataforseo_maps_http_sample( $kw, $profile_id );
			if ( $dfs_http['raw_sample'] !== '' ) {
				$raw_sample = $dfs_http['raw_sample'];
			}
			$comp = isset( $dfs_http['competitors'] ) && is_array( $dfs_http['competitors'] ) ? $dfs_http['competitors'] : array();
			if ( count( $comp ) >= 3 ) {
				return array(
					'fallback_level' => $lvl,
					'keyword_used'   => $kw,
					'competitors'    => $comp,
					'raw_sample'     => $raw_sample,
					'dfs_http_code'  => $dfs_http['code'],
				);
			}
		}
		return array(
			'fallback_level' => 0,
			'keyword_used'   => '',
			'competitors'    => array(),
			'raw_sample'     => $raw_sample,
			'note'           => 'Fewer than 3 competitors at all fallback levels',
		);
	}

	/**
	 * Requête DataForSEO Maps (échantillon corps pour diagnostic).
	 *
	 * @param string $keyword    Mot-clé.
	 * @param int    $profile_id Profil (ville → location_code DataForSEO).
	 * @return array{raw_sample: string, code: int, competitors: array<int, array<string, mixed>>}
	 */
	private static function dataforseo_maps_http_sample( $keyword, $profile_id ) {
		$login    = '';
		$password = '';
		if ( class_exists( 'Avision\Avision_Scan_Engine' ) ) {
			$creds = \Avision\Avision_Scan_Engine::get_dataforseo_credentials();
			if ( is_array( $creds ) ) {
				$login    = isset( $creds['login'] ) ? (string) $creds['login'] : '';
				$password = isset( $creds['password'] ) ? (string) $creds['password'] : '';
			}
		}
		if ( $login === '' || $password === '' ) {
			$login    = (string) get_option( 'dataforseo_login', '' );
			$password = (string) get_option( 'dataforseo_password', '' );
		}
		if ( $login === '' || $password === '' ) {
			return array( 'raw_sample' => '', 'code' => 0, 'competitors' => array() );
		}

		$location_code = 1006094;
		$city_codes    = array(
			'Paris' => 1006094,
			'Lyon'  => 1006121,
			'Pau'   => 1006183,
		);
		global $wpdb;
		if ( isset( $wpdb ) && $wpdb && (int) $profile_id > 0 ) {
			$table = $wpdb->prefix . 'avi_saas_profiles';
			$city_row = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT city FROM `{$table}` WHERE id = %d",
					(int) $profile_id
				)
			);
			$city = is_string( $city_row ) ? trim( $city_row ) : '';
			if ( $city !== '' ) {
				foreach ( $city_codes as $city_name => $code ) {
					if ( strcasecmp( $city, $city_name ) === 0 ) {
						$location_code = $code;
						break;
					}
				}
			}
		}

		$task = array(
			'keyword'       => $keyword,
			'language_code' => 'fr',
			'location_code' => $location_code,
			'device'        => 'desktop',
			'os'            => 'windows',
			'depth'         => 10,
		);

		$response = wp_remote_post(
			'https://api.dataforseo.com/v3/serp/google/maps/live/advanced',
			array(
				'method'  => 'POST',
				'timeout' => 20,
				'headers' => array(
					'Authorization' => 'Basic ' . base64_encode( $login . ':' . $password ),
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode( array( $task ) ),
			)
		);
		if ( is_wp_error( $response ) ) {
			return array(
				'raw_sample'  => substr( $response->get_error_message(), 0, 500 ),
				'code'        => 0,
				'competitors' => array(),
			);
		}
		$body_str = wp_remote_retrieve_body( $response );
		$body     = json_decode( $body_str, true );
		$items    = array();
		if ( is_array( $body ) && isset( $body['tasks'][0]['result'][0]['items'] ) && is_array( $body['tasks'][0]['result'][0]['items'] ) ) {
			$items = $body['tasks'][0]['result'][0]['items'];
		}
		$competitors = array();
		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$item_name = isset( $item['title'] ) ? trim( (string) $item['title'] ) : '';
			if ( $item_name === '' ) {
				continue;
			}
			$competitors[] = array(
				'name'          => $item_name,
				'address'       => isset( $item['address'] ) ? $item['address'] : '',
				'rating'        => isset( $item['rating']['value'] ) ? $item['rating']['value'] : null,
				'maps_position' => isset( $item['rank_group'] ) ? $item['rank_group'] : null,
			);
			if ( count( $competitors ) >= 3 ) {
				break;
			}
		}
		return array(
			'raw_sample'  => substr( (string) $body_str, 0, 500 ),
			'code'        => (int) wp_remote_retrieve_response_code( $response ),
			'competitors' => $competitors,
		);
	}

	/**
	 * Labels API pour l’UI.
	 *
	 * @return array<string, string>
	 */
	public static function get_api_labels() {
		return array(
			'gbp_api'       => 'Google Business Profile API',
			'google_places' => 'Google Places API',
			'maps_embed'    => 'Google Maps Embed',
			'dataforseo'    => 'DataForSEO',
			'openrouter'    => 'OpenRouter',
			'qdrant'        => 'Qdrant',
			'brevo'         => 'Brevo',
			'stripe'        => 'Stripe',
			'google_oauth'  => 'Google OAuth (profil ' . self::PROFILE_LIVE_TEST_ID . ')',
		);
	}

	/**
	 * Usage texte par API.
	 *
	 * @return array<string, string>
	 */
	public static function get_api_usage() {
		return array(
			'gbp_api'       => 'Fiche GBP, sync NAP, posts',
			'google_places' => 'Avis, photos, détails lieu',
			'maps_embed'    => 'Carte intégrée site / L5',
			'dataforseo'    => 'Radar, concurrents, Top 3',
			'openrouter'    => 'Chat AVI, Proof Extractor',
			'qdrant'        => 'RAG connaissance',
			'brevo'         => 'E-mails transactionnels',
			'stripe'        => 'Paiements Guardian / Radar',
			'google_oauth'  => 'Connexion fiche Google',
		);
	}

	/**
	 * Derniers runs Proof Extractor (log fichier).
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public static function get_proof_extractor_flow_runs() {
		$path = defined( 'AVISION_PATH' ) ? AVISION_PATH . 'logs/guardian-creators.log' : '';
		if ( $path === '' || ! is_readable( $path ) ) {
			return array();
		}
		$lines = @file( $path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
		if ( ! is_array( $lines ) ) {
			return array();
		}
		$runs = array();
		$slice = array_slice( $lines, -40 );
		foreach ( array_reverse( $slice ) as $line ) {
			if ( false !== stripos( $line, 'proof' ) || false !== stripos( $line, 'Wave2' ) ) {
				$runs[] = array(
					'line'   => $line,
					'step_review' => stripos( $line, 'avis' ) !== false || stripos( $line, 'review' ) !== false ? 'ok' : 'idle',
					'step_openrouter' => stripos( $line, 'generat' ) !== false ? 'ok' : 'idle',
					'step_post' => 'dry_run',
					'step_log' => 'ok',
					'step_email' => 'idle',
				);
			}
			if ( count( $runs ) >= 5 ) {
				break;
			}
		}
		return $runs;
	}

	/**
	 * Profils pour sélecteurs admin.
	 *
	 * @param int $limit Limite.
	 * @return array<int, array<string, mixed>>
	 */
	public static function get_profiles_for_select( $limit = 20 ) {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! $wpdb ) {
			return array();
		}
		$table = $wpdb->prefix . 'avi_saas_profiles';
		$rows  = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, business_name, city, user_id FROM `{$table}` ORDER BY id DESC LIMIT %d",
				max( 1, (int) $limit )
			),
			ARRAY_A
		);
		return is_array( $rows ) ? $rows : array();
	}
}
