<?php
/**
 * Shared icomm license-server telemetry client (install registry + heartbeat).
 *
 * Rules (do not violate):
 * 1) License server purchase / activate / updates / install monitoring MUST keep working.
 * 2) Never fake success for activate / purchase / redeem (user actions always hit the network).
 * 3) Never run license/telemetry HTTP on front-end HTML page load (ALTERNATE_WP_CRON safe).
 * 4) Queue pings to WP-Cron; deliver for real so the license server sees installs + versions.
 * 5) Behind WAF / blocked egress: after 1–2 short install attempts, fail fast (cooldown).
 *    Do not stack long hangs on visitors or admin.
 * 6) Every activation: at least one real attempt (prefer two) with a few-second timeout,
 *    then enter cooldown mode if blocked.
 *
 * Public API: icomm_lic_client_telemetry_bootstrap().
 */
if (!defined('ABSPATH')) {
	exit;
}

if (!isset($GLOBALS['icomm_lic_telemetry_registry']) || !is_array($GLOBALS['icomm_lic_telemetry_registry'])) {
	$GLOBALS['icomm_lic_telemetry_registry'] = [];
}

if (function_exists('icomm_lic_client_telemetry_bootstrap')) {
	return;
}

if (defined('ICOMM_LIC_CLIENT_TELEMETRY_LOADED')) {
	return;
}
define('ICOMM_LIC_CLIENT_TELEMETRY_LOADED', true);

add_action('plugins_loaded', 'icomm_lic_client_telemetry_maybe_schedule_cron', 20);
add_action('icomm_lic_telemetry_daily', 'icomm_lic_client_telemetry_cron_ping');
add_action('icomm_lic_telemetry_send_one', 'icomm_lic_client_telemetry_deliver', 10, 2);
add_action('admin_init', 'icomm_lic_client_telemetry_maybe_admin_ping', 5);
add_filter('http_request_args', 'icomm_lic_client_http_request_args', 5, 2);
add_filter('pre_http_request', 'icomm_lic_client_pre_http_request_bg_gate', 5, 3);
add_action('http_api_debug', 'icomm_lic_client_http_api_debug_note', 10, 5);
add_filter('http_response', 'icomm_lic_client_http_response_note', 10, 3);

/**
 * @param array{
 * plugin:string,
 * version:string,
 * file:string,
 * rest_base?:callable|string|null,
 * license_state?:callable|null
 * } $config
 */
function icomm_lic_client_telemetry_bootstrap(array $config): void {
	$plugin = isset($config['plugin']) ? sanitize_key((string) $config['plugin']) : '';
	$version = isset($config['version']) ? (string) $config['version'] : '';
	$file = isset($config['file']) ? (string) $config['file'] : '';
	if ($plugin === '' || $file === '') {
		return;
	}

	foreach ($GLOBALS['icomm_lic_telemetry_registry'] as $i => $entry) {
		if (($entry['plugin'] ?? '') === $plugin) {
			$GLOBALS['icomm_lic_telemetry_registry'][$i]['version'] = $version;
			icomm_lic_client_telemetry_maybe_handle_version_change($plugin, $version);
			return;
		}
	}

	$GLOBALS['icomm_lic_telemetry_registry'][] = [
		'plugin' => $plugin,
		'version' => $version,
		'rest_base' => $config['rest_base'] ?? null,
		'license_state' => $config['license_state'] ?? null,
	];

	register_activation_hook($file, static function () use ($plugin): void {
		// Already in wp-admin on Activate: report now (short timeout), then retry if needed.
		icomm_lic_client_telemetry_queue_install_report($plugin);
	});

	add_action('init', static function () use ($plugin): void {
		icomm_lic_client_telemetry_maybe_init_ping($plugin);
	}, 99);

	// After plugin update: report new version via telemetry ping (server syncs activation version).
	icomm_lic_client_telemetry_maybe_handle_version_change($plugin, $version);
}

/**
 * When installed plugin version changes, force telemetry ping.
 * Does NOT clear per-plugin /check throttles (that caused N×timeout hangs behind WAF).
 */
function icomm_lic_client_telemetry_maybe_handle_version_change(string $plugin, string $version): void {
	$plugin = sanitize_key($plugin);
	$version = trim($version);
	if ($plugin === '' || $version === '') {
		return;
	}

	$opt = 'icomm_lic_client_loaded_ver_' . $plugin;
	$prev = (string) get_option($opt, '');
	if ($prev === $version) {
		return;
	}
	update_option($opt, $version, false);

	delete_option('icomm_lic_telemetry_last_sent_version_' . $plugin);
	delete_option('icomm_lic_telemetry_last_admin_ping_' . $plugin);
	delete_transient('icomm_lic_tel_q_' . md5($plugin . '|ping'));
	delete_transient('icomm_lic_tel_q_' . md5($plugin . '|install'));
	// Version report must reach the license server even after a temporary egress failure.
	delete_transient('icomm_lic_telemetry_retry_after');
	delete_transient('icomm_lic_bg_http_cooldown');

	icomm_lic_client_telemetry_schedule($plugin, 'ping', 1);
	if (function_exists('spawn_cron')) {
		spawn_cron();
	}
	do_action('icomm_lic_client_version_changed', $plugin, $version, $prev);
}

/**
 * Install / version reports must not be swallowed by WAF egress cooldown.
 */
function icomm_lic_client_telemetry_must_report(string $plugin, string $event = 'ping'): bool {
	$plugin = sanitize_key($plugin);
	$event = sanitize_key($event);
	if ($event === 'install') {
		return true;
	}
	if ($plugin !== '' && function_exists('icomm_lic_client_telemetry_needs_version_report')) {
		return icomm_lic_client_telemetry_needs_version_report($plugin);
	}
	return false;
}

/**
 * On activation (already in wp-admin): clear cooldown, then 1 real HTTP attempt now.
 * If it fails, schedule a second attempt in a few seconds; then cooldown if still blocked.
 * No need to log out / log in again. The plugins-list redirect also retries via admin_init.
 */
function icomm_lic_client_telemetry_queue_install_report(string $plugin): void {
	$plugin = sanitize_key($plugin);
	if ($plugin === '') {
		return;
	}
	delete_transient('icomm_lic_bg_http_cooldown');
	delete_transient('icomm_lic_telemetry_retry_after');
	delete_transient('icomm_lic_tel_q_' . md5($plugin . '|install'));
	update_option('icomm_lic_telemetry_install_attempts_' . $plugin, 0, false);
	update_option('icomm_lic_telemetry_pending_install_' . $plugin, time(), false);

	// Same Activate click in admin: try once immediately (few seconds max).
	if (is_admin() || (defined('WP_CLI') && WP_CLI)) {
		icomm_lic_client_telemetry_deliver($plugin, 'install');
		// deliver() already schedules a second try on failure; just nudge cron.
		if (!icomm_lic_client_telemetry_is_registered($plugin) && function_exists('spawn_cron')) {
			spawn_cron();
		}
		return;
	}

	// Non-admin activation path (rare): queue only.
	icomm_lic_client_telemetry_schedule($plugin, 'install', 2);
	if (function_exists('spawn_cron')) {
		spawn_cron();
	}
}

function icomm_lic_client_telemetry_install_attempts(string $plugin): int {
	return (int) get_option('icomm_lic_telemetry_install_attempts_' . sanitize_key($plugin), 0);
}

function icomm_lic_client_telemetry_bump_install_attempt(string $plugin): int {
	$plugin = sanitize_key($plugin);
	$n = icomm_lic_client_telemetry_install_attempts($plugin) + 1;
	update_option('icomm_lic_telemetry_install_attempts_' . $plugin, $n, false);
	return $n;
}

/**
 * Admin: run pending install reports (max one plugin per request).
 */
function icomm_lic_client_telemetry_process_pending_installs(): void {
	static $ran = false;
	if ($ran) {
		return;
	}
	$ran = true;
	if (!isset($GLOBALS['icomm_lic_telemetry_registry']) || !is_array($GLOBALS['icomm_lic_telemetry_registry'])) {
		return;
	}
	foreach ($GLOBALS['icomm_lic_telemetry_registry'] as $entry) {
		$plugin = isset($entry['plugin']) ? sanitize_key((string) $entry['plugin']) : '';
		if ($plugin === '') {
			continue;
		}
		$pending = (int) get_option('icomm_lic_telemetry_pending_install_' . $plugin, 0);
		if ($pending <= 0) {
			continue;
		}
		if ((time() - $pending) > DAY_IN_SECONDS) {
			delete_option('icomm_lic_telemetry_pending_install_' . $plugin);
			continue;
		}
		if (icomm_lic_client_telemetry_is_registered($plugin)) {
			delete_option('icomm_lic_telemetry_pending_install_' . $plugin);
			continue;
		}
		if (icomm_lic_client_telemetry_install_attempts($plugin) >= 2) {
			delete_option('icomm_lic_telemetry_pending_install_' . $plugin);
			continue;
		}
		delete_transient('icomm_lic_tel_q_' . md5($plugin . '|install'));
		icomm_lic_client_telemetry_deliver($plugin, 'install');
		// One install report per admin request (avoid stacking timeouts).
		return;
	}
}

/**
 * @deprecated Use icomm_lic_client_telemetry_bootstrap().
 * @param array<string,mixed> $config
 */
function icomm_lic_client_telemetry_register(string $plugin, string $version, string $file, array $config = []): void {
	icomm_lic_client_telemetry_bootstrap([
		'plugin' => $plugin,
		'version' => $version,
		'file' => $file,
		'rest_base' => $config['rest_base'] ?? null,
		'license_state' => $config['license_state'] ?? null,
	]);
}

/**
 * Shared gate: after egress failure, skip background /ping+/check (not activate).
 */
function icomm_lic_client_bg_http_allowed(): bool {
	return (int) get_transient('icomm_lic_bg_http_cooldown') !== 1;
}

function icomm_lic_client_bg_http_note_failure(): void {
	$ttl = (int) apply_filters('icomm_lic_client_bg_http_cooldown_ttl', 45 * MINUTE_IN_SECONDS);
	if ($ttl < 5 * MINUTE_IN_SECONDS) {
		$ttl = 5 * MINUTE_IN_SECONDS;
	}
	if ($ttl > 6 * HOUR_IN_SECONDS) {
		$ttl = 6 * HOUR_IN_SECONDS;
	}
	set_transient('icomm_lic_bg_http_cooldown', 1, $ttl);
	set_transient('icomm_lic_telemetry_retry_after', 1, $ttl);
}

function icomm_lic_client_bg_http_note_success(): void {
	delete_transient('icomm_lic_bg_http_cooldown');
	delete_transient('icomm_lic_telemetry_retry_after');
}

/**
 * @deprecated Prefer icomm_lic_client_bg_http_allowed().
 */
function icomm_lic_client_egress_is_cooling_down(): bool {
	return !icomm_lic_client_bg_http_allowed();
}

/**
 * @deprecated Prefer icomm_lic_client_bg_http_note_failure().
 */
function icomm_lic_client_egress_mark_failure(): void {
	icomm_lic_client_bg_http_note_failure();
}

/**
 * @deprecated Prefer icomm_lic_client_bg_http_note_success().
 */
function icomm_lic_client_egress_clear(): void {
	icomm_lic_client_bg_http_note_success();
}

function icomm_lic_client_telemetry_retry_waiting(): bool {
	return (int) get_transient('icomm_lic_telemetry_retry_after') === 1;
}

/**
 * Cap timeouts for background license traffic so WAF/firewall cannot hang the site for 8-20s.
 *
 * @param array<string,mixed> $args
 * @return array<string,mixed>
 */
function icomm_lic_client_http_request_args(array $args, $url): array {
	$url = (string) $url;
	if ($url === '' || stripos($url, 'license.icomm.systems') === false) {
		return $args;
	}
	// Cap all background license traffic (ping/check/updates). Activate/purchase keep their own timeout.
	if (preg_match('#/(activate|redeem-purchase|create-order|purchase)(/|\?|$)#i', $url)) {
		return $args;
	}
	$t = isset($args['timeout']) ? (float) $args['timeout'] : 5.0;
	// Install / forced reports: allow a few seconds. Routine bg traffic: 2s cap.
	if (!empty($GLOBALS['icomm_lic_telemetry_force_ping'])) {
		if ($t > 5) {
			$args['timeout'] = 5;
		} elseif ($t < 3) {
			$args['timeout'] = 4;
		}
	} elseif ($t > 2.5) {
		$args['timeout'] = 2;
	}
	$args['redirection'] = 0;
	return $args;
}

/**
 * After proven egress failure: fail-fast background /ping and /check (no network wait).
 * Activate / purchase / redeem always pass through.
 *
 * @param false|array|WP_Error $preempt
 * @param array<string,mixed> $args
 * @param string $url
 * @return false|array|WP_Error
 */
function icomm_lic_client_pre_http_request_bg_gate($preempt, $args, $url) {
	if ($preempt !== false) {
		return $preempt;
	}
	$url = (string) $url;
	if ($url === '' || stripos($url, 'license.icomm.systems') === false) {
		return false;
	}
	if (preg_match('#/(activate|redeem-purchase|create-order|purchase)(/|\?|$)#i', $url)) {
		return false;
	}
	if (!empty($GLOBALS['icomm_lic_manual_connectivity_probe'])) {
		return false;
	}
	if (!empty($GLOBALS['icomm_lic_telemetry_force_ping'])) {
		return false;
	}
	if (!icomm_lic_client_bg_http_allowed()) {
		// Fail-fast any background license call (ping/check/updates/metadata) during cooldown.
		return new WP_Error('icomm_lic_bg_cooldown', 'License server egress cooling down (WAF/firewall).');
	}
	return false;
}

/**
 * Mark egress failure when /ping or /check times out (WP_Error path).
 *
 * @param mixed $response
 * @param string $context
 * @param string $class
 * @param array<string,mixed> $parsed_args
 * @param string $url
 */
function icomm_lic_client_http_api_debug_note($response, $context, $class, $parsed_args, $url): void {
	if ((string) $context !== 'response') {
		return;
	}
	$url = (string) $url;
	if ($url === '' || stripos($url, 'license.icomm.systems') === false) {
		return;
	}
	if (preg_match('#/(activate|redeem-purchase|create-order|purchase)(/|\?|$)#i', $url)) {
		return;
	}
	if (is_wp_error($response)) {
		$code = $response->get_error_code();
		if ($code !== 'icomm_lic_bg_cooldown') {
			icomm_lic_client_bg_http_note_failure();
		}
	}
}

/**
 * @param array|WP_Error $response
 * @param array<string,mixed> $args
 * @param string $url
 * @return array|WP_Error
 */
function icomm_lic_client_http_response_note($response, $args, $url) {
	$url = (string) $url;
	if ($url === '' || stripos($url, 'license.icomm.systems') === false) {
		return $response;
	}
	if (preg_match('#/(activate|redeem-purchase|create-order|purchase)(/|\?|$)#i', $url)) {
		return $response;
	}
	if (is_wp_error($response)) {
		return $response;
	}
	$code = (int) wp_remote_retrieve_response_code($response);
	if ($code === 200) {
		icomm_lic_client_bg_http_note_success();
	} elseif ($code === 0 || $code >= 500) {
		icomm_lic_client_bg_http_note_failure();
	}
	return $response;
}

/**
 * True when blocking license HTTP is allowed on this request (not a public HTML hit).
 */
function icomm_lic_client_telemetry_may_block_http_now(): bool {
	if (defined('WP_CLI') && WP_CLI) {
		return true;
	}
	// Forced install/version report (admin_init or cron): allow short blocking HTTP.
	if (!empty($GLOBALS['icomm_lic_telemetry_force_ping'])) {
		return true;
	}
	if (!wp_doing_cron()) {
		return false;
	}
	$uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
	if (strpos($uri, 'wp-cron.php') !== false) {
		return true;
	}
	// ALTERNATE_WP_CRON runs cron on the same request as the visitor/admin page.
	if (!is_admin()) {
		return false;
	}
	return true;
}

/**
 * @return array{ok:bool,ts:int,http_code:int,error:string,skipped:bool,site_url:string,endpoint:string}
 */
function icomm_lic_client_telemetry_get_ping_status(string $plugin): array {
	$plugin = sanitize_key($plugin);
	$stored = get_option('icomm_lic_telemetry_ping_status_' . $plugin, []);
	if (!is_array($stored)) {
		$stored = [];
	}

	return [
		'ok' => !empty($stored['ok']),
		'ts' => isset($stored['ts']) ? (int) $stored['ts'] : 0,
		'http_code' => isset($stored['http_code']) ? (int) $stored['http_code'] : 0,
		'error' => isset($stored['error']) ? (string) $stored['error'] : '',
		'skipped' => !empty($stored['skipped']),
		'site_url' => isset($stored['site_url']) ? (string) $stored['site_url'] : '',
		'endpoint' => isset($stored['endpoint']) ? (string) $stored['endpoint'] : '',
	];
}

function icomm_lic_client_telemetry_is_registered(string $plugin): bool {
	if (get_option('icomm_lic_telemetry_registered_' . sanitize_key($plugin), '') !== '1') {
		return false;
	}
	$status = icomm_lic_client_telemetry_get_ping_status($plugin);
	return (int) $status['http_code'] === 200 && !empty($status['ok']) && empty($status['skipped']);
}

/**
 * @param array{ok:bool,http_code?:int,error?:string,skipped?:bool,site_url?:string,endpoint?:string} $result
 */
function icomm_lic_client_telemetry_record_ping_result(string $plugin, array $result): void {
	$plugin = sanitize_key($plugin);
	if ($plugin === '') {
		return;
	}

	$payload = [
		'ok' => !empty($result['ok']),
		'ts' => time(),
		'http_code' => isset($result['http_code']) ? (int) $result['http_code'] : 0,
		'error' => isset($result['error']) ? (string) $result['error'] : '',
		'skipped' => !empty($result['skipped']),
		'site_url' => isset($result['site_url']) ? (string) $result['site_url'] : '',
		'endpoint' => isset($result['endpoint']) ? (string) $result['endpoint'] : '',
	];
	update_option('icomm_lic_telemetry_ping_status_' . $plugin, $payload, false);

	if (!empty($payload['ok']) && (int) $payload['http_code'] === 200 && empty($payload['skipped'])) {
		update_option('icomm_lic_telemetry_registered_' . $plugin, '1', false);
		$ver = icomm_lic_client_telemetry_version_for($plugin);
		if ($ver !== '') {
			update_option('icomm_lic_telemetry_last_sent_version_' . $plugin, $ver, false);
		}
	}
}

function icomm_lic_client_telemetry_needs_version_report(string $plugin): bool {
	$current = icomm_lic_client_telemetry_version_for($plugin);
	if ($current === '') {
		return false;
	}
	$sent = (string) get_option('icomm_lic_telemetry_last_sent_version_' . sanitize_key($plugin), '');
	return $sent !== $current;
}

function icomm_lic_client_telemetry_maybe_init_ping(string $plugin): void {
	$plugin = sanitize_key($plugin);
	if ($plugin === '') {
		return;
	}
	if (!icomm_lic_client_telemetry_is_registered($plugin) || icomm_lic_client_telemetry_needs_version_report($plugin)) {
		icomm_lic_client_telemetry_schedule($plugin, 'ping');
		return;
	}
	$opt = 'icomm_lic_telemetry_last_init_ping_' . $plugin;
	$last = (int) get_option($opt, 0);
	$now = time();
	if ($last > 0 && ($now - $last) < HOUR_IN_SECONDS) {
		return;
	}
	if (icomm_lic_client_telemetry_schedule($plugin, 'ping')) {
		update_option($opt, $now, false);
	}
}

function icomm_lic_client_telemetry_default_rest_base(): string {
	return 'https://license.icomm.systems/wp-json/icomm-lic/v1';
}

function icomm_lic_client_telemetry_rest_base_for(string $plugin): string {
	foreach ($GLOBALS['icomm_lic_telemetry_registry'] as $entry) {
		if (($entry['plugin'] ?? '') !== $plugin) {
			continue;
		}
		if (isset($entry['rest_base']) && is_callable($entry['rest_base'])) {
			$u = (string) call_user_func($entry['rest_base']);
			if ($u !== '') {
				return rtrim($u, '/');
			}
		}
	}
	$default = icomm_lic_client_telemetry_default_rest_base();
	return (string) apply_filters('icomm_lic_client_telemetry_rest_base', $default, $plugin);
}

function icomm_lic_client_telemetry_install_id(string $plugin): string {
	$opt = 'icomm_lic_tid_' . sanitize_key($plugin);
	$id = get_option($opt, '');
	if (is_string($id) && strlen($id) >= 16) {
		return $id;
	}
	if (function_exists('wp_generate_uuid4')) {
		$id = wp_generate_uuid4();
	} else {
		$id = wp_generate_password(32, false, false);
	}
	update_option($opt, $id, false);
	return $id;
}

function icomm_lic_client_telemetry_resolve_license_state(string $plugin): string {
	static $busy = false;
	if ($busy) {
		return 'unknown';
	}
	$busy = true;
	$out = 'unknown';
	foreach ($GLOBALS['icomm_lic_telemetry_registry'] as $entry) {
		if (($entry['plugin'] ?? '') !== $plugin) {
			continue;
		}
		if (isset($entry['license_state']) && is_callable($entry['license_state'])) {
			$st = sanitize_key((string) call_user_func($entry['license_state']));
			if (in_array($st, ['no_license', 'trial', 'licensed', 'expired', 'unknown'], true)) {
				$out = $st;
			}
		}
		break;
	}
	$busy = false;
	return $out;
}

function icomm_lic_client_telemetry_version_for(string $plugin): string {
	foreach ($GLOBALS['icomm_lic_telemetry_registry'] as $entry) {
		if (($entry['plugin'] ?? '') === $plugin) {
			return (string) ($entry['version'] ?? '');
		}
	}
	return '';
}

/**
 * @return bool True when queued or already queued recently.
 */
function icomm_lic_client_telemetry_send(string $plugin, string $event = 'ping'): bool {
	return icomm_lic_client_telemetry_schedule($plugin, $event);
}

/**
 * Queue delivery via WP-Cron. Never performs HTTP on the web request.
 *
 * @param int $delay_seconds Delay before cron runs (install uses 1).
 */
function icomm_lic_client_telemetry_schedule(string $plugin, string $event = 'ping', int $delay_seconds = 5): bool {
	$plugin = sanitize_key($plugin);
	$event = sanitize_key($event);
	if ($plugin === '') {
		return false;
	}
	if (!(bool) apply_filters('icomm_lic_client_telemetry_enabled', true, $plugin)) {
		icomm_lic_client_telemetry_record_ping_result($plugin, [
			'ok' => false,
			'error' => 'telemetry_disabled',
		]);
		return false;
	}

	// After a failed ping, space retries. Install + version reports always queue.
	if (!icomm_lic_client_telemetry_must_report($plugin, $event) && icomm_lic_client_telemetry_retry_waiting()) {
		return false;
	}

	$key = 'icomm_lic_tel_q_' . md5($plugin . '|' . $event);
	if (get_transient($key)) {
		return true;
	}
	set_transient($key, 1, 2 * MINUTE_IN_SECONDS);

	if ($delay_seconds < 1) {
		$delay_seconds = 1;
	}
	if ($delay_seconds > 60) {
		$delay_seconds = 60;
	}

	$hook = 'icomm_lic_telemetry_send_one';
	$args = [$plugin, $event !== '' ? $event : 'ping'];
	if (!wp_next_scheduled($hook, $args)) {
		wp_schedule_single_event(time() + $delay_seconds, $hook, $args);
	}
	if (function_exists('spawn_cron') && (is_admin() || icomm_lic_client_telemetry_must_report($plugin, $event))) {
		spawn_cron();
	}

	return true;
}

/**
 * Cron worker: real POST so the license server records the install.
 *
 * @param mixed $plugin
 * @param mixed $event
 */
function icomm_lic_client_telemetry_deliver($plugin, $event = 'ping'): void {
	$plugin = sanitize_key((string) $plugin);
	$event = sanitize_key((string) $event);
	if ($plugin === '') {
		return;
	}
	if (!(bool) apply_filters('icomm_lic_client_telemetry_enabled', true, $plugin)) {
		return;
	}

	$event = $event !== '' ? $event : 'ping';
	$must = icomm_lic_client_telemetry_must_report($plugin, $event);

	// Allow short blocking HTTP for install/version reports before the gate check.
	if ($must) {
		$GLOBALS['icomm_lic_telemetry_force_ping'] = 1;
	}

	// Never hang public pages when ALTERNATE_WP_CRON fires on the same hit.
	// Exception: version/install reports may run in admin (or forced) context.
	if (!icomm_lic_client_telemetry_may_block_http_now() && !($must && is_admin())) {
		if ($must) {
			unset($GLOBALS['icomm_lic_telemetry_force_ping']);
		}
		$delay = is_admin() ? 30 : 120;
		icomm_lic_client_telemetry_schedule($plugin, $event, $delay);
		return;
	}

	if (!$must && icomm_lic_client_telemetry_retry_waiting()) {
		return;
	}

	$attempt = 0;
	if ($event === 'install') {
		if (icomm_lic_client_telemetry_install_attempts($plugin) >= 2) {
			delete_option('icomm_lic_telemetry_pending_install_' . $plugin);
			return;
		}
		$attempt = icomm_lic_client_telemetry_bump_install_attempt($plugin);
	}

	$url = icomm_lic_client_telemetry_rest_base_for($plugin) . '/ping';
	$site_url = home_url('/');
	$payload = [
		'install_id' => icomm_lic_client_telemetry_install_id($plugin),
		'site_url' => $site_url,
		'plugin' => $plugin,
		'plugin_version' => icomm_lic_client_telemetry_version_for($plugin),
		'wp_version' => get_bloginfo('version'),
		'license_state' => icomm_lic_client_telemetry_resolve_license_state($plugin),
		'event' => $event,
	];

	$default_timeout = ($event === 'install' || $must) ? 4 : 2;
	$timeout = (int) apply_filters('icomm_lic_client_telemetry_timeout', $default_timeout, $plugin, $event);
	if ($event === 'install' || $must) {
		if ($timeout < 3) {
			$timeout = 3;
		}
		if ($timeout > 5) {
			$timeout = 5;
		}
	} else {
		if ($timeout < 1) {
			$timeout = 1;
		}
		if ($timeout > 3) {
			$timeout = 3;
		}
	}

	if ($must) {
		$GLOBALS['icomm_lic_telemetry_force_ping'] = 1;
	}
	$res = wp_remote_post($url, [
		'timeout' => $timeout,
		'redirection' => 0,
		'blocking' => true,
		'headers' => ['Content-Type' => 'application/json; charset=UTF-8'],
		'body' => wp_json_encode($payload),
	]);
	if ($must) {
		unset($GLOBALS['icomm_lic_telemetry_force_ping']);
	}

	if (is_wp_error($res)) {
		icomm_lic_client_telemetry_record_ping_result($plugin, [
			'ok' => false,
			'error' => $res->get_error_message(),
			'site_url' => $site_url,
			'endpoint' => $url,
		]);
		if ($event === 'install' && $attempt < 2) {
			// Second try in a few seconds; do not enter cooldown yet.
			delete_transient('icomm_lic_tel_q_' . md5($plugin . '|install'));
			icomm_lic_client_telemetry_schedule($plugin, 'install', 5);
			if (function_exists('spawn_cron')) {
				spawn_cron();
			}
			return;
		}
		if ($event === 'install') {
			delete_option('icomm_lic_telemetry_pending_install_' . $plugin);
		}
		icomm_lic_client_bg_http_note_failure();
		return;
	}

	$code = (int) wp_remote_retrieve_response_code($res);
	$raw = (string) wp_remote_retrieve_body($res);
	$data = json_decode($raw, true);
	$skipped = is_array($data) && !empty($data['skipped']);
	$ok = $code === 200 && is_array($data) && !empty($data['ok']) && !$skipped;

	$error = '';
	if (!$ok) {
		if ($skipped) {
			$reason = is_array($data) && isset($data['reason']) ? (string) $data['reason'] : 'domain_skipped';
			$error = 'skipped:' . $reason;
		} elseif (is_array($data) && !empty($data['error'])) {
			$error = (string) $data['error'];
		} elseif ($raw !== '') {
			$error = wp_strip_all_tags(substr($raw, 0, 240));
		} else {
			$error = 'http_' . $code;
		}
	}

	icomm_lic_client_telemetry_record_ping_result($plugin, [
		'ok' => $ok,
		'http_code' => $code,
		'error' => $error,
		'skipped' => $skipped,
		'site_url' => $site_url,
		'endpoint' => $url,
	]);

	if ($ok) {
		if ($event === 'install') {
			delete_option('icomm_lic_telemetry_pending_install_' . $plugin);
		}
		icomm_lic_client_bg_http_note_success();
		return;
	}

	// Failed HTTP response.
	if ($event === 'install' && $attempt < 2 && ($code === 0 || $code >= 500 || $code === 403 || $code === 429)) {
		delete_transient('icomm_lic_tel_q_' . md5($plugin . '|install'));
		icomm_lic_client_telemetry_schedule($plugin, 'install', 5);
		if (function_exists('spawn_cron')) {
			spawn_cron();
		}
		return;
	}
	if ($event === 'install') {
		delete_option('icomm_lic_telemetry_pending_install_' . $plugin);
	}
	if ($code === 0 || $code >= 500 || $code === 403 || $code === 429) {
		icomm_lic_client_bg_http_note_failure();
	}
}

function icomm_lic_client_telemetry_maybe_schedule_cron(): void {
	if (get_option('icomm_lic_telemetry_cron_v4') === '1') {
		return;
	}
	if (!wp_next_scheduled('icomm_lic_telemetry_daily')) {
		wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', 'icomm_lic_telemetry_daily');
	}
	update_option('icomm_lic_telemetry_cron_v4', '1', false);
}

function icomm_lic_client_telemetry_cron_ping(): void {
	foreach ($GLOBALS['icomm_lic_telemetry_registry'] as $entry) {
		$plugin = isset($entry['plugin']) ? (string) $entry['plugin'] : '';
		if ($plugin !== '') {
			icomm_lic_client_telemetry_schedule($plugin, 'ping');
		}
	}
}

function icomm_lic_client_telemetry_maybe_admin_ping(): void {
	if (!is_admin() || !current_user_can('manage_options')) {
		return;
	}
	// Pending installs: 1–2 real attempts (few seconds), then cooldown if blocked.
	icomm_lic_client_telemetry_process_pending_installs();

	if (!icomm_lic_client_bg_http_allowed()) {
		return;
	}
	$now = time();
	foreach ($GLOBALS['icomm_lic_telemetry_registry'] as $entry) {
		$plugin = isset($entry['plugin']) ? sanitize_key((string) $entry['plugin']) : '';
		if ($plugin === '') {
			continue;
		}
		if (!icomm_lic_client_telemetry_is_registered($plugin) || icomm_lic_client_telemetry_needs_version_report($plugin)) {
			icomm_lic_client_telemetry_schedule($plugin, 'ping');
			continue;
		}
		$last = (int) get_option('icomm_lic_telemetry_last_admin_ping_' . $plugin, 0);
		if ($last > 0 && ($now - $last) < DAY_IN_SECONDS) {
			continue;
		}
		if (icomm_lic_client_telemetry_schedule($plugin, 'ping')) {
			update_option('icomm_lic_telemetry_last_admin_ping_' . $plugin, $now, false);
		}
	}
}

/**
 * Cached license-server reachability for admin UI. Never performs HTTP.
 *
 * @return array{state:string,title:string,message:string,class:string,ts:int,error:string}
 */
function icomm_lic_client_connectivity_status(string $plugin = ''): array {
	$plugin = sanitize_key($plugin);
	$allowed = function_exists('icomm_lic_client_bg_http_allowed') ? icomm_lic_client_bg_http_allowed() : true;
	$ping = [
		'ok' => false,
		'ts' => 0,
		'http_code' => 0,
		'error' => '',
		'skipped' => false,
	];
	if ($plugin !== '' && function_exists('icomm_lic_client_telemetry_get_ping_status')) {
		$ping = array_merge($ping, icomm_lic_client_telemetry_get_ping_status($plugin));
	}

	$ts = isset($ping['ts']) ? (int) $ping['ts'] : 0;
	$error = isset($ping['error']) ? (string) $ping['error'] : '';
	$http = isset($ping['http_code']) ? (int) $ping['http_code'] : 0;
	$ok = !empty($ping['ok']) && $http === 200 && empty($ping['skipped']);

	if (!$allowed) {
		return [
			'state' => 'blocked',
			'title' => 'אין גישה לשרת הרישיונות',
			'message' => 'האחסון כנראה חוסם את license.icomm.systems (WAF/Firewall). הרישיון המקומי ממשיך לעבוד; הפעלה, בדיקות רקע ועדכונים אוטומטיים לא יעבדו עד שיפתחו יציאה לדומיין זה.',
			'class' => 'notice-warning',
			'ts' => $ts,
			'error' => $error !== '' ? $error : 'bg_http_cooldown',
		];
	}

	if ($ok) {
		return [
			'state' => 'ok',
			'title' => 'יש גישה לשרת הרישיונות',
			'message' => 'החיבור ל-license.icomm.systems תקין. הפעלה, דיווח גרסה ועדכונים יכולים לעבוד ברקע.',
			'class' => 'notice-success',
			'ts' => $ts,
			'error' => '',
		];
	}

	if ($ts > 0 && ($error !== '' || $http >= 400 || $http === 0)) {
		$detail = $error !== '' ? $error : ('HTTP ' . $http);
		return [
			'state' => 'fail',
			'title' => 'בעיית חיבור לשרת הרישיונות',
			'message' => 'הניסיון האחרון נכשל (' . $detail . '). אם ההפעלה נכשלת שוב ושוב, בדקו שאין חסימה ל-license.icomm.systems.',
			'class' => 'notice-warning',
			'ts' => $ts,
			'error' => $error,
		];
	}

	return [
		'state' => 'unknown',
		'title' => 'חיבור לשרת הרישיונות טרם נבדק',
		'message' => 'עדיין אין תוצאת ping שמורה. אחרי הפעלת רישיון או Cron ברקע יופיע כאן מצב החיבור. אין בדיקת רשת בטעינת העמוד.',
		'class' => 'notice-info',
		'ts' => $ts,
		'error' => '',
	];
}

/**
 * Mask a license key for admin display: asterisks + last N characters.
 */
function icomm_lic_client_mask_license_key(string $key, int $visible_tail = 6): string {
	$key = trim($key);
	if ($key === '') {
		return '';
	}
	$visible_tail = max(4, min(12, $visible_tail));
	$len = strlen($key);
	if ($len <= $visible_tail) {
		return str_repeat('*', max(6, $len));
	}
	return str_repeat('*', $len - $visible_tail) . substr($key, -$visible_tail);
}

/**
 * True when posted value is the masked display of $full_key (user did not change the key).
 */
function icomm_lic_client_is_masked_license_key(string $posted, string $full_key, int $visible_tail = 6): bool {
	$posted = trim($posted);
	$full_key = trim($full_key);
	if ($posted === '' || $full_key === '') {
		return false;
	}
	if ($posted === $full_key) {
		return false;
	}
	if ($posted === icomm_lic_client_mask_license_key($full_key, $visible_tail)) {
		return true;
	}
	$tail = substr($full_key, -$visible_tail);
	if (strlen($posted) < $visible_tail || substr($posted, -$visible_tail) !== $tail) {
		return false;
	}
	$prefix = substr($posted, 0, -$visible_tail);
	return $prefix !== '' && (bool) preg_match('/^[\*•·]+$/u', $prefix);
}

/**
 * Echo admin notice for license-server connectivity (cached only, no HTTP).
 */
function icomm_lic_client_render_connectivity_notice(string $plugin = ''): void {
	if (!is_admin()) {
		return;
	}
	$status = icomm_lic_client_connectivity_status($plugin);
	$title = (string) $status['title'];
	$message = (string) $status['message'];
	$class = (string) $status['class'];
	$ts = (int) $status['ts'];
	$state = (string) $status['state'];
	$border = $state === 'ok' ? '#00a32a' : ($state === 'blocked' || $state === 'fail' ? '#dba617' : '#72aee6');
	?>
	<div class="notice <?php echo esc_attr($class); ?> inline icomm-lic-connectivity-notice" dir="rtl" style="max-width:820px;margin:12px 0;padding:12px 14px;border-right:4px solid <?php echo esc_attr($border); ?>;">
		<p style="margin:0 0 6px;font-size:14px;"><strong><?php echo esc_html($title); ?></strong></p>
		<p style="margin:0;"><?php echo esc_html($message); ?></p>
		<?php if ($ts > 0) : ?>
			<p class="description" style="margin:6px 0 0;"><?php echo esc_html(sprintf('בדיקה אחרונה (שמורה): %s', wp_date('Y-m-d H:i', $ts))); ?></p>
		<?php endif; ?>
	</div>
	<?php
}
