<?php
// AirMax monitor library - stable async rebuild
if (!function_exists('airmax_seed_defaults')) {
function airmax_seed_defaults($db = null){ return true; }
}

if (!function_exists('airmax_vendor_options')) {
function airmax_vendor_options(){
    return ['auto'=>'Auto','ubiquiti'=>'Ubiquiti','mimosa'=>'Mimosa','mikrotik'=>'MikroTik','generic'=>'Generic'];
}
}

if (!function_exists('airmax_cache_dir')) {
function airmax_cache_dir(): string {
    $dir = __DIR__ . '/cache/airmax';
    if (!is_dir($dir)) { @mkdir($dir, 0777, true); }
    return $dir;
}
function airmax_result_cache_path(array $d): string {
    $id = (int)($d['id'] ?? 0);
    $host = preg_replace('/[^a-zA-Z0-9_.-]+/', '_', (string)($d['host'] ?? 'host'));
    return airmax_cache_dir() . '/result_' . $id . '_' . $host . '.json';
}
function airmax_route_cache_path(array $d): string {
    $id = (int)($d['id'] ?? 0);
    $host = preg_replace('/[^a-zA-Z0-9_.-]+/', '_', (string)($d['host'] ?? 'host'));
    return airmax_cache_dir() . '/route_' . $id . '_' . $host . '.json';
}
function airmax_live_cache_path(): string {
    return airmax_cache_dir() . '/live_cache.json';
}
function airmax_live_idx_path(): string {
    return airmax_cache_dir() . '/live_idx.txt';
}
}

if (!function_exists('airmax_invalidate_noc_card_cache')) {
function airmax_invalidate_noc_card_cache(): void {
    // Dashboard card caches are derived views.  Any successful/manual device probe
    // must make the next dashboard read re-hydrate from the authoritative device
    // result cache instead of keeping stale sender/receiver states.
    $base = __DIR__ . '/cache';
    foreach (['noc_receivers_card.json','noc_sectors_card.json'] as $file) {
        @unlink($base . '/' . $file);
    }
}
}

if (!function_exists('airmax_dashboard_metric_value')) {
function airmax_dashboard_metric_value(array $metrics, array $old, string $key, bool $float = true) {
    if (array_key_exists($key, $metrics) && $metrics[$key] !== '' && $metrics[$key] !== null) {
        return $float ? (float)$metrics[$key] : $metrics[$key];
    }
    return $old[$key] ?? null;
}
}

if (!function_exists('airmax_dashboard_item_from_cache')) {
function airmax_dashboard_item_from_cache(array $device, array $old = []): array {
    $id = (int)($device['id'] ?? 0);
    $item = [
        'id'=>$id,
        'host'=>(string)($device['host'] ?? ''),
        'name'=>(string)($device['name'] ?? '—'),
        'model'=>(string)(($old['model'] ?? '—') ?: '—'),
        'cpu'=>$old['cpu'] ?? null,
        'memory'=>$old['memory'] ?? null,
        'quality'=>$old['quality'] ?? null,
        'capacity'=>$old['capacity'] ?? null,
        'signal'=>$old['signal'] ?? null,
        'ccq'=>$old['ccq'] ?? null,
        'clients'=>$old['clients'] ?? null,
        'stations'=>$old['stations'] ?? null,
        'state'=>(string)(($old['state'] ?? 'WARN') ?: 'WARN'),
        'uptime'=>(string)(($old['uptime'] ?? '') ?: ''),
    ];
    $cache = function_exists('airmax_json_read') ? airmax_json_read(airmax_result_cache_path($device), null) : null;
    if (!is_array($cache) || !isset($cache['out']['probe']) || !is_array($cache['out']['probe'])) {
        return $item;
    }
    $p = $cache['out']['probe'];
    $metrics = is_array($p['metrics'] ?? null) ? $p['metrics'] : [];
    $bridge = is_array($p['bridge'] ?? null) ? $p['bridge'] : (is_array($cache['out']['bridge'] ?? null) ? $cache['out']['bridge'] : []);
    $clients = null;
    if (array_key_exists('clients', $metrics) && $metrics['clients'] !== '' && $metrics['clients'] !== null) {
        $clients = (int)$metrics['clients'];
    } elseif (array_key_exists('bridge_count', $bridge) && $bridge['bridge_count'] !== '' && $bridge['bridge_count'] !== null) {
        $clients = (int)$bridge['bridge_count'];
    }
    $route = is_array($metrics['_route'] ?? null) ? $metrics['_route'] : [];
    return [
        'id'=>$id,
        'host'=>(string)($device['host'] ?? ''),
        'name'=>(string)($device['name'] ?? '—'),
        'model'=>(string)(($metrics['model'] ?? ($old['model'] ?? '—')) ?: '—'),
        'cpu'=>airmax_dashboard_metric_value($metrics, $old, 'cpu'),
        'memory'=>airmax_dashboard_metric_value($metrics, $old, 'memory'),
        'quality'=>airmax_dashboard_metric_value($metrics, $old, 'quality'),
        'capacity'=>airmax_dashboard_metric_value($metrics, $old, 'capacity'),
        'signal'=>airmax_dashboard_metric_value($metrics, $old, 'signal'),
        'ccq'=>airmax_dashboard_metric_value($metrics, $old, 'ccq'),
        'clients'=>$clients !== null ? $clients : (($old['clients'] ?? null) !== null ? (int)$old['clients'] : null),
        'stations'=>$clients !== null ? $clients : (($old['stations'] ?? null) !== null ? (int)$old['stations'] : null),
        'uptime'=>(string)(($metrics['uptime'] ?? ($old['uptime'] ?? '')) ?: ''),
        'state'=>(string)(($p['state'] ?? ($old['state'] ?? 'DOWN')) ?: 'DOWN'),
        'vendor'=>(string)($metrics['vendor'] ?? ($route['vendor'] ?? ($device['preferred_vendor'] ?? ''))),
        'mode'=>(string)($route['mode'] ?? ''),
        'client_count_method'=>(string)($bridge['client_count_method'] ?? ($device['client_count_method'] ?? '')),
        'last_probe_ts'=>(int)($cache['ts'] ?? 0),
    ];
}
}

if (!function_exists('airmax_json_read')) {
function airmax_json_read(string $path, $default=null) {
    if (!is_file($path)) return $default;
    $raw = @file_get_contents($path);
    $data = json_decode((string)$raw, true);
    return is_array($data) ? $data : $default;
}
function airmax_json_write(string $path, array $data): void {
    @file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), LOCK_EX);
}
}

if (!function_exists('airmax_route_cache_set')) {
function airmax_route_cache_set(array $d, array $route): void {
    airmax_json_write(airmax_route_cache_path($d), $route + ['ts'=>time()]);
}
function airmax_route_cache_get(array $d): ?array {
    $r = airmax_json_read(airmax_route_cache_path($d), null);
    return is_array($r) ? $r : null;
}
}

if (!function_exists('airmax_ensure_probe_schema')) {
function airmax_ensure_probe_schema($db = null){
    if (!($db instanceof mysqli)) return true;
    @$db->query("CREATE TABLE IF NOT EXISTS airmax_devices (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(160) NOT NULL DEFAULT '',
        host VARCHAR(120) NOT NULL DEFAULT '',
        username VARCHAR(120) NULL,
        password VARCHAR(255) NULL,
        notes TEXT NULL,
        sort_order INT NOT NULL DEFAULT 0,
        is_enabled TINYINT(1) NOT NULL DEFAULT 1,
        created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
        INDEX (host),
        INDEX (is_enabled)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
    @$db->query("CREATE TABLE IF NOT EXISTS airmax_live_cache (
        device_id INT NOT NULL PRIMARY KEY,
        payload LONGTEXT NULL,
        updated_at DATETIME NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
    @$db->query("CREATE TABLE IF NOT EXISTS device_activity_log (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        device_id INT NULL,
        device_name VARCHAR(160) NULL,
        host VARCHAR(120) NULL,
        device_type VARCHAR(20) NULL,
        event_source VARCHAR(64) NOT NULL DEFAULT 'airmax_probe',
        event_key VARCHAR(80) NOT NULL DEFAULT 'DEVICE_PROBE',
        status VARCHAR(30) NOT NULL DEFAULT '',
        raw_event TEXT NULL,
        arabic_summary TEXT NULL,
        details_json LONGTEXT NULL,
        event_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY idx_device_time (device_id, event_at),
        KEY idx_event_time (event_at),
        KEY idx_status (status)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    try {
        @$db->query("ALTER TABLE device_activity_log MODIFY COLUMN raw_event MEDIUMTEXT NULL");
        @$db->query("ALTER TABLE device_activity_log MODIFY COLUMN details_json LONGTEXT NULL");
    } catch (Throwable $e) {}
    $cols = [
        "ALTER TABLE airmax_devices ADD COLUMN device_type VARCHAR(20) NOT NULL DEFAULT 'access'",
        "ALTER TABLE airmax_devices ADD COLUMN preferred_vendor VARCHAR(40) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN snmp_community VARCHAR(80) NULL DEFAULT 'public'",
        "ALTER TABLE airmax_devices ADD COLUMN detected_protocol VARCHAR(10) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN detected_port INT NULL",
        "ALTER TABLE airmax_devices ADD COLUMN detected_vendor VARCHAR(50) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN detected_os VARCHAR(50) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN detected_mode VARCHAR(50) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN detected_route TEXT NULL",
        "ALTER TABLE airmax_devices ADD COLUMN login_path VARCHAR(255) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN status_path VARCHAR(255) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN bridge_path VARCHAR(255) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN sector_id INT NULL",
        "ALTER TABLE airmax_devices ADD COLUMN parent_device_id INT NULL",
        "ALTER TABLE airmax_devices ADD COLUMN client_count_method VARCHAR(100) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN probe_status VARCHAR(50) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN probe_progress VARCHAR(10) NULL",
        "ALTER TABLE airmax_devices ADD COLUMN probe_message TEXT NULL",
        "ALTER TABLE airmax_devices ADD COLUMN probe_last_error TEXT NULL",
        "ALTER TABLE airmax_devices ADD COLUMN probe_meta_json LONGTEXT NULL",
        "ALTER TABLE airmax_devices ADD COLUMN probe_raw_capture_json LONGTEXT NULL",
        "ALTER TABLE airmax_devices ADD COLUMN probe_locked TINYINT(1) NOT NULL DEFAULT 0",
        "ALTER TABLE airmax_devices ADD COLUMN probe_last_success_at DATETIME NULL",
        "ALTER TABLE airmax_devices ADD COLUMN probe_last_error_at DATETIME NULL"
    ];
    foreach ($cols as $sql) { try { @$db->query($sql); } catch (Throwable $e) {} }

    // Older installations may already have device_type as ENUM or a too-short field.
    // Saving the new canonical values (access/station) then fails with:
    // Data truncated for column 'device_type'.  Normalize the column type safely.
    try {
        $rs = @$db->query("SHOW COLUMNS FROM airmax_devices LIKE 'device_type'");
        $row = ($rs && $rs->num_rows) ? $rs->fetch_assoc() : null;
        $type = strtolower((string)($row['Type'] ?? ''));
        if ($type === '' || strpos($type, 'varchar(20)') === false) {
            @$db->query("ALTER TABLE airmax_devices MODIFY COLUMN device_type VARCHAR(20) NOT NULL DEFAULT 'access'");
        }
    } catch (Throwable $e) {}
    try {
        $rs = @$db->query("SHOW COLUMNS FROM airmax_devices LIKE 'parent_device_id'");
        $row = ($rs && $rs->num_rows) ? $rs->fetch_assoc() : null;
        $type = strtolower((string)($row['Type'] ?? ''));
        if ($type === '' || strpos($type, 'int') === false) {
            @$db->query("ALTER TABLE airmax_devices MODIFY COLUMN parent_device_id INT NULL");
        }
    } catch (Throwable $e) {}
    try {
        $rs = @$db->query("SHOW COLUMNS FROM airmax_devices LIKE 'preferred_vendor'");
        $row = ($rs && $rs->num_rows) ? $rs->fetch_assoc() : null;
        $type = strtolower((string)($row['Type'] ?? ''));
        if ($type === '' || strpos($type, 'varchar(40)') === false) {
            @$db->query("ALTER TABLE airmax_devices MODIFY COLUMN preferred_vendor VARCHAR(40) NULL DEFAULT 'auto'");
        }
    } catch (Throwable $e) {}

    // Canonical values used by the matching engine: access = ignored, station = LAN0 matching.
    try {
        @$db->query("UPDATE airmax_devices SET device_type='station' WHERE LOWER(TRIM(COALESCE(device_type,''))) IN ('station','receiver','client','cpe','powerbeam')");
        @$db->query("UPDATE airmax_devices SET device_type='access' WHERE device_type IS NULL OR TRIM(device_type)='' OR LOWER(TRIM(device_type)) NOT IN ('access','station')");
        @$db->query("UPDATE airmax_devices SET preferred_vendor='auto' WHERE preferred_vendor IS NULL OR TRIM(preferred_vendor)='' ");
        @$db->query("UPDATE airmax_devices SET preferred_vendor='mimosa' WHERE LOWER(TRIM(preferred_vendor)) IN ('mimosa','mimosac5','mimosac5x','c5','c5x','b5','b5c','mimosaos')");
        @$db->query("UPDATE airmax_devices SET parent_device_id=NULL WHERE parent_device_id IS NOT NULL AND parent_device_id=id");
    } catch (Throwable $e) {}
    return true;
}
}

if (!function_exists('airmax_shell_arg')) {
function airmax_shell_arg(string $arg): string {
    return escapeshellarg($arg);
}
}

if (!function_exists('airmax_find_ssh_binary')) {
function airmax_find_ssh_binary(): string {
    $candidates = [];
    if (stripos(PHP_OS_FAMILY, 'Windows') !== false) {
        $windir = getenv('WINDIR') ?: 'C:\\Windows';
        $candidates[] = $windir . '\\System32\\OpenSSH\\ssh.exe';
        $candidates[] = 'C:\\Windows\\System32\\OpenSSH\\ssh.exe';
    }
    $candidates[] = 'ssh';
    foreach ($candidates as $candidate) {
        if ($candidate === 'ssh') return $candidate;
        if (is_file($candidate)) return $candidate;
    }
    return '';
}
}

if (!function_exists('airmax_proc_read_available')) {
function airmax_proc_read_available($pipe): string {
    if (!is_resource($pipe)) return '';
    $out = '';
    while (!feof($pipe)) {
        $chunk = @fread($pipe, 8192);
        if ($chunk === false || $chunk === '') break;
        $out .= $chunk;
        if (strlen($out) > 262144) break;
    }
    return $out;
}
}

if (!function_exists('airmax_run_ssh_command')) {
function airmax_run_ssh_command(string $host, string $username, string $password, string $remoteCommand, int $timeout = 18): array {
    if (!function_exists('proc_open')) {
        return ['ok'=>false, 'stdout'=>'', 'stderr'=>'proc_open غير متاح على الخادم.', 'exit'=>-1];
    }
    $ssh = airmax_find_ssh_binary();
    if ($ssh === '') {
        return ['ok'=>false, 'stdout'=>'', 'stderr'=>'لم يتم العثور على ssh.exe على الخادم.', 'exit'=>-1];
    }
    $tmp = rtrim((string)sys_get_temp_dir(), "\\/") . DIRECTORY_SEPARATOR . 'ua_ssh_askpass_' . uniqid('', true) . '.cmd';
    $askpass = '@echo off' . "\r\n"
        . 'powershell -NoProfile -ExecutionPolicy Bypass -Command "[Console]::Out.Write($env:UA_SSH_PASSWORD)"' . "\r\n";
    @file_put_contents($tmp, $askpass);

    $target = $username . '@' . $host;
    $args = [
        $ssh,
        '-o', 'BatchMode=no',
        '-o', 'NumberOfPasswordPrompts=1',
        '-o', 'PreferredAuthentications=password,keyboard-interactive,publickey',
        '-o', 'PubkeyAuthentication=yes',
        '-o', 'StrictHostKeyChecking=no',
        '-o', 'UserKnownHostsFile=NUL',
        '-o', 'ConnectTimeout=6',
        '-o', 'ServerAliveInterval=5',
        '-o', 'ServerAliveCountMax=1',
        $target,
        $remoteCommand,
    ];
    $cmd = implode(' ', array_map('airmax_shell_arg', $args));
    $desc = [0=>['pipe','r'], 1=>['pipe','w'], 2=>['pipe','w']];
    $env = $_ENV;
    $env['SSH_ASKPASS'] = $tmp;
    $env['SSH_ASKPASS_REQUIRE'] = 'force';
    $env['DISPLAY'] = 'none';
    $env['UA_SSH_PASSWORD'] = $password;
    $proc = @proc_open($cmd, $desc, $pipes, null, $env);
    if (!is_resource($proc)) {
        @unlink($tmp);
        return ['ok'=>false, 'stdout'=>'', 'stderr'=>'تعذر تشغيل SSH.', 'exit'=>-1];
    }
    @fclose($pipes[0]);
    @stream_set_blocking($pipes[1], false);
    @stream_set_blocking($pipes[2], false);
    $stdout = '';
    $stderr = '';
    $start = microtime(true);
    $exit = null;
    while (true) {
        $stdout .= airmax_proc_read_available($pipes[1]);
        $stderr .= airmax_proc_read_available($pipes[2]);
        $status = @proc_get_status($proc);
        if (!$status || empty($status['running'])) {
            $exit = is_array($status) ? (int)($status['exitcode'] ?? 0) : 0;
            break;
        }
        if ((microtime(true) - $start) > $timeout) {
            @proc_terminate($proc);
            $stderr .= "\nانتهت مهلة سحب اللوج من الجهاز.";
            $exit = -1;
            break;
        }
        usleep(120000);
    }
    $stdout .= airmax_proc_read_available($pipes[1]);
    $stderr .= airmax_proc_read_available($pipes[2]);
    foreach ([1,2] as $i) { if (isset($pipes[$i]) && is_resource($pipes[$i])) @fclose($pipes[$i]); }
    @proc_close($proc);
    @unlink($tmp);
    $stdout = trim(str_replace("\r\n", "\n", $stdout));
    $stderr = trim(str_replace("\r\n", "\n", $stderr));
    return ['ok'=>($exit === 0 && $stdout !== ''), 'stdout'=>$stdout, 'stderr'=>$stderr, 'exit'=>$exit];
}
}

if (!function_exists('airmax_native_log_remote_command')) {
function airmax_native_log_remote_command(): string {
    return "printf '=== DATE ===\\n'; date; printf '\\n=== UPTIME ===\\n'; uptime; printf '\\n=== LOG ===\\n'; (logread 2>/dev/null || cat /var/log/messages 2>/dev/null || true); printf '\\n=== DMESG ===\\n'; dmesg 2>/dev/null";
}
}

if (!function_exists('airmax_native_log_summary')) {
function airmax_native_log_summary(string $log): array {
    $lines = preg_split('/\R/u', $log) ?: [];
    $logLines = 0;
    $dmesgLines = 0;
    $section = '';
    foreach ($lines as $line) {
        $trim = trim($line);
        if ($trim === '=== LOG ===') { $section = 'log'; continue; }
        if ($trim === '=== DMESG ===') { $section = 'dmesg'; continue; }
        if (str_starts_with($trim, '===')) { $section = ''; continue; }
        if ($trim === '') continue;
        if ($section === 'log') $logLines++;
        if ($section === 'dmesg') $dmesgLines++;
    }
    return ['log_lines'=>$logLines, 'dmesg_lines'=>$dmesgLines, 'total_lines'=>count(array_filter($lines, static fn($l) => trim((string)$l) !== ''))];
}
}

if (!function_exists('airmax_tcp_port_open')) {
function airmax_tcp_port_open(string $host, int $port, float $timeout = 2.5): bool {
    if ($host === '' || $port <= 0) return false;
    $errno = 0;
    $errstr = '';
    $fp = @fsockopen($host, $port, $errno, $errstr, $timeout);
    if (is_resource($fp)) {
        @fclose($fp);
        return true;
    }
    return false;
}
}

if (!function_exists('airmax_native_log_extract_text')) {
function airmax_native_log_extract_text(string $body, string $contentType = ''): string {
    $body = trim(str_replace("\r\n", "\n", $body));
    if ($body === '') return '';
    $ct = strtolower($contentType);
    if (str_contains($ct, 'json') || str_starts_with($body, '{') || str_starts_with($body, '[')) {
        $json = json_decode($body, true);
        $found = [];
        $walk = function ($value) use (&$walk, &$found): void {
            if (is_array($value)) {
                foreach ($value as $k => $v) {
                    $key = strtolower((string)$k);
                    if (is_string($v) && preg_match('/(syslogd|dropbear|wireless:|dnsmasq|kernel|dmesg|auth|assoc|dhcp)/i', $key . ' ' . $v)) {
                        $found[] = $v;
                    } elseif (is_array($v)) {
                        $walk($v);
                    }
                }
            }
        };
        if (is_array($json)) $walk($json);
        if ($found) return trim(implode("\n", $found));
    }
    if (preg_match('/<pre\b[^>]*>(.*?)<\/pre>/is', $body, $m)) {
        return trim(html_entity_decode(strip_tags($m[1]), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
    }
    if (preg_match('/<textarea\b[^>]*>(.*?)<\/textarea>/is', $body, $m)) {
        return trim(html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
    }
    if (stripos($body, '<html') !== false || stripos($body, '<body') !== false) {
        $text = trim(html_entity_decode(strip_tags($body), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
        $text = preg_replace("/[ \t]+/u", ' ', $text);
        $text = preg_replace("/\n{3,}/u", "\n\n", $text);
        return trim((string)$text);
    }
    return $body;
}
}

if (!function_exists('airmax_native_log_looks_real')) {
function airmax_native_log_looks_real(string $text): bool {
    $text = trim($text);
    if ($text === '' || strlen($text) < 40) return false;
    return (bool)preg_match('/(syslogd|dropbear|wireless:|dnsmasq|dhcp|kernel|dmesg|assoc|auth|lighttpd|httpd|filesystem|provmode|wpa_supplicant)/i', $text);
}
}

if (!function_exists('airmax_fetch_device_native_log_http')) {
function airmax_fetch_device_native_log_http(array $device, float $timeout = 5.0): array {
    $device = airmax_build_device($device);
    $host = trim((string)($device['host'] ?? ''));
    $protocol = strtolower((string)($device['protocol'] ?? 'https'));
    $port = (int)($device['port'] ?? ($protocol === 'http' ? 80 : 443));
    if ($host === '' || $port <= 0) {
        return ['ok'=>false, 'log'=>'', 'error'=>'عنوان الجهاز أو منفذ الويب غير مكتمل.', 'details'=>['method'=>'http']];
    }
    if (!airmax_tcp_port_open($host, $port, min(2.5, $timeout))) {
        return ['ok'=>false, 'log'=>'', 'error'=>'واجهة الويب ' . strtoupper($protocol) . ':' . $port . ' لا ترد من السيرفر.', 'details'=>['method'=>'http', 'protocol'=>$protocol, 'port'=>$port]];
    }
    $base = $protocol . '://' . $host . ':' . $port;
    $cookie = airmax_cookiejar_file($host);
    @unlink($cookie);
    $attempts = [];
    airmax_do_classic_login($device, $base, $timeout, $attempts);
    airmax_do_airos8_login($device, $base, $timeout, $attempts);
    $paths = [
        '/log.cgi',
        '/syslog.cgi',
        '/messages.cgi',
        '/systemlog.cgi',
        '/system.log',
        '/log.html',
        '/log.htm',
        '/api/log',
        '/api/logs',
        '/api/system/log',
        '/api/system/logs',
        '/support.cgi',
        '/diagnostics.cgi',
    ];
    $checked = [];
    foreach ($paths as $path) {
        $resp = airmax_http($base . $path, ['timeout'=>$timeout, 'cookie'=>$cookie]);
        $checked[] = $path . ':' . (int)$resp['status'];
        if ((int)$resp['status'] !== 200 || trim((string)$resp['body']) === '') continue;
        $contentType = '';
        $text = airmax_native_log_extract_text((string)$resp['body'], $contentType);
        if (!airmax_native_log_looks_real($text)) continue;
        $log = "=== DATE ===\n" . date('Y-m-d H:i:s') . "\n\n=== LOG ===\n" . $text;
        $summary = airmax_native_log_summary($log);
        return ['ok'=>true, 'log'=>$log, 'error'=>'', 'details'=>$summary + ['method'=>'http', 'host'=>$host, 'protocol'=>$protocol, 'port'=>$port, 'path'=>$path]];
    }
    return ['ok'=>false, 'log'=>'', 'error'=>'لم يتم العثور على مسار لوج حقيقي عبر واجهة الويب.', 'details'=>['method'=>'http', 'host'=>$host, 'protocol'=>$protocol, 'port'=>$port, 'checked'=>implode(', ', $checked), 'login_attempts'=>$attempts]];
}
}

if (!function_exists('airmax_fetch_device_native_log')) {
function airmax_fetch_device_native_log(mysqli $db, array $device): array {
    $device = airmax_build_device($device);
    $host = trim((string)($device['host'] ?? ''));
    $username = trim((string)($device['username'] ?? ''));
    $password = (string)($device['password'] ?? '');
    if ($host === '' || $username === '') {
        return ['ok'=>false, 'log'=>'', 'error'=>'عنوان الجهاز واسم المستخدم مطلوبان لسحب اللوج الحقيقي.', 'details'=>[]];
    }
    $sshError = '';
    $sshDetails = ['method'=>'ssh', 'host'=>$host, 'username'=>$username];
    if (airmax_tcp_port_open($host, 22, 2.5)) {
        $result = airmax_run_ssh_command($host, $username, $password, airmax_native_log_remote_command(), 24);
    } else {
        $result = ['ok'=>false, 'stdout'=>'', 'stderr'=>'منفذ SSH 22 لا يرد من السيرفر.', 'exit'=>255];
    }
    if (!empty($result['ok'])) {
        $log = (string)$result['stdout'];
        $summary = airmax_native_log_summary($log);
        return ['ok'=>true, 'log'=>$log, 'error'=>'', 'details'=>$summary + ['method'=>'ssh', 'host'=>$host, 'username'=>$username]];
    }
    $sshError = trim((string)($result['stderr'] ?? ''));
    if ($sshError === '') $sshError = 'تعذر سحب اللوج الحقيقي عبر SSH.';
    $sshDetails['exit'] = $result['exit'] ?? null;

    $http = airmax_fetch_device_native_log_http($device, 5.0);
    if (!empty($http['ok'])) return $http;

    $httpError = trim((string)($http['error'] ?? ''));
    $details = $sshDetails + [
        'ssh_error'=>$sshError,
        'http_error'=>$httpError,
        'fallback_method'=>'http',
    ];
    if (isset($http['details']) && is_array($http['details'])) $details['http_details'] = $http['details'];
    return ['ok'=>false, 'log'=>'', 'error'=>'فشل سحب اللوج الحقيقي. SSH: ' . $sshError . ($httpError !== '' ? ' | Web: ' . $httpError : ''), 'details'=>$details];
}
}

if (!function_exists('airmax_device_log_summary')) {
function airmax_device_log_summary(string $status, string $rawEvent, string $source = ''): string {
    if (is_file(__DIR__ . '/event_translation_lib.php')) require_once __DIR__ . '/event_translation_lib.php';
    if (function_exists('ua_device_log_translate')) return ua_device_log_translate($status, $rawEvent, $source);
    return $status === 'success' ? 'تم فحص الجهاز بنجاح.' : 'فشل فحص الجهاز.';
}
}

if (!function_exists('airmax_device_log_event')) {
function airmax_device_log_event(mysqli $db, array $device, string $eventKey, string $status, string $rawEvent, array $details = [], string $source = 'airmax_probe'): void {
    airmax_ensure_probe_schema($db);
    $summary = airmax_device_log_summary($status, $rawEvent, $source);
    $detailsJson = json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $deviceId = (int)($device['id'] ?? 0);
    $name = (string)($device['name'] ?? '');
    $host = (string)($device['host'] ?? '');
    $type = function_exists('airmax_device_role') ? airmax_device_role((string)($device['device_type'] ?? '')) : (string)($device['device_type'] ?? '');
    $stmt = @$db->prepare("INSERT INTO device_activity_log (device_id, device_name, host, device_type, event_source, event_key, status, raw_event, arabic_summary, details_json, event_at, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,NOW(),NOW())");
    if (!$stmt) return;
    $stmt->bind_param('isssssssss', $deviceId, $name, $host, $type, $source, $eventKey, $status, $rawEvent, $summary, $detailsJson);
    @$stmt->execute();
    $stmt->close();
}
}

if (!function_exists('airmax_get_all')) {
function airmax_get_all($db = null, bool $enabledOnly=false): array {
    if (!($db instanceof mysqli)) return [];
    $sql = "SELECT * FROM airmax_devices";
    if ($enabledOnly) $sql .= " WHERE is_enabled=1";
    $sql .= " ORDER BY sort_order ASC, id ASC";
    $q = @$db->query($sql);
    if (!$q) return [];
    $out = [];
    while ($r = $q->fetch_assoc()) $out[] = $r;
    return $out;
}
}

if (!function_exists('airmax_build_device')) {
function airmax_build_device($device): array {
    if (is_string($device)) {
        return [
            'id'=>0,'name'=>$device,'host'=>$device,'device_type'=>'station',
            'protocol'=>'https','port'=>443,'preferred_vendor'=>'auto','username'=>'ubnt','password'=>''
        ];
    }
    $d = is_array($device) ? $device : [];
    $d['id'] = (int)($d['id'] ?? 0);
    $d['name'] = (string)($d['name'] ?? $d['host'] ?? ('Device #'.$d['id']));
    $d['host'] = trim((string)($d['host'] ?? ''));
    $d['protocol'] = strtolower((string)($d['protocol'] ?? 'https')) === 'http' ? 'http' : 'https';
    $d['port'] = (int)($d['port'] ?? ($d['protocol']==='http' ? 80 : 443));
    $d['device_type'] = airmax_device_role((string)($d['device_type'] ?? 'access'));
    $d['preferred_vendor'] = function_exists('airmax_preferred_vendor') ? airmax_preferred_vendor((string)($d['preferred_vendor'] ?? $d['vendor_hint'] ?? 'auto')) : (string)($d['preferred_vendor'] ?? $d['vendor_hint'] ?? 'auto');
    $d['parent_device_id'] = (int)($d['parent_device_id'] ?? 0);
    $d['username'] = (string)($d['username'] ?? 'ubnt');
    $d['password'] = (string)($d['password'] ?? '');
    $d['snmp_community'] = trim((string)($d['snmp_community'] ?? 'public'));
    if ($d['snmp_community'] === '') $d['snmp_community'] = 'public';
   return $d;
}
}

if (!function_exists('airmax_device_role')) {
function airmax_device_role($type): string {
    $t = strtolower(trim((string)$type));
    $t = str_replace(['-', '_', ' '], '', $t);
    // Backward compatibility with previous values used by this project.
    if (in_array($t, ['station','receiver','client','powerbeam','cpe','استيشن','مستقبل'], true)) return 'station';
    return 'access';
}
}

if (!function_exists('airmax_device_is_station')) {
function airmax_device_is_station($device): bool {
    if (is_array($device)) return airmax_device_role($device['device_type'] ?? '') === 'station';
    return airmax_device_role($device) === 'station';
}
}

if (!function_exists('airmax_device_role_text')) {
function airmax_device_role_text($type): string {
    return airmax_device_role($type) === 'station' ? 'Station / مستقبل' : 'Access / قطاع';
}
}


if (!function_exists('airmax_preferred_vendor')) {
function airmax_preferred_vendor($vendor): string {
    $v = strtolower(trim((string)$vendor));
    $v = str_replace(['-', '_', ' '], '', $v);
    if (in_array($v, ['mimosa','mimosac5','mimosac5x','c5','c5x','b5','b5c','mimosaos'], true)) return 'mimosa';
    if (in_array($v, ['ubiquiti','ubnt','airmax','airos','powerbeam','litebeam'], true)) return 'ubiquiti';
    if (in_array($v, ['mikrotik','routeros'], true)) return 'mikrotik';
    if ($v === 'generic') return 'generic';
    return 'auto';
}
}

if (!function_exists('airmax_cookiejar_file')) {
function airmax_cookiejar_file($host = ''): string {
    $dir = sys_get_temp_dir() . '/airmax_cookies';

    if (!is_dir($dir)) {
        @mkdir($dir, 0777, true);
    }

    $name = preg_replace('/[^a-zA-Z0-9]/', '_', (string)$host);
    if ($name === '') $name = 'default';

    return $dir . '/cookie_' . $name . '.txt';
}
}

if (!function_exists('airmax_http')) {
function airmax_http(string $url, array $opts=[]): array {
    $timeout = isset($opts['timeout']) ? (float)$opts['timeout'] : 4.0;
    $cookie = (string)($opts['cookie'] ?? '');
    $post = $opts['post'] ?? null;
    $headers = (array)($opts['headers'] ?? []);
    // Some legacy radios on routed management networks complete TCP quickly
    // but need around 10 seconds for the TLS handshake/first HTTP byte. Keep
    // the connect phase inside the caller's total budget instead of cutting it
    // off at 2.5 seconds and reporting a false DOWN state.
    $connectTimeout = isset($opts['connect_timeout']) ? (float)$opts['connect_timeout'] : min(45.0, $timeout);

    // Some fresh Windows installations run this application with a PHP build
    // where ext-curl has not been enabled yet.  Keep device discovery working
    // through PHP's native HTTP streams instead of terminating the whole scan
    // with an undefined curl_init() fatal error.
    if (!function_exists('curl_init')) {
        $streamHeaders = array_merge(['Expect:', 'User-Agent: Mozilla/5.0 AirMaxMonitor/1.0'], $headers);
        $cookieStore = $cookie !== '' ? $cookie . '.stream.json' : '';
        $storedCookies = [];
        if ($cookieStore !== '' && is_file($cookieStore)) {
            $decodedCookies = json_decode((string)@file_get_contents($cookieStore), true);
            if (is_array($decodedCookies)) $storedCookies = $decodedCookies;
        }
        if ($storedCookies) {
            $pairs = [];
            foreach ($storedCookies as $name => $value) {
                if ($name !== '') $pairs[] = $name . '=' . $value;
            }
            if ($pairs) $streamHeaders[] = 'Cookie: ' . implode('; ', $pairs);
        }

        $httpOptions = [
            'method' => $post === null ? 'GET' : 'POST',
            'header' => implode("\r\n", $streamHeaders),
            'timeout' => max(0.6, $timeout),
            'ignore_errors' => true,
            'follow_location' => 0,
            'max_redirects' => 0,
        ];
        if ($post !== null) {
            $httpOptions['content'] = is_array($post) ? http_build_query($post) : (string)$post;
            $hasContentType = false;
            foreach ($streamHeaders as $header) {
                if (stripos((string)$header, 'content-type:') === 0) {
                    $hasContentType = true;
                    break;
                }
            }
            if (!$hasContentType) {
                $httpOptions['header'] .= "\r\nContent-Type: application/x-www-form-urlencoded";
            }
        }

        $context = stream_context_create([
            'http' => $httpOptions,
            'ssl' => [
                'verify_peer' => false,
                'verify_peer_name' => false,
                'allow_self_signed' => true,
                'ciphers' => 'DEFAULT@SECLEVEL=0',
                'security_level' => 0,
            ],
        ]);
        if (function_exists('error_clear_last')) error_clear_last();
        $body = @file_get_contents($url, false, $context);
        $lastError = error_get_last();
        $responseHeaders = isset($http_response_header) && is_array($http_response_header)
            ? $http_response_header
            : [];
        $status = 0;
        foreach ($responseHeaders as $header) {
            if (preg_match('~^HTTP/\S+\s+(\d{3})~i', (string)$header, $match)) {
                $status = (int)$match[1];
            }
            if (stripos((string)$header, 'Set-Cookie:') === 0) {
                $cookieLine = trim(substr((string)$header, strlen('Set-Cookie:')));
                $cookiePair = trim(explode(';', $cookieLine, 2)[0] ?? '');
                $separator = strpos($cookiePair, '=');
                if ($separator !== false) {
                    $name = trim(substr($cookiePair, 0, $separator));
                    $value = trim(substr($cookiePair, $separator + 1));
                    if ($name !== '') {
                        if ($value === '') unset($storedCookies[$name]);
                        else $storedCookies[$name] = $value;
                    }
                }
            }
        }
        if ($cookieStore !== '' && $responseHeaders) {
            @file_put_contents($cookieStore, json_encode($storedCookies, JSON_UNESCAPED_SLASHES), LOCK_EX);
        }
        $body = $body === false ? '' : (string)$body;
        $error = '';
        if ($body === '' && $status === 0 && is_array($lastError)) {
            $error = (string)($lastError['message'] ?? 'HTTP stream request failed');
        }

        return [
            'status' => $status,
            'body' => $body,
            'error' => $error,
            'transport' => 'stream',
            'snippet' => function_exists('mb_substr')
                ? mb_substr(trim($body), 0, 180)
                : substr(trim($body), 0, 180),
        ];
    }

    $ch = curl_init($url);
    $curl = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => true,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_CONNECTTIMEOUT_MS => (int)max(200, $connectTimeout * 1000),
        CURLOPT_TIMEOUT_MS => (int)max(600, $timeout * 1000),
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_ENCODING => '',
        CURLOPT_USERAGENT => 'Mozilla/5.0 AirMaxMonitor/1.0',
        CURLOPT_HTTPHEADER => array_merge(['Expect:'], $headers),
    ];
    // Device management addresses must be reached directly.  Windows/PHP can
    // inherit a system VPN/proxy even when a browser correctly bypasses LAN
    // hosts, which made the same receiver open in the browser but fail here.
    $targetHost = strtolower((string)(parse_url($url, PHP_URL_HOST) ?: ''));
    $isPrivateTarget = filter_var($targetHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
      && !filter_var($targetHost, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    if ($isPrivateTarget || $targetHost === 'localhost') {
        $curl[CURLOPT_PROXY] = '';
        if (defined('CURLOPT_NOPROXY')) $curl[CURLOPT_NOPROXY] = '*';
    }
    if ($cookie !== '') {
        $curl[CURLOPT_COOKIEJAR] = $cookie;
        $curl[CURLOPT_COOKIEFILE] = $cookie;
    }
    if ($post !== null) {
        $curl[CURLOPT_POST] = true;
        $curl[CURLOPT_POSTFIELDS] = is_array($post) ? http_build_query($post) : (string)$post;
    }

    if (defined('CURLOPT_SSL_CIPHER_LIST')) {
        $curl[CURLOPT_SSL_CIPHER_LIST] = 'DEFAULT@SECLEVEL=0';
    }

    curl_setopt_array($ch, $curl);
    $raw = curl_exec($ch);
    $err = curl_error($ch);
    $status = 0;
    $body = '';
    if ($raw !== false) {
        $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $hsz = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $body = (string)substr((string)$raw, $hsz);
    }
    curl_close($ch);

    return [
        'status' => $status,
        'body' => $body,
        'error' => (string)$err,
        'transport' => 'curl',
        'snippet' => mb_substr(trim((string)$body), 0, 180),
    ];
}
}


if (!function_exists('airmax_http_transport_dead')) {
function airmax_http_transport_dead(array $resp): bool {
    if ((int)($resp['status'] ?? 0) !== 0) return false;
    $err = strtolower((string)($resp['error'] ?? ''));
    if ($err === '') return false;
    foreach (['timeout','timed out','could not connect','couldn\'t connect','connection refused','no route to host','network is unreachable','host is unreachable','ssl connection timeout','operation timed out'] as $needle) {
        if (strpos($err, $needle) !== false) return true;
    }
    return false;
}
}

if (!function_exists('airmax_attempts_transport_dead')) {
function airmax_attempts_transport_dead(array $attempts): bool {
    if (!$attempts) return false;
    $sawTransportFailure = false;
    foreach ($attempts as $attempt) {
        if ((int)($attempt['status'] ?? 0) > 0) return false;
        $error = strtolower(trim((string)($attempt['error'] ?? '')));
        if ($error === '') continue;
        foreach (['timeout','timed out','could not connect','couldn\'t connect','connection refused','no route to host','network is unreachable','host is unreachable','ssl connection timeout','operation timed out'] as $needle) {
            if (strpos($error, $needle) !== false) {
                $sawTransportFailure = true;
                continue 2;
            }
        }
        return false;
    }
    return $sawTransportFailure;
}
}

if (!function_exists('airmax_metric_normalize_key')) {
function airmax_metric_normalize_key(string $key): string {
    return strtolower((string)preg_replace('/[^a-z0-9]+/i', '', $key));
}
}

if (!function_exists('airmax_metric_flatten')) {
function airmax_metric_flatten($value, array &$out, string $path = '', int $depth = 0): void {
    if ($depth > 10) return;
    if (is_string($value)) {
        $trimmed = trim($value);
        if ($trimmed !== '' && (($trimmed[0] ?? '') === '{' || ($trimmed[0] ?? '') === '[')) {
            $decoded = json_decode($trimmed, true);
            if (is_array($decoded)) {
                airmax_metric_flatten($decoded, $out, $path, $depth + 1);
                return;
            }
        }
    }
    if (!is_array($value)) return;
    foreach ($value as $key => $item) {
        $keyText = is_string($key) ? $key : '';
        $nextPath = $path;
        if ($keyText !== '') $nextPath = ($path === '' ? $keyText : ($path . '.' . $keyText));
        if (is_array($item)) {
            airmax_metric_flatten($item, $out, $nextPath, $depth + 1);
            continue;
        }
        if (is_string($item)) {
            $trimmed = trim($item);
            if ($trimmed !== '' && (($trimmed[0] ?? '') === '{' || ($trimmed[0] ?? '') === '[')) {
                $decoded = json_decode($trimmed, true);
                if (is_array($decoded)) {
                    airmax_metric_flatten($decoded, $out, $nextPath, $depth + 1);
                    continue;
                }
            }
        }
        if ($keyText === '' || (!is_scalar($item) && $item !== null)) continue;
        $out[] = [
            'key' => airmax_metric_normalize_key($keyText),
            'path' => airmax_metric_normalize_key($nextPath),
            'value' => $item,
        ];
    }
}
}

if (!function_exists('airmax_metric_find')) {
function airmax_metric_find(array $payload, array $aliases) {
    $flat = [];
    airmax_metric_flatten($payload, $flat);
    $wanted = [];
    foreach ($aliases as $alias) {
        $normalized = airmax_metric_normalize_key((string)$alias);
        if ($normalized !== '') $wanted[$normalized] = true;
    }
    foreach ($flat as $entry) {
        if (isset($wanted[(string)$entry['key']]) || isset($wanted[(string)$entry['path']])) return $entry['value'];
    }
    return null;
}
}

if (!function_exists('airmax_metric_percent')) {
function airmax_metric_percent($value): ?float {
    $value = airmax_scalar_or_null($value);
    if (!is_numeric($value)) return null;
    $number = (float)$value;
    // These four dashboard columns are percentages.  Reject counters, byte
    // totals and PHY rates instead of presenting a plausible but false value.
    if (!is_finite($number) || $number < 0 || $number > 100) return null;
    return round($number, 1);
}
}

if (!function_exists('airmax_extract_live_health_metrics')) {
function airmax_extract_live_health_metrics(array $payload): array {
    $metrics = [
        'quality' => airmax_metric_percent(airmax_metric_find($payload, [
            'airmaxquality', 'pollingquality', 'linkquality', 'linkqualitypercent',
            'wirelessquality', 'rxquality', 'qualitypercent', 'quality',
        ])),
        'capacity' => airmax_metric_percent(airmax_metric_find($payload, [
            'airmaxcapacity', 'pollingcapacity', 'linkcapacity', 'linkcapacitypercent',
            'wirelesscapacity', 'capacitypercent', 'capacity',
        ])),
        'cpu' => airmax_metric_percent(airmax_metric_find($payload, [
            'cpuload', 'cpuusage', 'cpuusagepercent', 'cpuutilization',
            'cpuutilizationpercent', 'processorload', 'processorusage', 'cpu',
        ])),
        'memory' => airmax_metric_percent(airmax_metric_find($payload, [
            'memoryusage', 'memoryusagepercent', 'memoryutilization',
            'memoryutilizationpercent', 'memoryload', 'ramusage', 'ramusagepercent',
            'memusage', 'memusagepercent', 'memory', 'ram',
        ])),
    ];

    if ($metrics['memory'] === null) {
        $total = airmax_scalar_or_null(airmax_metric_find($payload, ['totalram', 'memorytotal', 'totalmemory', 'ramtotal', 'memtotal']));
        $free = airmax_scalar_or_null(airmax_metric_find($payload, ['freeram', 'memoryfree', 'freememory', 'ramfree', 'memfree', 'availablememory']));
        $used = airmax_scalar_or_null(airmax_metric_find($payload, ['usedram', 'memoryusedbytes', 'usedmemory', 'ramused', 'memusedbytes']));
        if (is_numeric($total) && (float)$total > 0) {
            if (is_numeric($free)) $metrics['memory'] = round((1 - ((float)$free / (float)$total)) * 100, 1);
            elseif (is_numeric($used)) $metrics['memory'] = round(((float)$used / (float)$total) * 100, 1);
            if ($metrics['memory'] !== null && ($metrics['memory'] < 0 || $metrics['memory'] > 100)) $metrics['memory'] = null;
        }
    }
    return $metrics;
}
}

if (!function_exists('airmax_fill_missing_live_health_metrics')) {
function airmax_fill_missing_live_health_metrics(array $metrics, array $payload): array {
    $found = airmax_extract_live_health_metrics($payload);
    foreach (['quality', 'capacity', 'cpu', 'memory'] as $key) {
        if ((!array_key_exists($key, $metrics) || $metrics[$key] === null || $metrics[$key] === '') && $found[$key] !== null) {
            $metrics[$key] = $found[$key];
        }
    }
    return $metrics;
}
}

if (!function_exists('airmax_parse_status_body')) {
function airmax_parse_status_body(string $body): array {
    $j = json_decode($body, true);
    if (!is_array($j)) return [];
    $wireless = isset($j['wireless']) && is_array($j['wireless']) ? $j['wireless'] : [];
    $host = isset($j['host']) && is_array($j['host']) ? $j['host'] : [];
    $poll = isset($wireless['polling']) && is_array($wireless['polling']) ? $wireless['polling'] : [];
    $metrics = [
        'os' => isset($host['fwversion']) ? ((stripos((string)$host['fwversion'], 'v8')!==false || isset($j['chain_names'])) ? 'airOS' : 'airOSClassic') : (isset($j['chain_names']) ? 'airOS' : 'airOSClassic'),
        'model' => (string)($host['devmodel'] ?? $host['hostname'] ?? ''),
        'signal' => isset($wireless['signal']) ? $wireless['signal'] : null,
        'noise' => isset($wireless['noisef']) ? $wireless['noisef'] : (isset($wireless['noise']) ? $wireless['noise'] : null),
        'ccq' => isset($wireless['ccq']) ? ((float)$wireless['ccq'] > 100 ? ((float)$wireless['ccq']/10.0) : (float)$wireless['ccq']) : null,
        'quality' => isset($poll['quality']) ? $poll['quality'] : null,
        'capacity' => isset($poll['capacity']) ? $poll['capacity'] : null,
        'clients' => isset($wireless['count']) ? (int)$wireless['count'] : 0,
        'cpu' => isset($host['cpuload']) ? (float)$host['cpuload'] : null,
        'memory' => (isset($host['totalram']) && isset($host['freeram']) && (float)$host['totalram'] > 0)
            ? round((1 - ((float)$host['freeram']/(float)$host['totalram'])) * 100, 1)
            : null,
        'uptime' => isset($host['uptime']) ? $host['uptime'] : null,
        'tx_rate' => isset($wireless['txrate']) ? $wireless['txrate'] : null,
        'rx_rate' => isset($wireless['rxrate']) ? $wireless['rxrate'] : null,
        'channel' => isset($wireless['frequency']) ? $wireless['frequency'] : (isset($wireless['channel']) ? $wireless['channel'] : null),
        'bridge_count' => 0,
        'bridge_pages' => 0,
        'bridge_source' => '',
        '_transport' => 'curl',
    ];
    // Newer airOS builds sometimes move these four health values while keeping
    // the rest of status.cgi unchanged.  Enrich missing values only; the
    // proven classic paths above always win.
    return airmax_fill_missing_live_health_metrics($metrics, $j);
}
}





if (!function_exists('airmax_scalar_or_null')) {
function airmax_scalar_or_null($value) {
    if ($value === null) return null;
    if (is_numeric($value)) return $value + 0;
    $v = trim((string)$value);
    if ($v === '') return null;
    if (preg_match('/-?\d+(?:\.\d+)?/', $v, $m)) return $m[0] + 0;
    return $v;
}
}

if (!function_exists('airmax_mimosa_xml_values')) {
function airmax_mimosa_xml_values(string $body): array {
    $out = [];
    $body = trim($body);
    if ($body === '') return [];
    if (function_exists('simplexml_load_string')) {
        $xml = @simplexml_load_string($body, 'SimpleXMLElement', LIBXML_NOCDATA);
        if ($xml instanceof SimpleXMLElement) {
            $nodes = @$xml->xpath('//*[not(*)]');
            if (is_array($nodes)) {
                foreach ($nodes as $node) {
                    $key = (string)$node->getName();
                    $val = trim((string)$node);
                    if ($key !== '' && $val !== '') $out[$key] = $val;
                }
            }
        }
    }
    if (!$out && preg_match_all('/<([A-Za-z_][A-Za-z0-9_\-]*)\b[^>]*>([^<]*)<\/\1>/s', $body, $m, PREG_SET_ORDER)) {
        foreach ($m as $row) {
            $key = (string)$row[1];
            $val = trim(strip_tags((string)$row[2]));
            if ($key !== '' && $val !== '') $out[$key] = html_entity_decode($val, ENT_QUOTES | ENT_XML1, 'UTF-8');
        }
    }
    return $out;
}
}

if (!function_exists('airmax_mimosa_value')) {
function airmax_mimosa_value(array $values, array $keys, $default = null) {
    $lower = [];
    foreach ($values as $k => $v) $lower[strtolower((string)$k)] = $v;
    foreach ($keys as $k) {
        $lk = strtolower((string)$k);
        if (array_key_exists($lk, $lower)) return $lower[$lk];
    }
    return $default;
}
}

if (!function_exists('airmax_mimosa_api_url')) {
function airmax_mimosa_api_url(string $base, string $path, array $device): string {
    $username = trim((string)($device['username'] ?? ''));
    if ($username === '') $username = 'configure';
    $qs = http_build_query([
        'username' => $username,
        'password' => (string)($device['password'] ?? ''),
    ]);
    return rtrim($base, '/') . $path . ($qs !== '' ? ('?' . $qs) : '');
}
}

if (!function_exists('airmax_mimosa_api_get')) {
function airmax_mimosa_api_get(array $device, string $base, string $path, float $timeout, array &$attempts): array {
    $resp = airmax_http(airmax_mimosa_api_url($base, $path, $device), [
        'timeout' => $timeout,
        'headers' => ['Accept: application/xml, text/xml, */*'],
    ]);
    $values = airmax_mimosa_xml_values((string)($resp['body'] ?? ''));
    $okXml = $values || stripos((string)($resp['body'] ?? ''), '<mimosa') !== false || stripos((string)($resp['body'] ?? ''), '<response') !== false;
    $dead = function_exists('airmax_http_transport_dead') ? airmax_http_transport_dead($resp) : false;
    $attempts[] = [
        'step' => 'probe mimosa ' . $path,
        'status' => (int)($resp['status'] ?? 0),
        'transport' => (string)($resp['transport'] ?? 'curl'),
        'error' => (string)($resp['error'] ?? ''),
        'snippet' => (string)($resp['snippet'] ?? ''),
        'xml' => $okXml,
        'transport_dead' => $dead,
    ];
    return ['resp'=>$resp, 'values'=>$values, 'ok_xml'=>$okXml, 'transport_dead'=>$dead];
}
}

if (!function_exists('airmax_mimosa_candidate_routes')) {
function airmax_mimosa_candidate_routes(array $device): array {
    $device = airmax_build_device($device);
    $configuredProtocol = strtolower((string)($device['protocol'] ?? 'https')) === 'http' ? 'http' : 'https';
    $configuredPort = (int)($device['port'] ?? ($configuredProtocol === 'http' ? 80 : 443));
    if ($configuredPort <= 0) $configuredPort = ($configuredProtocol === 'http' ? 80 : 443);

    $candidates = [[$configuredProtocol, $configuredPort]];

    // Mimosa management is often HTTP/80 unless HTTPS is explicitly enabled.
    // Try only standard alternate management routes, and only once per route.
    foreach ([['http', 80], ['https', 443]] as $std) {
        if ($std[0] === $configuredProtocol && (int)$std[1] === $configuredPort) continue;
        $candidates[] = $std;
    }

    $out = [];
    $seen = [];
    foreach ($candidates as $c) {
        $k = $c[0] . ':' . (int)$c[1];
        if (isset($seen[$k])) continue;
        $seen[$k] = true;
        $out[] = ['protocol'=>$c[0], 'port'=>(int)$c[1], 'base'=>$c[0] . '://' . (string)$device['host'] . ':' . (int)$c[1]];
    }
    return $out;
}
}


if (!function_exists('airmax_mimosa_product_name')) {
function airmax_mimosa_product_name(string $code): string {
    $code = strtoupper(trim($code));
    $map = [
        'B02'=>'B5c', 'B11'=>'B11', 'B24'=>'B24', 'B01'=>'B5', 'B04'=>'B5-Lite',
        'C01'=>'C5', 'C02'=>'C5c', 'C04'=>'C5x', 'B5X'=>'B5x'
    ];
    return $map[$code] ?? ($code !== '' ? $code : 'Mimosa');
}
}

if (!function_exists('airmax_mimosa_parse_webui_bootstrap')) {
function airmax_mimosa_parse_webui_bootstrap(string $html): array {
    $out = [];
    if ($html === '') return $out;
    if (preg_match('/<title>\s*([^<]+?)\s*<\/title>/i', $html, $m)) {
        $out['title'] = trim(html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
    }
    if (preg_match_all('/mimosa\.([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\'([^\']*)\'|"([^"]*)"|true|false|-?\d+(?:\.\d+)?)/i', $html, $m, PREG_SET_ORDER)) {
        foreach ($m as $row) {
            $key = (string)$row[1];
            $raw = (string)$row[2];
            if (isset($row[3]) && $row[3] !== '') {
                $val = $row[3];
            } elseif (isset($row[4]) && $row[4] !== '') {
                $val = $row[4];
            } elseif (strcasecmp($raw, 'true') === 0) {
                $val = true;
            } elseif (strcasecmp($raw, 'false') === 0) {
                $val = false;
            } elseif (is_numeric($raw)) {
                $val = $raw + 0;
            } else {
                $val = trim($raw, "'\"");
            }
            $out[$key] = $val;
        }
    }
    if (!isset($out['product']) && isset($out['title'])) $out['product'] = (string)$out['title'];
    if (isset($out['product'])) $out['modelName'] = airmax_mimosa_product_name((string)$out['product']);
    return $out;
}
}

if (!function_exists('airmax_mimosa_ajax_url')) {
function airmax_mimosa_ajax_url(string $base, string $query, array $args=[]): string {
    $params = array_merge(['q'=>$query, 'mimosa_ajax'=>1], $args);
    return rtrim($base, '/') . '/?' . http_build_query($params);
}
}

if (!function_exists('airmax_mimosa_cookie_file')) {
function airmax_mimosa_cookie_file(array $device, string $protocol, int $port): string {
    $host = (string)($device['host'] ?? 'mimosa');
    return airmax_cookiejar_file('mimosa_' . $protocol . '_' . $port . '_' . $host);
}
}

if (!function_exists('airmax_mimosa_web_login')) {
function airmax_mimosa_web_login(array $device, string $base, string $protocol, int $port, float $timeout, array &$attempts): array {
    $cookie = airmax_mimosa_cookie_file($device, $protocol, $port);
    @unlink($cookie);

    $home = airmax_http(rtrim($base, '/') . '/', [
        'timeout' => $timeout,
        'cookie' => $cookie,
        'headers' => ['Accept: text/html,application/xhtml+xml,*/*'],
    ]);
    $attempts[] = [
        'step' => 'probe mimosa webui /',
        'status' => (int)($home['status'] ?? 0),
        'transport' => (string)($home['transport'] ?? 'curl'),
        'error' => (string)($home['error'] ?? ''),
        'snippet' => (string)($home['snippet'] ?? ''),
        'transport_dead' => function_exists('airmax_http_transport_dead') ? airmax_http_transport_dead($home) : false,
    ];
    if (function_exists('airmax_http_transport_dead') && airmax_http_transport_dead($home)) {
        return ['ok'=>false, 'cookie'=>$cookie, 'home'=>$home, 'bootstrap'=>[], 'transport_dead'=>true, 'role'=>0, 'error'=>'transport_dead'];
    }

    $bootstrap = airmax_mimosa_parse_webui_bootstrap((string)($home['body'] ?? ''));
    $password = (string)($device['password'] ?? '');
    if ($password === '') {
        return ['ok'=>false, 'cookie'=>$cookie, 'home'=>$home, 'bootstrap'=>$bootstrap, 'role'=>0, 'error'=>'missing_password'];
    }

    $username = trim((string)($device['username'] ?? ''));
    if ($username === '') $username = 'configure';
    $login = airmax_http(rtrim($base, '/') . '/login.php', [
        'timeout' => $timeout,
        'cookie' => $cookie,
        'post' => ['username'=>$username, 'password'=>$password],
        'headers' => ['Accept: application/json, text/javascript, */*'],
    ]);
    $j = json_decode((string)($login['body'] ?? ''), true);
    $role = is_array($j) ? (int)($j['role'] ?? 0) : 0;
    $attempts[] = [
        'step' => 'probe mimosa login.php',
        'status' => (int)($login['status'] ?? 0),
        'transport' => (string)($login['transport'] ?? 'curl'),
        'error' => (string)($login['error'] ?? ''),
        'snippet' => (string)($login['snippet'] ?? ''),
        'json' => is_array($j),
        'role' => $role,
        'transport_dead' => function_exists('airmax_http_transport_dead') ? airmax_http_transport_dead($login) : false,
    ];
    $ok = ((int)($login['status'] ?? 0) === 200 && is_array($j) && $role > 0 && empty($j['error']));
    if (!$ok && !(function_exists('airmax_http_transport_dead') && airmax_http_transport_dead($login))) {
        // Current C5x firmware routes login through the normal Mimosa AJAX
        // dispatcher.  Keep login.php as the first path for older devices and
        // use this only as a compatibility fallback.
        $ajaxLogin = airmax_http(airmax_mimosa_ajax_url($base, 'index.login'), [
            'timeout' => $timeout,
            'cookie' => $cookie,
            'post' => ['username'=>$username, 'password'=>$password],
            'headers' => ['Accept: application/json, text/javascript, */*'],
        ]);
        $ajaxJson = json_decode((string)($ajaxLogin['body'] ?? ''), true);
        $ajaxRole = is_array($ajaxJson) ? (int)($ajaxJson['role'] ?? 0) : 0;
        $attempts[] = [
            'step' => 'probe mimosa index.login ajax fallback',
            'status' => (int)($ajaxLogin['status'] ?? 0),
            'transport' => (string)($ajaxLogin['transport'] ?? 'curl'),
            'error' => (string)($ajaxLogin['error'] ?? ''),
            'snippet' => (string)($ajaxLogin['snippet'] ?? ''),
            'json' => is_array($ajaxJson),
            'role' => $ajaxRole,
            'transport_dead' => function_exists('airmax_http_transport_dead') ? airmax_http_transport_dead($ajaxLogin) : false,
        ];
        $ajaxOk = ((int)($ajaxLogin['status'] ?? 0) === 200 && is_array($ajaxJson) && empty($ajaxJson['error'])
            && ($ajaxRole > 0 || isset($ajaxJson['productName']) || isset($ajaxJson['version'])));
        if ($ajaxOk) {
            $login = $ajaxLogin;
            $j = $ajaxJson;
            $role = $ajaxRole > 0 ? $ajaxRole : 1;
            $ok = true;
        }
    }
    return ['ok'=>$ok, 'cookie'=>$cookie, 'home'=>$home, 'bootstrap'=>$bootstrap, 'login'=>$login, 'login_json'=>$j, 'role'=>$role, 'error'=>$ok ? '' : (is_array($j) ? (string)($j['error'] ?? 'login_failed') : 'login_not_json')];
}
}

if (!function_exists('airmax_mimosa_fetch_dashboard_metrics')) {
function airmax_mimosa_fetch_dashboard_metrics(string $base, string $cookie, float $timeout, array &$attempts): array {
    // This is the read-only JSON source used by the embedded dashboard on
    // current Mimosa firmware.  It avoids scraping rendered labels and keeps
    // all writes/configuration endpoints completely out of the monitor.
    $path = '/cgi/dashboard.php';
    $resp = airmax_http(rtrim($base, '/') . $path, [
        'timeout' => $timeout,
        'cookie' => $cookie,
        'headers' => ['Accept: application/json, text/javascript, */*'],
    ]);
    $json = json_decode((string)($resp['body'] ?? ''), true);
    $metrics = is_array($json) ? airmax_extract_live_health_metrics($json) : ['quality'=>null, 'capacity'=>null, 'cpu'=>null, 'memory'=>null];
    $attempts[] = [
        'step' => 'probe mimosa dashboard metrics',
        'status' => (int)($resp['status'] ?? 0),
        'transport' => (string)($resp['transport'] ?? 'curl'),
        'error' => (string)($resp['error'] ?? ''),
        'snippet' => (string)($resp['snippet'] ?? ''),
        'json' => is_array($json),
        'metrics_found' => count(array_filter($metrics, static function($value){ return $value !== null; })),
        'transport_dead' => function_exists('airmax_http_transport_dead') ? airmax_http_transport_dead($resp) : false,
    ];
    return ['ok'=>is_array($json), 'source'=>$path, 'metrics'=>$metrics, 'json'=>$json];
}
}

if (!function_exists('airmax_mimosa_fdb_row_is_ethernet')) {
function airmax_mimosa_fdb_row_is_ethernet($port, $node): bool {
    /*
     * Mimosa C5x WebUI exposes Diagnostics > MAC Forwarding Table as FDB rows.
     * The displayed "Ethernet" port is represented in the raw JSON as port=4
     * on current C5x firmware, usually with node=0.  This is the Mimosa
     * equivalent of the customer-side/LAN ownership source.  Do not compare
     * these rows against Ubiquiti raw bridge sightings; only Ubiquiti LAN0
     * client rows are ownership sources.
     */
    if (is_numeric($port) && (int)$port === 4) return true;
    if (is_numeric($node) && (int)$node === 0) return true;
    $portText = is_string($port) ? strtolower(trim($port)) : strtolower(trim((string)$port));
    if ($portText === 'ethernet') return true;
    if (strpos($portText, 'eth') !== false || strpos($portText, 'lan') !== false) return true;
    return false;
}
}

if (!function_exists('airmax_mimosa_age_seconds')) {
function airmax_mimosa_age_seconds($age): ?int {
    if ($age === null || $age === '') return null;
    if (is_int($age) || ctype_digit((string)$age)) return max(0, (int)$age);
    $s = strtolower(trim((string)$age));
    $total = 0;
    if (preg_match_all('/(\d+)\s*(hour|hours|minute|minutes|second|seconds)/i', $s, $m, PREG_SET_ORDER)) {
        foreach ($m as $part) {
            $n = (int)$part[1];
            $unit = strtolower($part[2]);
            if (strpos($unit, 'hour') === 0) $total += $n * 3600;
            elseif (strpos($unit, 'minute') === 0) $total += $n * 60;
            else $total += $n;
        }
        return $total;
    }
    return null;
}
}

if (!function_exists('airmax_mimosa_fetch_fdb')) {
function airmax_mimosa_fetch_fdb(array $device, string $base, string $cookie, float $timeout, array &$attempts): array {
    $path = '/?q=diagnostics.fdb&mimosa_ajax=1';
    $resp = airmax_http(airmax_mimosa_ajax_url($base, 'diagnostics.fdb'), [
        'timeout' => $timeout,
        'cookie' => $cookie,
        'headers' => ['Accept: application/json, text/javascript, */*'],
    ]);
    $j = json_decode((string)($resp['body'] ?? ''), true);
    $attempts[] = [
        'step' => 'probe mimosa diagnostics.fdb',
        'status' => (int)($resp['status'] ?? 0),
        'transport' => (string)($resp['transport'] ?? 'curl'),
        'error' => (string)($resp['error'] ?? ''),
        'snippet' => (string)($resp['snippet'] ?? ''),
        'json' => is_array($j),
        'role' => is_array($j) && isset($j['role']) ? (int)$j['role'] : null,
        'transport_dead' => function_exists('airmax_http_transport_dead') ? airmax_http_transport_dead($resp) : false,
    ];
    if (!is_array($j)) return ['ok'=>false, 'source'=>$path, 'error'=>'not_json', 'resp'=>$resp, 'json'=>null, 'count'=>0, 'raw_count'=>0, 'macs'=>[], 'raw_macs'=>[], 'raw'=>[]];
    if (isset($j['role']) && (int)$j['role'] === 0) return ['ok'=>false, 'source'=>$path, 'error'=>'role_0', 'resp'=>$resp, 'json'=>$j, 'count'=>0, 'raw_count'=>0, 'macs'=>[], 'raw_macs'=>[], 'raw'=>[]];

    $data = $j['data'] ?? [];
    if (is_string($data)) {
        $decoded = json_decode($data, true);
        if (is_array($decoded)) $data = $decoded;
    }
    if (!is_array($data)) return ['ok'=>false, 'source'=>$path, 'error'=>'data_not_array', 'resp'=>$resp, 'json'=>$j, 'count'=>0, 'raw_count'=>0, 'macs'=>[], 'raw_macs'=>[], 'raw'=>[]];

    $raw = [];
    $rawMacs = [];
    $clientMacs = [];
    foreach ($data as $mac => $row) {
        $nm = airmax_norm_mac($mac);
        if ($nm === '') continue;
        $row = is_array($row) ? $row : [];
        $node = $row['node'] ?? null;
        $port = $row['port'] ?? '';
        $age = $row['age'] ?? null;
        $ageSeconds = function_exists('airmax_mimosa_age_seconds') ? airmax_mimosa_age_seconds($age) : (is_numeric($age) ? (int)$age : null);
        $rawMacs[] = $nm;
        $isEthernet = function_exists('airmax_mimosa_fdb_row_is_ethernet') ? airmax_mimosa_fdb_row_is_ethernet($port, $node) : false;
        if ($isEthernet) $clientMacs[] = $nm;
        $raw[] = [
            'mac'=>$nm,
            'age'=>$age,
            'age_seconds'=>$ageSeconds,
            'port'=>$port,
            'node'=>$node,
            'is_ethernet'=>$isEthernet,
            'ownership_candidate'=>$isEthernet,
            'ownership_policy'=>$isEthernet ? 'mimosa_fdb_port4_ethernet' : 'diagnostic_only_non_ethernet',
        ];
    }
    $clientMacs = airmax_unique_macs($clientMacs);
    $rawMacs = airmax_unique_macs($rawMacs);
    return [
        'ok'=>true,
        'source'=>$path,
        'count'=>count($clientMacs),
        'raw_count'=>count($rawMacs),
        'macs'=>$clientMacs,
        'raw_macs'=>$rawMacs,
        'raw'=>$raw,
        'ownership_policy'=>'mimosa_fdb_port4_ethernet_all_ages',
        'diagnostic_note'=>'Only Ethernet/port 4 FDB rows are promoted to subscriber matching; Wireless/non-Ethernet rows remain diagnostic raw data. Age is retained for diagnostics and future confidence analysis, not used to drop ownership rows.',
        'resp'=>$resp,
        'json'=>$j,
    ];
}
}

if (!function_exists('airmax_mimosa_try_fetch_webui')) {
function airmax_mimosa_try_fetch_webui(array $device, float $timeout=4.0): array {
    $device = airmax_build_device($device);
    $attempts = [];
    $raw = [];
    foreach (airmax_mimosa_candidate_routes($device) as $route) {
        $protocol = (string)$route['protocol'];
        $port = (int)$route['port'];
        $base = (string)$route['base'];
        $login = airmax_mimosa_web_login($device, $base, $protocol, $port, $timeout, $attempts);
        if (!empty($login['transport_dead'])) continue;
        $bootstrap = (array)($login['bootstrap'] ?? []);
        $hasWebUi = ((int)(($login['home']['status'] ?? 0)) === 200) && ($bootstrap || stripos((string)(($login['home']['body'] ?? '')), 'mimosa') !== false || stripos((string)(($login['home']['body'] ?? '')), 'airspan') !== false);
        if (!$hasWebUi) continue;

        $fdb = ['ok'=>false, 'count'=>0, 'raw_count'=>0, 'macs'=>[], 'raw_macs'=>[], 'raw'=>[], 'source'=>'', 'error'=>'not_station_or_not_attempted'];
        if (!empty($login['ok']) && airmax_device_is_station($device)) {
            $fdb = airmax_mimosa_fetch_fdb($device, $base, (string)$login['cookie'], $timeout, $attempts);
        }
        $dashboard = ['ok'=>false, 'source'=>'', 'metrics'=>['quality'=>null, 'capacity'=>null, 'cpu'=>null, 'memory'=>null], 'json'=>null];
        if (!empty($login['ok'])) {
            $dashboard = airmax_mimosa_fetch_dashboard_metrics($base, (string)$login['cookie'], $timeout, $attempts);
        }

        $raw['mimosa_webui_bootstrap'] = $bootstrap;
        $raw['mimosa_login'] = ['ok'=>(bool)($login['ok'] ?? false), 'role'=>(int)($login['role'] ?? 0), 'error'=>(string)($login['error'] ?? '')];
        $raw['mimosa_fdb_summary'] = ['ok'=>(bool)($fdb['ok'] ?? false), 'count'=>(int)($fdb['count'] ?? 0), 'raw_count'=>(int)($fdb['raw_count'] ?? 0), 'source'=>(string)($fdb['source'] ?? ''), 'error'=>(string)($fdb['error'] ?? '')];
        $raw['mimosa_dashboard_metrics'] = ['ok'=>(bool)($dashboard['ok'] ?? false), 'source'=>(string)($dashboard['source'] ?? ''), 'metrics'=>(array)($dashboard['metrics'] ?? [])];

        $model = (string)($bootstrap['modelName'] ?? ($bootstrap['title'] ?? 'Mimosa'));
        $deviceMode = (string)($bootstrap['mode'] ?? '');
        $metrics = [
            'vendor' => 'mimosa',
            'os' => 'MimosaOS',
            'model' => $model !== '' ? $model : 'Mimosa',
            'device_mode' => $deviceMode,
            'serial' => '',
            'firmware' => '',
            'temperature' => null,
            'signal' => null,
            'noise' => null,
            'ccq' => null,
            'quality' => $dashboard['metrics']['quality'] ?? null,
            'capacity' => $dashboard['metrics']['capacity'] ?? null,
            'clients' => (int)($fdb['count'] ?? 0),
            'cpu' => $dashboard['metrics']['cpu'] ?? null,
            'memory' => $dashboard['metrics']['memory'] ?? null,
            'uptime' => null,
            'tx_rate' => null,
            'rx_rate' => null,
            'channel' => null,
            'ethernet_port_speed' => null,
            'bytes_received' => null,
            'bytes_sent' => null,
            'bridge_count' => (int)($fdb['count'] ?? 0),
            'bridge_pages' => !empty($fdb['ok']) ? 1 : 0,
            'bridge_source' => !empty($fdb['ok']) ? (string)$fdb['source'] : '',
            'receiver_mac_count' => !empty($fdb['ok']) ? count((array)$fdb['macs']) : 0,
            'receiver_raw_mac_count' => !empty($fdb['ok']) ? count((array)$fdb['raw_macs']) : 0,
            '_transport' => 'curl',
            '_route' => ['protocol'=>$protocol, 'port'=>$port, 'mode'=>'mimosa_webui', 'vendor'=>'mimosa'],
            '_status_path' => '/',
            '_mimosa_webui' => true,
            '_mimosa_login_ok' => (bool)($login['ok'] ?? false),
            '_mimosa_fdb' => $fdb,
        ];
        return $metrics + ['_attempts'=>$attempts, '_raw_capture'=>$raw];
    }
    return ['_attempts'=>$attempts, '_raw_capture'=>$raw];
}
}

if (!function_exists('airmax_mimosa_try_fetch_details')) {
function airmax_mimosa_try_fetch_details(array $device, float $timeout=4.0): array {
    $device = airmax_build_device($device);
    // Mimosa C5x/B5 web UI exposes the needed FDB through authenticated AJAX:
    // /?q=diagnostics.fdb&mimosa_ajax=1 .  Try this first because it is the
    // same source used by Diagnostics > MAC Forwarding Table.
    $webui = airmax_mimosa_try_fetch_webui($device, $timeout);
    if (!empty($webui['_mimosa_webui'])) {
        return $webui;
    }

    $configuredProtocol = strtolower((string)($device['protocol'] ?? 'https')) === 'http' ? 'http' : 'https';
    $configuredPort = (int)($device['port'] ?? ($configuredProtocol === 'http' ? 80 : 443));
    $protocol = $configuredProtocol;
    $port = $configuredPort > 0 ? $configuredPort : ($protocol === 'http' ? 80 : 443);
    $attempts = [];
    $raw = [];

    $status = null;
    $selectedRoute = null;
    foreach (airmax_mimosa_candidate_routes($device) as $route) {
        $probe = airmax_mimosa_api_get($device, (string)$route['base'], '/core/api/service/status', $timeout, $attempts);
        if (!empty($probe['transport_dead'])) {
            // Do not waste time probing five REST endpoints on the same dead protocol/port.
            // Move once to the next standard management route.
            continue;
        }
        $status = $probe;
        $selectedRoute = $route;
        break;
    }

    if ($selectedRoute === null) {
        return [
            '_route' => ['protocol'=>$protocol, 'port'=>$port, 'mode'=>'mimosa_rest', 'vendor'=>'mimosa'],
            '_status_path' => '/core/api/service/status',
            '_attempts' => $attempts,
            '_raw_capture' => $raw,
        ];
    }

    $protocol = (string)$selectedRoute['protocol'];
    $port = (int)$selectedRoute['port'];
    $base = (string)$selectedRoute['base'];

    $info = airmax_mimosa_api_get($device, $base, '/core/api/service/device-info', $timeout, $attempts);
    $link = airmax_mimosa_api_get($device, $base, '/core/api/service/link-info', $timeout, $attempts);
    $eth = airmax_mimosa_api_get($device, $base, '/core/api/service/ethernet', $timeout, $attempts);
    if (empty($eth['values']) && empty($eth['transport_dead'])) {
        $eth = airmax_mimosa_api_get($device, $base, '/core/api/service/ethernet-conf', $timeout, $attempts);
    }

    $statusVals = (array)($status['values'] ?? []);
    $infoVals = (array)($info['values'] ?? []);
    $linkVals = (array)($link['values'] ?? []);
    $ethVals = (array)($eth['values'] ?? []);
    $hasAny = $statusVals || $infoVals || $linkVals || $ethVals;
    if (!$hasAny) {
        return [
            '_route' => ['protocol'=>$protocol, 'port'=>$port, 'mode'=>'mimosa_rest', 'vendor'=>'mimosa'],
            '_status_path' => '/core/api/service/status',
            '_attempts' => $attempts,
            '_raw_capture' => $raw,
        ];
    }

    $raw['mimosa_status_values'] = $statusVals;
    $raw['mimosa_device_info_values'] = $infoVals;
    $raw['mimosa_link_values'] = $linkVals;
    $raw['mimosa_ethernet_values'] = $ethVals;

    $model = (string)airmax_mimosa_value($infoVals, ['Model','DeviceModel','Product','DeviceName'], 'Mimosa');
    $deviceMode = (string)airmax_mimosa_value($infoVals, ['DeviceMode','WirelessMode','Mode'], '');
    $frequency = airmax_mimosa_value($linkVals, ['Frequency','Channel','Chains_1_2','Chains_3_4'], null);
    $clients = airmax_mimosa_value($statusVals + $linkVals, ['ConnectedClients','Clients','ClientCount','Stations','NumClients'], 0);
    $health = airmax_extract_live_health_metrics([
        'status'=>$statusVals,
        'device_info'=>$infoVals,
        'link'=>$linkVals,
        'ethernet'=>$ethVals,
    ]);

    $metrics = [
        'vendor' => 'mimosa',
        'os' => 'MimosaOS',
        'model' => $model,
        'device_mode' => $deviceMode,
        'serial' => (string)airmax_mimosa_value($infoVals, ['SerialNumber','Serial'], ''),
        'firmware' => (string)airmax_mimosa_value($infoVals, ['Version','Firmware','SoftwareVersion'], ''),
        'temperature' => airmax_scalar_or_null(airmax_mimosa_value($infoVals, ['Temperature'], null)),
        'signal' => airmax_scalar_or_null(airmax_mimosa_value($statusVals, ['SignalStrength','RSSI','RxSignal'], null)),
        'noise' => airmax_scalar_or_null(airmax_mimosa_value($statusVals, ['Noise','NoiseFloor'], null)),
        'ccq' => null,
        'quality' => $health['quality'],
        'capacity' => $health['capacity'] !== null
            ? $health['capacity']
            : airmax_scalar_or_null(airmax_mimosa_value($linkVals + $statusVals, ['MaxCapacity','Capacity'], null)),
        'clients' => is_numeric($clients) ? (int)$clients : 0,
        'cpu' => $health['cpu'],
        'memory' => $health['memory'],
        'uptime' => airmax_mimosa_value($statusVals + $infoVals, ['LinkUptime','Uptime','LastReboot'], null),
        'tx_rate' => airmax_scalar_or_null(airmax_mimosa_value($statusVals, ['TxRate','Tx_Phys_Rate','TxPhysRate'], null)),
        'rx_rate' => airmax_scalar_or_null(airmax_mimosa_value($statusVals, ['RxRate','Rx_Phys_Rate','RxPhysRate'], null)),
        'channel' => $frequency,
        'ethernet_port_speed' => airmax_mimosa_value($ethVals, ['PortSpeed','GigabitEthernetPort'], null),
        'bytes_received' => airmax_scalar_or_null(airmax_mimosa_value($linkVals, ['BytesReceived'], null)),
        'bytes_sent' => airmax_scalar_or_null(airmax_mimosa_value($linkVals, ['BytesSent'], null)),
        'bridge_count' => 0,
        'bridge_pages' => 0,
        'bridge_source' => '',
        '_transport' => 'curl',
        '_route' => ['protocol'=>$protocol, 'port'=>$port, 'mode'=>'mimosa_rest', 'vendor'=>'mimosa'],
        '_status_path' => '/core/api/service/status',
        '_mimosa_rest' => true,
    ];
    return $metrics + ['_attempts'=>$attempts, '_raw_capture'=>$raw];
}
}

if (!function_exists('airmax_status_body_is_airos8')) {
function airmax_status_body_is_airos8(string $body): bool {
    if ($body === '') return false;
    $j = json_decode($body, true);
    if (is_array($j)) {
        if (isset($j['chain_names'])) return true;
        $host = isset($j['host']) && is_array($j['host']) ? $j['host'] : [];
        $fw = strtolower((string)($host['fwversion'] ?? $host['firmware'] ?? $host['version'] ?? ''));
        $model = strtolower((string)($host['devmodel'] ?? $host['hostname'] ?? ''));
        if ($fw !== '' && (strpos($fw, 'v8') !== false || strpos($fw, 'wa.v8') !== false || strpos($fw, 'xc.v8') !== false)) return true;
        if ($model !== '' && (strpos($model, 'powerbeam 5ac') !== false || strpos($model, '5ac') !== false)) return true;
    }
    $low = strtolower($body);
    return (strpos($low, 'chain_names') !== false || strpos($low, 'wa.v8') !== false || strpos($low, 'xc.v8') !== false || strpos($low, 'powerbeam 5ac') !== false);
}
}

if (!function_exists('airmax_norm_mac')) {
function airmax_norm_mac($mac): string {
    $mac = strtoupper(trim((string)$mac));
    if ($mac === '') return '';
    $mac = str_replace(['-', '.', ' '], ['', '', ''], $mac);
    $mac = preg_replace('/[^0-9A-F]/', '', $mac);
    if (strlen($mac) !== 12) return '';
    if ($mac === '000000000000' || $mac === 'FFFFFFFFFFFF') return '';
    return implode(':', str_split($mac, 2));
}
}

if (!function_exists('airmax_unique_macs')) {
function airmax_unique_macs(array $macs): array {
    $out = [];
    foreach ($macs as $m) {
        $n = airmax_norm_mac($m);
        if ($n !== '') $out[$n] = true;
    }
    $list = array_keys($out);
    sort($list, SORT_STRING);
    return $list;
}
}

if (!function_exists('airmax_extract_macs_from_text')) {
function airmax_extract_macs_from_text(string $text): array {
    $out = [];
    if ($text === '') return [];
    if (preg_match_all('/(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}/i', $text, $m)) {
        foreach ($m[0] as $mac) $out[] = $mac;
    }
    if (preg_match_all('/\b[0-9A-F]{12}\b/i', $text, $m2)) {
        foreach ($m2[0] as $mac) $out[] = $mac;
    }
    return airmax_unique_macs($out);
}
}

if (!function_exists('airmax_decimal_oid_to_mac')) {
function airmax_decimal_oid_to_mac(string $oidTail): string {
    $parts = preg_split('/\./', trim($oidTail), -1, PREG_SPLIT_NO_EMPTY);
    if (count($parts) !== 6) return '';
    $hex = [];
    foreach ($parts as $p) {
        if (!ctype_digit($p)) return '';
        $n = (int)$p;
        if ($n < 0 || $n > 255) return '';
        $hex[] = sprintf('%02X', $n);
    }
    return implode(':', $hex);
}
}

if (!function_exists('airmax_row_port_is_lan0')) {
function airmax_row_port_is_lan0(string $port): bool {
    $p = strtolower(trim($port));
    return ($p === 'eth0' || $p === 'lan0' || strpos($p, 'eth0') !== false || strpos($p, 'lan0') !== false);
}
}


if (!function_exists('airmax_json_row_macs')) {
function airmax_json_row_macs(array $row): array {
    $macs = [];
    foreach (['mac','macaddr','mac_address','address','hwaddr','hw_addr','addr','sta_mac'] as $k) {
        if (isset($row[$k])) $macs[] = (string)$row[$k];
    }
    $encoded = json_encode($row, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    if (is_string($encoded)) {
        foreach (airmax_extract_macs_from_text($encoded) as $m) $macs[] = $m;
    }
    return airmax_unique_macs($macs);
}
}

if (!function_exists('airmax_split_bridge_json_macs')) {
function airmax_split_bridge_json_macs(array $rows): array {
    $raw = [];
    $client = [];
    foreach ($rows as $row) {
        if (!is_array($row)) continue;
        $rowMacs = airmax_json_row_macs($row);
        foreach ($rowMacs as $m) $raw[] = $m;
        $port = trim((string)($row['port'] ?? $row['interface'] ?? $row['ifname'] ?? $row['dev'] ?? $row['iface'] ?? ''));
        if ($port !== '' && airmax_row_port_is_lan0($port)) {
            foreach ($rowMacs as $m) $client[] = $m;
        }
    }
    return [
        'raw_macs' => airmax_unique_macs($raw),
        'client_macs' => airmax_unique_macs($client),
    ];
}
}

if (!function_exists('airmax_split_bridge_html_macs')) {
function airmax_split_bridge_html_macs(string $html): array {
    $raw = airmax_extract_macs_from_text($html);
    $client = [];
    if ($html !== '') {
        if (preg_match_all('/<tr[^>]*>(.*?)<\/tr>/is', $html, $rows)) {
            foreach ($rows[1] as $row) {
                if (stripos($row, 'LAN0') !== false || stripos($row, 'eth0') !== false) {
                    foreach (airmax_extract_macs_from_text($row) as $m) $client[] = $m;
                }
            }
        } else {
            foreach (preg_split('/\r\n|\r|\n/', $html) as $line) {
                if (stripos($line, 'LAN0') !== false || stripos($line, 'eth0') !== false) {
                    foreach (airmax_extract_macs_from_text($line) as $m) $client[] = $m;
                }
            }
        }
    }
    return [
        'raw_macs' => airmax_unique_macs($raw),
        'client_macs' => airmax_unique_macs($client),
    ];
}
}

if (!function_exists('airmax_try_bridge_lan0_json')) {
function airmax_try_bridge_lan0_json(array $device, float $timeout=2.4): array {
    $base = strtolower((string)$device['protocol']) . '://' . $device['host'] . ':' . (int)$device['port'];
    $cookie = airmax_cookiejar_file($device['host']);
    $resp = airmax_http($base . '/brmacs.cgi?brmacs=y', [
        'timeout'=>$timeout,
        'cookie'=>$cookie,
        'headers'=>['Accept: application/json, text/javascript, */*; q=0.01', 'X-Requested-With: XMLHttpRequest']
    ]);
    if ((int)$resp['status'] < 200 || (int)$resp['status'] >= 400 || trim((string)$resp['body']) === '') {
        return ['count'=>0, 'macs'=>[], 'raw_macs'=>[], 'source'=>'', 'raw'=>[], 'error'=>(string)($resp['error'] ?? '')];
    }
    $j = json_decode((string)$resp['body'], true);
    if (!is_array($j) || !isset($j['brmacs']) || !is_array($j['brmacs'])) {
        return ['count'=>0, 'macs'=>[], 'raw_macs'=>airmax_extract_macs_from_text((string)$resp['body']), 'source'=>'', 'raw'=>[], 'error'=>'invalid brmacs json'];
    }
    $split = airmax_split_bridge_json_macs((array)$j['brmacs']);
    $clientMacs = (array)$split['client_macs'];
    $rawMacs = (array)$split['raw_macs'];
    return [
        'count'=>count($clientMacs),
        'macs'=>$clientMacs,
        'raw_macs'=>$rawMacs,
        'raw_count'=>count($rawMacs),
        'source'=>'/brmacs.cgi?brmacs=y',
        'raw'=>[],
        'error'=>'',
    ];
}
}

if (!function_exists('airmax_macs_lan0_from_bridge_html')) {
function airmax_macs_lan0_from_bridge_html(string $html): array {
    $split = airmax_split_bridge_html_macs($html);
    return (array)($split['client_macs'] ?? []);
}
}

if (!function_exists('airmax_count_lan0_from_bridge_html')) {
function airmax_count_lan0_from_bridge_html(string $html): int {
    return count(airmax_macs_lan0_from_bridge_html($html));
}
}

if (!function_exists('airmax_try_bridge_lan0')) {
function airmax_try_bridge_lan0(array $device, float $timeout=2.4): array {
    $base = strtolower((string)$device['protocol']) . '://' . $device['host'] . ':' . (int)$device['port'];
    $cookie = airmax_cookiejar_file($device['host']);
    $paths = [
        '/brmacs.cgi',
        '/brmacs.cgi?br=br0',
        '/bridge.cgi',
        '/index.cgi',
    ];
    $bestRaw = [];
    $bestRawSource = '';
    foreach ($paths as $path) {
        $resp = airmax_http($base . $path, ['timeout'=>$timeout, 'cookie'=>$cookie]);
        if ((int)$resp['status'] >= 200 && (int)$resp['status'] < 400 && $resp['body'] !== '') {
            $split = airmax_split_bridge_html_macs((string)$resp['body']);
            $rawMacs = (array)($split['raw_macs'] ?? []);
            $clientMacs = (array)($split['client_macs'] ?? []);
            if (count($rawMacs) > count($bestRaw)) {
                $bestRaw = $rawMacs;
                $bestRawSource = $path;
            }
            if (count($clientMacs) > 0) {
                return [
                    'count'=>count($clientMacs),
                    'macs'=>$clientMacs,
                    'raw_macs'=>$rawMacs,
                    'raw_count'=>count($rawMacs),
                    'source'=>$path,
                    'raw'=>[],
                    'error'=>'',
                ];
            }
        }
    }
    return ['count'=>0, 'macs'=>[], 'raw_macs'=>$bestRaw, 'raw_count'=>count($bestRaw), 'source'=>$bestRawSource, 'raw'=>[], 'error'=>''];
}
}



if (!function_exists('airmax_try_snmp_lan0')) {
function airmax_try_snmp_lan0(array $device, int $timeoutMs=1800): array {
    if (!function_exists('snmp2_real_walk')) {
        return ['count'=>0, 'macs'=>[], 'source'=>'', 'raw'=>[], 'error'=>'snmp extension not loaded'];
    }

    if (function_exists('snmp_set_quick_print')) { @snmp_set_quick_print(true); }
    if (function_exists('snmp_set_enum_print')) { @snmp_set_enum_print(false); }
    if (function_exists('snmp_set_oid_numeric_print')) { @snmp_set_oid_numeric_print(true); }

    $host = (string)($device['host'] ?? '');
    $community = (string)($device['snmp_community'] ?? 'public');
    if ($host === '' || $community === '') {
        return ['count'=>0, 'macs'=>[], 'source'=>'', 'raw'=>[], 'error'=>'missing snmp host/community'];
    }

    $timeoutUs = max(300000, $timeoutMs * 1000);
    $retries = 0;

    $ifNameWalk = @snmp2_real_walk($host, $community, '.1.3.6.1.2.1.31.1.1.1.1', $timeoutUs, $retries);
    if (!is_array($ifNameWalk) || !$ifNameWalk) {
        $ifNameWalk = @snmp2_real_walk($host, $community, '.1.3.6.1.2.1.2.2.1.2', $timeoutUs, $retries);
    }
    $basePortWalk = @snmp2_real_walk($host, $community, '.1.3.6.1.2.1.17.1.4.1.2', $timeoutUs, $retries);
    $fdbPortWalk  = @snmp2_real_walk($host, $community, '.1.3.6.1.2.1.17.4.3.1.2', $timeoutUs, $retries);

    if (!is_array($basePortWalk) || !is_array($fdbPortWalk) || !$basePortWalk || !$fdbPortWalk) {
        return ['count'=>0, 'macs'=>[], 'source'=>'', 'raw'=>[], 'error'=>'snmp bridge tables unavailable'];
    }

    $parseInt = function($v): ?int {
        $v = trim((string)$v);
        if (preg_match('/(-?\d+)\s*$/', $v, $m)) return (int)$m[1];
        if (preg_match('/INTEGER:\s*(-?\d+)/i', $v, $m)) return (int)$m[1];
        return null;
    };
    $parseStr = function($v): string {
        $v = trim((string)$v);
        if (preg_match('/STRING:\s*"?([^"]*)"?$/i', $v, $m)) return trim($m[1]);
        if (preg_match('/=\s*"?([^"]*)"?$/', $v, $m)) return trim($m[1]);
        return trim($v, '" ');
    };

    $ifNamesByIndex = [];
    if (is_array($ifNameWalk)) {
        foreach ($ifNameWalk as $oid => $val) {
            if (preg_match('/\.([0-9]+)$/', (string)$oid, $m)) {
                $ifNamesByIndex[(int)$m[1]] = $parseStr($val);
            }
        }
    }

    $rawMacs = [];
    foreach ($fdbPortWalk as $oid => $_val) {
        if (preg_match('/\.([0-9]+(?:\.[0-9]+){5})$/', (string)$oid, $m)) {
            $mac = airmax_decimal_oid_to_mac($m[1]);
            if ($mac !== '') $rawMacs[] = $mac;
        }
    }

    $targetBasePorts = [];
    foreach ($basePortWalk as $oid => $val) {
        if (!preg_match('/\.([0-9]+)$/', (string)$oid, $m)) continue;
        $basePort = (int)$m[1];
        $ifIndex = $parseInt($val);
        if ($ifIndex === null) continue;
        $ifName = strtolower((string)($ifNamesByIndex[$ifIndex] ?? ''));
        if ($ifName !== '' && (strpos($ifName, 'eth0') !== false || strpos($ifName, 'lan0') !== false || strpos($ifName, 'ether') !== false || strpos($ifName, 'gigabit') !== false || strpos($ifName, 'ge') === 0)) {
            $targetBasePorts[$basePort] = $ifName;
        }
    }

    if (!$targetBasePorts) {
        return ['count'=>0, 'macs'=>[], 'source'=>'', 'raw'=>[], 'error'=>'snmp target interface not mapped'];
    }

    $macs = [];
    foreach ($fdbPortWalk as $oid => $val) {
        $port = $parseInt($val);
        if ($port === null || !isset($targetBasePorts[$port])) continue;
        if (preg_match('/\.([0-9]+(?:\.[0-9]+){5})$/', (string)$oid, $m)) {
            $mac = airmax_decimal_oid_to_mac($m[1]);
            if ($mac !== '') $macs[] = $mac;
        }
    }
    $macs = airmax_unique_macs($macs);

    return [
        'count' => count($macs),
        'macs' => $macs,
        'raw_macs' => airmax_unique_macs($rawMacs ?? []),
        'raw_count' => count(airmax_unique_macs($rawMacs ?? [])),
        'source' => 'snmp:dot1dTpFdbPort',
        'raw' => [],
        'error' => '',
    ];
}
}

if (!function_exists('airmax_do_classic_login')) {
function airmax_do_classic_login(array $device, string $base, float $timeout, array &$attempts): bool {
    $cookie = airmax_cookiejar_file($device['host']);
    $resp = airmax_http($base . '/login.cgi', [
        'timeout' => $timeout,
        'cookie' => $cookie,
        'post' => ['username'=>$device['username'], 'password'=>$device['password']],
        'headers' => ['Content-Type: application/x-www-form-urlencoded']
    ]);
    $attempts[] = ['step'=>'login classic','status'=>$resp['status'],'transport'=>$resp['transport'],'error'=>$resp['error'],'snippet'=>$resp['snippet']];
    return in_array((int)$resp['status'], [200,302,303], true) || $resp['error'] === '';
}
}

if (!function_exists('airmax_do_airos8_login')) {
function airmax_do_airos8_login(array $device, string $base, float $timeout, array &$attempts): bool {
    $cookie = airmax_cookiejar_file($device['host']);
    $payload = json_encode(['username'=>$device['username'], 'password'=>$device['password']]);
    $resp = airmax_http($base . '/api/auth', [
        'timeout' => $timeout,
        'cookie' => $cookie,
        'post' => $payload,
        'headers' => ['Content-Type: application/json']
    ]);
    $attempts[] = ['step'=>'login airos8 + /api/auth','status'=>$resp['status'],'transport'=>$resp['transport'],'error'=>$resp['error'],'snippet'=>$resp['snippet']];
    return in_array((int)$resp['status'], [200,201,204,302,303], true) || stripos((string)$resp['body'], 'token') !== false || stripos((string)$resp['body'], 'csrf') !== false;
}
}

if (!function_exists('airmax_try_fetch_details')) {
function airmax_try_fetch_details(array $device, float $timeout=45.0): array {
    $device = airmax_build_device($device);
    $protocol = strtolower((string)$device['protocol']);
    $port = (int)$device['port'];
    $base = $protocol . '://' . $device['host'] . ':' . $port;
    $preferredVendor = function_exists('airmax_preferred_vendor') ? airmax_preferred_vendor($device['preferred_vendor'] ?? 'auto') : 'auto';
    if ($preferredVendor === 'mimosa') {
        return airmax_mimosa_try_fetch_details($device, $timeout);
    }
    $cookie = airmax_cookiejar_file($device['host']);
    $attempts = [];
    $raw = [];
    $metrics = [];

    // First try direct status
    $resp = airmax_http($base . '/status.cgi', ['timeout'=>$timeout, 'cookie'=>$cookie]);
    $attempts[] = ['step'=>'probe /status.cgi','status'=>$resp['status'],'transport'=>$resp['transport'],'error'=>$resp['error'],'snippet'=>$resp['snippet']];
    $transportDead = function_exists('airmax_http_transport_dead') ? airmax_http_transport_dead($resp) : false;
    if ((int)$resp['status'] === 200) {
        $metrics = airmax_parse_status_body($resp['body']);
        if ($metrics) {
            $isAiros8 = airmax_status_body_is_airos8((string)$resp['body']);
            if ($isAiros8) {
                // WA.v8 / airOS 8 devices may expose /status.cgi without an authenticated
                // bridge session.  Authenticate here before the MAC-table probe runs;
                // otherwise brmacs stays empty while the device status looks successful.
                airmax_do_airos8_login($device, $base, max(2.0, $timeout), $attempts);
            }
            $metrics['_route'] = ['protocol'=>$protocol,'port'=>$port,'mode'=>($isAiros8 ? 'airos8' : 'classic')];
            $metrics['_status_path'] = '/status.cgi';
            return $metrics + ['_attempts'=>$attempts, '_raw_capture'=>$raw];
        }
    }

    // 302 = classic login required
    if ((int)$resp['status'] === 302 || stripos($resp['snippet'], 'login') !== false) {
        if (airmax_do_classic_login($device, $base, max(2.0, $timeout), $attempts)) {
            $resp2 = airmax_http($base . '/status.cgi', ['timeout'=>$timeout, 'cookie'=>$cookie]);
            $attempts[] = ['step'=>'probe /status.cgi','status'=>$resp2['status'],'transport'=>$resp2['transport'],'error'=>$resp2['error'],'snippet'=>$resp2['snippet']];
            if ((int)$resp2['status'] === 200) {
                $metrics = airmax_parse_status_body($resp2['body']);
                if ($metrics) {
                    $metrics['_route'] = ['protocol'=>$protocol,'port'=>$port,'mode'=>'classic'];
                    $metrics['_status_path'] = '/status.cgi';
                    return $metrics + ['_attempts'=>$attempts, '_raw_capture'=>$raw];
                }
            }
        }
    }

    // 403 / JSON auth / airos8 fallback
    if ((int)$resp['status'] === 403 || stripos($resp['snippet'], 'forbidden') !== false || stripos($resp['body'], 'chain_names') !== false) {
        if (airmax_do_airos8_login($device, $base, max(2.0, $timeout), $attempts)) {
            foreach (['/status.cgi','/amdata.cgi'] as $path) {
                $resp3 = airmax_http($base . $path, ['timeout'=>$timeout, 'cookie'=>$cookie]);
                $attempts[] = ['step'=>'probe ' . $path,'status'=>$resp3['status'],'transport'=>$resp3['transport'],'error'=>$resp3['error'],'snippet'=>$resp3['snippet']];
                if ((int)$resp3['status'] === 200) {
                    $metrics = airmax_parse_status_body($resp3['body']);
                    if ($metrics) {
                        $metrics['_route'] = ['protocol'=>$protocol,'port'=>$port,'mode'=>'airos8'];
                        $metrics['_status_path'] = $path;
                        return $metrics + ['_attempts'=>$attempts, '_raw_capture'=>$raw];
                    }
                }
            }
        }
    }

    // Last fallback: try route cache mode if exists and not already tried
    $cached = airmax_route_cache_get($device);
    if (is_array($cached) && isset($cached['mode']) && $cached['mode'] !== '') {
        $mode = (string)$cached['mode'];
        $cachedVendor = function_exists('airmax_preferred_vendor') ? airmax_preferred_vendor($cached['vendor'] ?? '') : '';
        if ((($mode === 'mimosa_rest' || $mode === 'mimosa_webui') || $cachedVendor === 'mimosa') && !$transportDead) {
            $mimosa = airmax_mimosa_try_fetch_details($device, $timeout);
            if ((!empty($mimosa['_mimosa_rest']) || !empty($mimosa['_mimosa_webui'])) && (isset($mimosa['model']) || isset($mimosa['signal']) || isset($mimosa['clients']))) {
                return $mimosa;
            }
            $attempts = array_merge($attempts, (array)($mimosa['_attempts'] ?? []));
            $raw['mimosa_rest'] = (array)($mimosa['_raw_capture'] ?? []);
        } elseif ($mode === 'airos8') {
            if (airmax_do_airos8_login($device, $base, max(2.0, $timeout), $attempts)) {
                $resp4 = airmax_http($base . '/status.cgi', ['timeout'=>$timeout, 'cookie'=>$cookie]);
                $attempts[] = ['step'=>'probe /status.cgi','status'=>$resp4['status'],'transport'=>$resp4['transport'],'error'=>$resp4['error'],'snippet'=>$resp4['snippet']];
                if ((int)$resp4['status'] === 200) {
                    $metrics = airmax_parse_status_body($resp4['body']);
                    if ($metrics) {
                        $metrics['_route'] = ['protocol'=>$protocol,'port'=>$port,'mode'=>'airos8'];
                        $metrics['_status_path'] = '/status.cgi';
                        return $metrics + ['_attempts'=>$attempts, '_raw_capture'=>$raw];
                    }
                }
            }
        } else {
            if (airmax_do_classic_login($device, $base, max(2.0, $timeout), $attempts)) {
                $resp5 = airmax_http($base . '/status.cgi', ['timeout'=>$timeout, 'cookie'=>$cookie]);
                $attempts[] = ['step'=>'probe /status.cgi','status'=>$resp5['status'],'transport'=>$resp5['transport'],'error'=>$resp5['error'],'snippet'=>$resp5['snippet']];
                if ((int)$resp5['status'] === 200) {
                    $metrics = airmax_parse_status_body($resp5['body']);
                    if ($metrics) {
                        $metrics['_route'] = ['protocol'=>$protocol,'port'=>$port,'mode'=>'classic'];
                        $metrics['_status_path'] = '/status.cgi';
                        return $metrics + ['_attempts'=>$attempts, '_raw_capture'=>$raw];
                    }
                }
            }
        }
    }

    if ($preferredVendor === 'auto' && !$transportDead) {
        $mimosa = airmax_mimosa_try_fetch_details($device, $timeout);
        if ((!empty($mimosa['_mimosa_rest']) || !empty($mimosa['_mimosa_webui'])) && (isset($mimosa['model']) || isset($mimosa['signal']) || isset($mimosa['clients']))) {
            return $mimosa;
        }
        $attempts = array_merge($attempts, (array)($mimosa['_attempts'] ?? []));
        $raw['mimosa_rest'] = (array)($mimosa['_raw_capture'] ?? []);
    }

    return [
        '_route' => ['protocol'=>$protocol,'port'=>$port,'mode'=>''],
        '_status_path' => '/status.cgi',
        '_attempts' => $attempts,
        '_raw_capture' => $raw,
    ];
}
}

if (!function_exists('airmax_probe_device')) {
function airmax_probe_device($device, float $timeout=45.0): array {
    $d = airmax_build_device($device);
    $metrics = airmax_try_fetch_details($d, $timeout);
    $hasInitialMetrics = isset($metrics['model']) || isset($metrics['signal']) || isset($metrics['clients']) || isset($metrics['cpu']);
    $preferredVendor = function_exists('airmax_preferred_vendor')
        ? airmax_preferred_vendor($d['preferred_vendor'] ?? 'auto')
        : 'auto';

    // Ubiquiti/auto devices used to be pinned permanently to the configured
    // protocol after a failed scan. Probe the other standard management route
    // before declaring the device down. Mimosa already performs this route
    // discovery internally.
    if (!$hasInitialMetrics && $preferredVendor !== 'mimosa') {
        $firstAttempts = (array)($metrics['_attempts'] ?? []);
        $firstRaw = (array)($metrics['_raw_capture'] ?? []);
        $candidates = [['http', 80], ['https', 443]];
        foreach ($candidates as $candidate) {
            [$candidateProtocol, $candidatePort] = $candidate;
            if ($candidateProtocol === (string)$d['protocol'] && $candidatePort === (int)$d['port']) continue;
            $alternate = $d;
            $alternate['protocol'] = $candidateProtocol;
            $alternate['port'] = $candidatePort;
            $alternateMetrics = airmax_try_fetch_details($alternate, $timeout);
            $firstAttempts = array_merge($firstAttempts, (array)($alternateMetrics['_attempts'] ?? []));
            $firstRaw[$candidateProtocol . ':' . $candidatePort] = (array)($alternateMetrics['_raw_capture'] ?? []);
            $alternateHasMetrics = isset($alternateMetrics['model']) || isset($alternateMetrics['signal']) || isset($alternateMetrics['clients']) || isset($alternateMetrics['cpu']);
            if ($alternateHasMetrics) {
                $metrics = $alternateMetrics;
                $metrics['_attempts'] = $firstAttempts;
                $metrics['_raw_capture'] = $firstRaw;
                break;
            }
            $metrics['_attempts'] = $firstAttempts;
            $metrics['_raw_capture'] = $firstRaw;
        }
    }
    $attempts = (array)($metrics['_attempts'] ?? []);
    $rawCapture = (array)($metrics['_raw_capture'] ?? []);
    $route = (array)($metrics['_route'] ?? ['protocol'=>$d['protocol'],'port'=>$d['port'],'mode'=>'']);
    $statusPath = (string)($metrics['_status_path'] ?? '/status.cgi');

    $bridge = ['bridge_count'=>0,'raw_mac_count'=>0,'bridge_pages'=>0,'bridge_source'=>'','client_count_method'=>null,'bridge_path'=>'','raw'=>[],'macs'=>[],'raw_macs'=>[]];
    $hasMetrics = isset($metrics['model']) || isset($metrics['signal']) || isset($metrics['clients']) || isset($metrics['cpu']);
    $isStation = airmax_device_is_station($d);
    if ($hasMetrics && !$isStation) {
        // Access/Sector devices are monitored for reachability only.
        // Subscriber MAC matching must never run on Access/Sector tables because they can see network-wide MACs.
        $metrics['clients'] = 0;
        $metrics['bridge_count'] = 0;
        $metrics['receiver_mac_count'] = 0;
        $metrics['bridge_source'] = '';
        $metrics['_bridge_path'] = '';
        $bridge = ['bridge_count'=>0,'raw_mac_count'=>0,'bridge_pages'=>0,'bridge_source'=>'','client_count_method'=>'access_skipped','bridge_path'=>'','raw'=>[],'macs'=>[],'raw_macs'=>[],'skipped'=>true,'reason'=>'access_device'];
    } elseif ($hasMetrics) {
        $metricVendor = function_exists('airmax_preferred_vendor') ? airmax_preferred_vendor($metrics['vendor'] ?? ($route['vendor'] ?? '')) : '';
        $mimosaFdb = ($metricVendor === 'mimosa' && isset($metrics['_mimosa_fdb']) && is_array($metrics['_mimosa_fdb'])) ? $metrics['_mimosa_fdb'] : [];

        if ($metricVendor === 'mimosa' && !empty($mimosaFdb['ok'])) {
            // Use the exact table used by Mimosa UI: Diagnostics > MAC Forwarding Table.
            // Only Ethernet entries are promoted to subscriber matching; wireless/uplink
            // entries stay in raw_macs for diagnostics only.
            $metrics['clients'] = (int)($mimosaFdb['count'] ?? 0);
            $metrics['bridge_count'] = (int)($mimosaFdb['count'] ?? 0);
            $metrics['receiver_mac_count'] = count((array)($mimosaFdb['macs'] ?? []));
            $metrics['receiver_raw_mac_count'] = count((array)($mimosaFdb['raw_macs'] ?? []));
            $metrics['bridge_source'] = (string)($mimosaFdb['source'] ?? '/?q=diagnostics.fdb&mimosa_ajax=1');
            $metrics['_bridge_path'] = (string)($mimosaFdb['source'] ?? '/?q=diagnostics.fdb&mimosa_ajax=1');
            $bridge = [
                'bridge_count'=>(int)($mimosaFdb['count'] ?? 0),
                'raw_mac_count'=>(int)($mimosaFdb['raw_count'] ?? count((array)($mimosaFdb['raw_macs'] ?? []))),
                'bridge_pages'=>1,
                'bridge_source'=>(string)($mimosaFdb['source'] ?? '/?q=diagnostics.fdb&mimosa_ajax=1'),
                'client_count_method'=>'mimosa_ajax_fdb_ethernet_port4',
                'ownership_policy'=>(string)($mimosaFdb['ownership_policy'] ?? 'mimosa_fdb_port4_ethernet_all_ages'),
                'bridge_path'=>(string)($mimosaFdb['source'] ?? '/?q=diagnostics.fdb&mimosa_ajax=1'),
                'raw'=>(array)($mimosaFdb['raw'] ?? []),
                'macs'=>array_values((array)($mimosaFdb['macs'] ?? [])),
                'raw_macs'=>array_values((array)($mimosaFdb['raw_macs'] ?? [])),
            ];
        } else {
            $b = [];
            if ($metricVendor !== 'mimosa') {
                $b = airmax_try_bridge_lan0_json($d, min(45.0, $timeout));
                if ((int)$b['count'] <= 0) {
                    $b = airmax_try_bridge_lan0($d, min(45.0, $timeout));
                }
            }
            if ((int)($b['count'] ?? 0) > 0) {
                $metrics['clients'] = (int)$b['count'];
                $metrics['bridge_count'] = (int)$b['count'];
                $metrics['receiver_mac_count'] = count((array)($b['macs'] ?? []));
                $metrics['receiver_raw_mac_count'] = count((array)($b['raw_macs'] ?? []));
                $metrics['bridge_source'] = (string)$b['source'];
                $metrics['_bridge_path'] = (string)$b['source'];
                $bridge = [
                    'bridge_count'=>(int)$b['count'],
                    'raw_mac_count'=>(int)($b['raw_count'] ?? count((array)($b['raw_macs'] ?? []))),
                    'bridge_pages'=>1,
                    'bridge_source'=>(string)$b['source'],
                    'client_count_method'=>((string)$b['source'] === '/brmacs.cgi?brmacs=y' ? 'brmacs_json_eth0' : 'lan0'),
                    'bridge_path'=>(string)$b['source'],
                    'raw'=>(array)$b['raw'],
                    'macs'=>array_values((array)($b['macs'] ?? [])),
                    'raw_macs'=>array_values((array)($b['raw_macs'] ?? [])),
                ];
            } else {
                $usedSnmp = false;
                $shouldTrySnmp = (($metrics['os'] ?? '') === 'airOSClassic') || $metricVendor === 'mimosa' || (($route['mode'] ?? '') === 'mimosa_rest');
                if ($shouldTrySnmp) {
                    $s = airmax_try_snmp_lan0($d, 1800);
                    if ((int)($s['count'] ?? 0) > 0) {
                        $metrics['clients'] = (int)$s['count'];
                        $metrics['bridge_count'] = (int)$s['count'];
                        $metrics['receiver_mac_count'] = count((array)($s['macs'] ?? []));
                        $metrics['receiver_raw_mac_count'] = count((array)($s['raw_macs'] ?? []));
                        $metrics['bridge_source'] = (string)$s['source'];
                        $metrics['_bridge_path'] = (string)$s['source'];
                        $bridge = [
                            'bridge_count'=>(int)$s['count'],
                            'raw_mac_count'=>(int)($s['raw_count'] ?? count((array)($s['raw_macs'] ?? []))),
                            'bridge_pages'=>1,
                            'bridge_source'=>(string)$s['source'],
                            'client_count_method'=>($metricVendor === 'mimosa' ? 'snmp_ethernet_mimosa' : 'snmp_lan0'),
                            'bridge_path'=>(string)$s['source'],
                            'raw'=>(array)($s['raw'] ?? []),
                            'macs'=>array_values((array)($s['macs'] ?? [])),
                            'raw_macs'=>array_values((array)($s['raw_macs'] ?? [])),
                        ];
                        $usedSnmp = true;
                    }
                }
                if (!$usedSnmp) {
                    $metrics['bridge_count'] = (int)($metrics['bridge_count'] ?? 0);
                    $metrics['bridge_source'] = (string)($metrics['bridge_source'] ?? '');
                    $method = 'wireless_count';
                    if ($metricVendor === 'mimosa') {
                        $method = !empty($metrics['_mimosa_login_ok']) ? 'mimosa_webui_without_fdb' : 'mimosa_login_failed';
                    }
                    $bridge = [
                        'bridge_count'=>(int)($metrics['bridge_count'] ?? 0),
                        'raw_mac_count'=>(int)($b['raw_count'] ?? count((array)($b['raw_macs'] ?? []))),
                        'bridge_pages'=>0,
                        'bridge_source'=>(string)($metrics['bridge_source'] ?? ''),
                        'client_count_method'=>$method,
                        'bridge_path'=>'',
                        'raw'=>[],
                        'macs'=>[],
                        'raw_macs'=>array_values((array)($b['raw_macs'] ?? [])),
                    ];
                }
            }
        }
    }
    $latency = null;
    foreach ($attempts as $a) {
        if (($a['status'] ?? 0) === 200) { $latency = null; break; }
    }
    $detailParts = [];
    if (isset($metrics['signal']) && $metrics['signal'] !== null) $detailParts[] = 'Signal: ' . $metrics['signal'] . ' dBm';
    if (isset($metrics['ccq']) && $metrics['ccq'] !== null) $detailParts[] = 'CCQ: ' . $metrics['ccq'] . '%';
    if (isset($metrics['quality']) && $metrics['quality'] !== null) $detailParts[] = 'Quality: ' . $metrics['quality'] . '%';
    if (isset($metrics['capacity']) && $metrics['capacity'] !== null) $detailParts[] = 'Capacity: ' . $metrics['capacity'] . '%';
    if (isset($metrics['clients'])) $detailParts[] = 'Clients: ' . (int)$metrics['clients'];
    if (!empty($metrics['_transport'])) $detailParts[] = 'Transport: ' . $metrics['_transport'];

    return [
        'id' => (int)$d['id'],
        'name' => (string)$d['name'],
        'type' => airmax_device_role((string)$d['device_type']),
        'host' => (string)$d['host'],
        'protocol' => strtolower((string)$route['protocol']),
        'port' => (int)$route['port'],
        'state' => $hasMetrics ? 'UP' : 'DOWN',
        'up' => $hasMetrics,
        'latency_ms' => $latency,
        'reachable' => $hasMetrics,
        'details' => implode(' | ', $detailParts),
        'metrics' => array_diff_key($metrics, array_flip(['_attempts','_raw_capture'])),
        'attempts' => $attempts,
        'raw_capture' => $rawCapture,
        'bridge' => $bridge,
    ];
}
}

if (!function_exists('airmax_probe_and_persist')) {
function airmax_probe_and_persist($db, int $id): array {
    if (!($db instanceof mysqli)) {
        return ['ok'=>false,'status'=>'failed','message'=>'DB not ready','locked'=>false];
    }
    airmax_ensure_probe_schema($db);
    $id = (int)$id;
    $q = @$db->query("SELECT * FROM airmax_devices WHERE id={$id} LIMIT 1");
    $row = $q ? $q->fetch_assoc() : null;
    if (!$row) return ['ok'=>false,'status'=>'failed','message'=>'device not found','locked'=>false];

    $device = airmax_build_device($row);
    $probe = airmax_probe_device($device, 45.0);
    $metrics = is_array($probe['metrics'] ?? null) ? $probe['metrics'] : [];
    $route = is_array($metrics['_route'] ?? null) ? $metrics['_route'] : ['protocol'=>$device['protocol'],'port'=>$device['port'],'mode'=>''];
    $mode = (string)($route['mode'] ?? '');
    $protocol = strtolower((string)($route['protocol'] ?? $device['protocol']));
    $port = (int)($route['port'] ?? $device['port']);
    $vendor = function_exists('airmax_preferred_vendor') ? airmax_preferred_vendor($metrics['vendor'] ?? ($route['vendor'] ?? ($device['preferred_vendor'] ?? 'auto'))) : (string)($metrics['vendor'] ?? 'auto');
    if ($vendor === 'auto') {
        if (($mode === 'mimosa_rest' || $mode === 'mimosa_webui')) {
            $vendor = 'mimosa';
        } elseif (!empty($probe['up'])) {
            $vendor = 'ubiquiti';
        }
    }
    $os = (string)($metrics['os'] ?? ($vendor === 'mimosa' ? 'MimosaOS' : ($mode === 'airos8' ? 'airOS' : (($vendor === 'ubiquiti' && !empty($probe['up'])) ? 'airOSClassic' : ''))));
    $display = strtoupper($protocol) . ':' . $port . ($mode !== '' ? ' | ' . $mode : '');
    $status = !empty($probe['up']) ? 'success' : 'failed';
    $locked = !empty($probe['up']) ? 1 : 0;
    $message = !empty($probe['up']) ? 'تم الجلب وتثبيت المسار الناجح تلقائياً' : 'فشل الاتصال بواجهة الفحص';
    $transportBlocked = $status === 'failed'
        && function_exists('airmax_attempts_transport_dead')
        && airmax_attempts_transport_dead((array)($probe['attempts'] ?? []));
    if ($transportBlocked) {
        $message = 'تعذر على خادم البرنامج الوصول إلى شبكة الأجهزة. افصل VPN/Proton فعلياً أو استثنِ شبكة الأجهزة، ثم أعد الفحص؛ لم يتم تغيير المسار المحفوظ.';
    }
    $clientMethod = (string)(($probe['bridge']['client_count_method'] ?? '') ?: 'wireless_count');
    if (!empty($probe['up']) && $vendor === 'mimosa' && airmax_device_is_station($device)) {
        if ($clientMethod === 'mimosa_ajax_fdb_ethernet') {
            $message = 'تم جلب جدول MAC Forwarding من Mimosa وتثبيت المسار الناجح تلقائياً';
        } elseif ($clientMethod === 'mimosa_login_failed') {
            $message = 'تم الوصول إلى واجهة Mimosa، لكن فشل تسجيل الدخول. تحقق من كلمة مرور الجهاز ثم أعد الفحص.';
        } elseif ($clientMethod === 'mimosa_webui_without_fdb' || $clientMethod === 'mimosa_rest_status_without_fdb') {
            $message = 'تم الوصول إلى واجهة Mimosa، لكن لم يتم استخراج MACات Ethernet من جدول MAC Forwarding بعد.';
        }
    }
    $lastError = '';
    foreach ((array)($probe['attempts'] ?? []) as $a) {
        if (!empty($a['error'])) $lastError = (string)$a['error'];
    }
    $bridgePath = (string)($probe['bridge']['bridge_path'] ?? '');
    $statusPath = (string)($metrics['_status_path'] ?? '/status.cgi');
    $metaJson = json_encode([
        'host'=>$device['host'],
        'protocol'=>$protocol,
        'port'=>$port,
        'mode'=>$mode,
        'vendor'=>$vendor,
        'os'=>$os
    ], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    $rawJson = json_encode((array)($probe['raw_capture'] ?? []), JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);

    $esc = fn($v) => "'" . $db->real_escape_string((string)$v) . "'";
    $routeAssignments = $status === 'success'
        ? "detected_protocol=".$esc($protocol).", "
          . "detected_port=".$port.", "
          . "detected_vendor=".$esc($vendor).", "
          . "detected_os=".$esc($os).", "
          . "detected_mode=".$esc($mode).", "
          . "detected_route=".$esc($display).", "
        : '';
    $configuredRouteAssignments = $status === 'success'
        ? "protocol=".$esc($protocol).", port=".$port.", "
        : '';
    $sql = "UPDATE airmax_devices SET "
        . $routeAssignments
        . "probe_status=".$esc($status).", "
        . "probe_progress='100', "
        . "probe_message=".$esc($message).", "
        . "probe_last_error=".$esc($lastError).", "
        . "probe_meta_json=".$esc($metaJson).", "
        . "probe_raw_capture_json=".$esc($rawJson).", "
        . "probe_locked=".$locked.", "
        . $configuredRouteAssignments
        . "client_count_method=".$esc($clientMethod).", "
        . "bridge_path=".$esc($bridgePath).", "
        . "status_path=".$esc($statusPath).", "
        . "probe_last_error_at=".($status==='failed' ? 'NOW()' : 'probe_last_error_at').", "
        . "probe_last_success_at=".($status==='success' ? 'NOW()' : 'probe_last_success_at')." "
        . "WHERE id=".$id;
    @$db->query($sql);

    if ($status === 'success') {
        airmax_route_cache_set($device, ['protocol'=>$protocol,'port'=>$port,'mode'=>$mode,'vendor'=>$vendor,'os'=>$os]);
    }
    $responseOut = [
        'ok'=>$status==='success',
        'status'=>$status,
        'message'=>$message,
        'locked'=>(bool)$locked,
        'route'=>['display'=>$display,'vendor'=>$vendor,'os'=>$os,'mode'=>$mode],
        'probe'=>$probe,
        'bridge'=>$probe['bridge'] ?? ['bridge_count'=>0,'bridge_pages'=>0,'bridge_source'=>'','client_count_method'=>$clientMethod,'bridge_path'=>$bridgePath,'raw'=>[]],
        'transport_blocked'=>$transportBlocked,
    ];
    $existingResult = airmax_json_read(airmax_result_cache_path($device), null);
    $hasSuccessfulCache = is_array($existingResult) && !empty($existingResult['out']['ok']);
    if (!$transportBlocked || !$hasSuccessfulCache || $status === 'success') {
        airmax_json_write(airmax_result_cache_path($device), [
        'ts'=>time(),
        'out'=>$responseOut,
        ]);
    }

    // store lightweight live cache
    $payload = json_encode([
        'id'=>$id,
        'host'=>$device['host'],
        'name'=>$device['name'],
        'state'=>$probe['state'] ?? 'DOWN',
        'metrics'=>$probe['metrics'] ?? [],
        'updated_at'=>time()
    ], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    if (!$transportBlocked || $status === 'success') {
        @$db->query("REPLACE INTO airmax_live_cache(device_id,payload,updated_at) VALUES ($id,'".$db->real_escape_string($payload)."',NOW())");
    }
    $rawEventParts = [
        'status=' . $status,
        'device=' . (string)$device['name'],
        'host=' . (string)$device['host'],
        'route=' . $display,
        'vendor=' . $vendor,
        'os=' . $os,
        'client_count_method=' . $clientMethod,
    ];
    if ($bridgePath !== '') $rawEventParts[] = 'bridge_path=' . $bridgePath;
    if ($lastError !== '') $rawEventParts[] = 'last_error=' . $lastError;
    if ((string)($probe['details'] ?? '') !== '') $rawEventParts[] = 'details=' . (string)$probe['details'];
    airmax_device_log_event($db, $device, 'DEVICE_PROBE', $status, implode('; ', $rawEventParts), [
        'message' => $message,
        'route' => $display,
        'protocol' => $protocol,
        'port' => $port,
        'mode' => $mode,
        'vendor' => $vendor,
        'os' => $os,
        'last_error' => $lastError,
        'client_count_method' => $clientMethod,
        'bridge_path' => $bridgePath,
        'up' => !empty($probe['up']),
    ], 'airmax_probe');
    if (function_exists('airmax_invalidate_noc_card_cache')) airmax_invalidate_noc_card_cache();

    return $responseOut;
}
}

if (!function_exists('airmax_probe_all')) {
function airmax_probe_all($db): array {
    if (!($db instanceof mysqli)) return ['items'=>[],'summary'=>['total'=>0,'down'=>0,'total_stations'=>0]];
    airmax_ensure_probe_schema($db);
    $devices = airmax_get_all($db, true);
    $items = [];
    $live = airmax_json_read(airmax_live_cache_path(), ['items'=>[], 'next_idx'=>0, 'updated_at'=>0]);
    if (!is_array($live)) $live = ['items'=>[], 'next_idx'=>0, 'updated_at'=>0];
    foreach ($devices as $d) {
        $id = (int)($d['id'] ?? 0);
        $cache = airmax_json_read(airmax_result_cache_path($d), null);
        if (is_array($cache) && isset($cache['out']['probe'])) {
            $p = $cache['out']['probe'];
            $items[$id] = [
                'id'=>$id, 'host'=>$d['host'], 'name'=>$d['name'], 'type'=>airmax_device_role($d['device_type'] ?? ''),
                'state'=>$p['state'] ?? 'DOWN',
                'metrics'=>$p['metrics'] ?? [],
                'bridge'=>$p['bridge'] ?? ($cache['out']['bridge'] ?? [])
            ];
        } else {
            $items[$id] = [
                'id'=>$id,'host'=>$d['host'],'name'=>$d['name'], 'type'=>airmax_device_role($d['device_type'] ?? ''),
                'state'=>'WARN','metrics'=>[], 'bridge'=>[]
            ];
        }
    }

    // Refresh one device per call max
    $count = count($devices);
    if ($count > 0) {
        $idx = (int)($live['next_idx'] ?? 0);
        $target = $devices[$idx % $count];
        $rid = (int)($target['id'] ?? 0);
        $cached = airmax_json_read(airmax_result_cache_path($target), null);
        $age = is_array($cached) ? (time() - (int)($cached['ts'] ?? 0)) : 999999;
        if ($age > 30) {
            $out = airmax_probe_and_persist($db, $rid);
            if (isset($out['probe'])) {
                $items[$rid] = [
                    'id'=>$rid,'host'=>$target['host'],'name'=>$target['name'], 'type'=>airmax_device_role($target['device_type'] ?? ''),
                    'state'=>$out['probe']['state'] ?? 'DOWN',
                    'metrics'=>$out['probe']['metrics'] ?? [],
                    'bridge'=>$out['probe']['bridge'] ?? ($out['bridge'] ?? [])
                ];
            }
        }
        $live['next_idx'] = $idx + 1;
        $live['updated_at'] = time();
        airmax_json_write(airmax_live_cache_path(), $live);
    }

    $list = array_values($items);
    $down = 0;
    $stations = 0;
    foreach ($list as $it) {
        $st = strtoupper((string)($it['state'] ?? ''));
        if (in_array($st, ['DOWN','OFFLINE'], true)) $down++;
        if (airmax_device_is_station($it['type'] ?? '')) $stations += (int)($it['metrics']['clients'] ?? 0);
    }
    return ['items'=>$list,'summary'=>['total'=>count($list),'down'=>$down,'total_stations'=>$stations]];
}
}
