<?php
declare(strict_types=1);

/**
 * On-demand remote access control plane (heartbeat type: ra).
 * v1: WireGuard + reverse SSH (RSSH) opt-in from device (lab / alpha).
 */
final class RemoteAccessRepository {
    public const OPT_NONE = 'none';
    public const OPT_WIREGUARD = 'wireguard';
    public const OPT_RSSH = 'rssh';

    public const STATUS_PENDING = 'pending';
    public const STATUS_ACTIVE = 'active';
    public const STATUS_INACTIVE = 'inactive';
    public const STATUS_CLOSED = 'closed';
    public const STATUS_CLOSED_TIMEOUT = 'closed_timeout';

    private static bool $schemaReady = false;

    public static function ensureSchema(): void {
        if (self::$schemaReady) {
            return;
        }
        $pdo = Database::pdo();
        if (!Database::tableExists($pdo, 'remote_access_devices')) {
            self::createTables($pdo);
            self::$schemaReady = true;
            return;
        }
        self::ensureSessionColumns($pdo);
        self::ensureRsshColumns($pdo);
        self::ensureDeviceMetaColumn($pdo);
        self::ensureLastClientIpColumn($pdo);
        self::$schemaReady = true;
    }

    private static function addColumnIfMissing(PDO $pdo, string $table, string $column, string $ddl): void {
        if (!Database::tableExists($pdo, $table)) {
            return;
        }
        $names = Database::columnNames($pdo, $table);
        if (in_array($column, $names, true)) {
            return;
        }
        $pdo->exec('ALTER TABLE ' . $table . ' ADD COLUMN ' . $column . ' ' . $ddl);
    }

    private static function ensureLastClientIpColumn(PDO $pdo): void {
        $col = Database::isMysql() ? 'VARCHAR(64) NULL' : 'TEXT NULL';
        self::addColumnIfMissing($pdo, 'remote_access_devices', 'last_client_ip', $col);
    }

    private static function ensureDeviceMetaColumn(PDO $pdo): void {
        $col = Database::isMysql() ? 'LONGTEXT NULL' : 'TEXT NULL';
        self::addColumnIfMissing($pdo, 'remote_access_devices', 'device_meta_json', $col);
    }

    /** @param list<string> $where @param list<mixed> $params */
    private static function appendCompanyScopeSql(string $column, array &$where, array &$params): void {
        Rbac::appendCompanyScope($column, $where, $params);
    }

    public static function canAccessDevice(string $deviceId): bool {
        $device = self::findDevice($deviceId);
        if ($device === null) {
            return Rbac::isGlobalAdmin();
        }

        return Rbac::canAccessCompany((int) ($device['company_id'] ?? 0));
    }

    public static function canOperatorCloseSession(string $sessionId, int $operatorUserId): bool {
        $session = self::findSession($sessionId);
        if ($session === null) {
            return false;
        }
        $companyId = (int) ($session['company_id'] ?? 0);
        if (!Rbac::canAccessCompany($companyId)) {
            return false;
        }
        if (Rbac::isGlobalAdmin() || Rbac::can('remote_access_admin', $companyId)) {
            return true;
        }

        return $operatorUserId > 0
            && (int) ($session['operator_user_id'] ?? 0) === $operatorUserId
            && Rbac::can('remote_access_operate', $companyId);
    }

    /** @return array<string, mixed>|null */
    public static function findSession(string $sessionId): ?array {
        self::ensureSchema();
        $st = Database::pdo()->prepare('SELECT * FROM remote_access_sessions WHERE session_id = ? LIMIT 1');
        $st->execute([$sessionId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);

        return $row ?: null;
    }

    private static function ensureRsshColumns(PDO $pdo): void {
        if (!Database::tableExists($pdo, 'remote_access_sessions')) {
            return;
        }
        $cols = [
            'rssh_bastion_host' => Database::isMysql() ? 'VARCHAR(255) NULL' : 'TEXT NULL',
            'rssh_bastion_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL',
            'rssh_username' => Database::isMysql() ? 'VARCHAR(64) NULL' : 'TEXT NULL',
            'rssh_secret' => Database::isMysql() ? 'VARCHAR(128) NULL' : 'TEXT NULL',
            'rssh_remote_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL',
            'rssh_local_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL',
        ];
        foreach ($cols as $name => $ddl) {
            self::addColumnIfMissing($pdo, 'remote_access_sessions', $name, $ddl);
        }
    }

    private static function ensureSessionColumns(PDO $pdo): void {
        $col = Database::isMysql() ? 'VARCHAR(64) NULL' : 'TEXT NULL';
        self::addColumnIfMissing($pdo, 'remote_access_sessions', 'wg_client_public_key', $col);
    }

    private static function createTables(PDO $pdo): void {
        $path = Database::isMysql()
            ? __DIR__ . '/../sql/migrations/007_remote_access.sql'
            : __DIR__ . '/../sql/migrations/007_remote_access.sqlite.sql';
        if (!is_readable($path)) {
            throw new RuntimeException('remote access migration missing: ' . $path);
        }
        $sql = (string) file_get_contents($path);
        foreach (array_filter(array_map('trim', explode(';', $sql))) as $stmt) {
            if ($stmt !== '') {
                $pdo->exec($stmt);
            }
        }
    }

    /** @return array<string, mixed> heartbeat.ra response payload */
    public static function handleDeviceHeartbeat(array $heartbeat, string $clientIp): array {
        self::ensureSchema();
        $deviceId = trim((string) ($heartbeat['device_id'] ?? ''));
        if ($deviceId === '') {
            return self::raEnvelope('deny', ['reason' => 'missing_device_id']);
        }

        $status = strtolower(trim((string) ($heartbeat['status'] ?? 'enable')));
        $random = trim((string) ($heartbeat['random'] ?? ''));
        $appVersion = trim((string) ($heartbeat['app_version'] ?? ''));
        $capabilities = $heartbeat['capabilities'] ?? [];
        if (!is_array($capabilities)) {
            $capabilities = [];
        }
        $tunnelMode = self::inferTunnelMode($capabilities, $heartbeat);

        if ($status === 'disable') {
            return self::handleDisable($deviceId, $random, $clientIp, $appVersion);
        }

        $existing = self::findDevice($deviceId);
        $pendingSession = null;
        if ($existing !== null && self::isRateLimited($existing)) {
            $pendingSession = self::findConnectableSession($deviceId, $random);
            if ($pendingSession === null) {
                return self::raEnvelope('wait', ['reason' => 'rate_limited']);
            }
        }

        $deviceMeta = is_array($heartbeat['device'] ?? null) ? $heartbeat['device'] : [];
        $routeScope = trim((string) ($heartbeat['vpn_route_scope'] ?? ''));
        $appScope = trim((string) ($heartbeat['vpn_app_scope'] ?? ''));
        if ($routeScope !== '') {
            $deviceMeta['vpn_route_scope'] = $routeScope;
        }
        if ($appScope !== '') {
            $deviceMeta['vpn_app_scope'] = $appScope;
        }

        self::upsertDevicePoll($deviceId, $random, $appVersion, $tunnelMode, $clientIp, $deviceMeta);

        $device = self::findDevice($deviceId);
        if (!$device || !(int) ($device['whitelisted'] ?? 0)) {
            self::logEvent($deviceId, null, null, 'poll_wait', 'not_whitelisted', [
                'opt_in' => $tunnelMode,
            ], $clientIp);
            return self::raEnvelope('wait', ['reason' => 'not_whitelisted']);
        }

        if ($tunnelMode === self::OPT_NONE) {
            return self::raEnvelope('wait', ['reason' => 'opt_in_none']);
        }

        if ($tunnelMode === self::OPT_RSSH) {
            $session = $pendingSession ?? self::findConnectableSession($deviceId, $random);
            if ($session === null) {
                self::logEvent($deviceId, null, null, 'poll_wait', 'no_pending_session', null, $clientIp);
                return self::raEnvelope('wait', ['reason' => 'no_pending_session']);
            }
            self::touchSessionActivity((string) ($session['session_id'] ?? ''));
            return self::buildConnectResponse($session, $deviceId, $clientIp);
        }

        $session = $pendingSession ?? self::findConnectableSession($deviceId, $random);
        if ($session === null) {
            self::logEvent($deviceId, null, null, 'poll_wait', 'no_pending_session', null, $clientIp);
            return self::raEnvelope('wait', ['reason' => 'no_pending_session']);
        }

        self::touchSessionActivity((string) ($session['session_id'] ?? ''));

        return self::buildConnectResponse($session, $deviceId, $clientIp);
    }

    /** @param array<string, mixed> $device */
    private static function isRateLimited(array $device): bool {
        $min = max(15, (int) cfg('remote_access.min_poll_interval_s', 45));
        $last = (string) ($device['last_seen_at'] ?? '');
        if ($last === '') {
            return false;
        }
        $ts = strtotime($last);
        if ($ts === false) {
            return false;
        }
        return (time() - $ts) < $min;
    }

    private static function touchSessionActivity(string $sessionId): void {
        if ($sessionId === '') {
            return;
        }
        Database::pdo()->prepare(
            'UPDATE remote_access_sessions SET last_activity_at = ? WHERE session_id = ?'
        )->execute([self::nowSql(), $sessionId]);
    }

    /** @param list<string> $capabilities */
    private static function inferTunnelMode(array $capabilities, array $heartbeat): string {
        foreach ($capabilities as $cap) {
            $c = strtolower(trim((string) $cap));
            if ($c === 'wireguard' || $c === 'wg') {
                return self::OPT_WIREGUARD;
            }
            if ($c === 'ssh_reverse' || $c === 'rssh') {
                return self::OPT_RSSH;
            }
        }
        $mode = strtolower(trim((string) ($heartbeat['tunnel_mode'] ?? '')));
        if (in_array($mode, [self::OPT_WIREGUARD, self::OPT_RSSH, self::OPT_NONE], true)) {
            return $mode;
        }
        return self::OPT_WIREGUARD;
    }

    private static function handleDisable(string $deviceId, string $random, string $clientIp, string $appVersion): array {
        $pdo = Database::pdo();
        $now = self::nowSql();
        $pdo->prepare(
            'UPDATE remote_access_devices SET whitelisted = 0, opt_in_mode = ?, last_random = ?, app_version = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
        )->execute([self::OPT_NONE, $random, $appVersion, $now, $now, $deviceId]);

        self::closeActiveSessionsForDevice($deviceId, 'user_disabled');
        self::logEvent($deviceId, null, null, 'user_disable', 'device_opt_out', null, $clientIp);
        self::logEvent($deviceId, null, null, 'whitelist_revoked', 'device_opt_out', null, $clientIp);

        return self::raEnvelope('disabled', ['reason' => 'user_opt_out']);
    }

    private static function upsertDevicePoll(
        string $deviceId,
        string $random,
        string $appVersion,
        string $tunnelMode,
        string $clientIp,
        array $deviceMeta = []
    ): void {
        $pdo = Database::pdo();
        $now = self::nowSql();
        $metaJson = self::encodeDeviceMeta($deviceMeta);
        $existing = self::findDevice($deviceId);
        if ($existing) {
            if ($metaJson !== null) {
                $pdo->prepare(
                    'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?,
                     device_meta_json = ?, last_client_ip = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
                )->execute([$tunnelMode, $random, $appVersion, $metaJson, $clientIp, $now, $now, $deviceId]);
            } else {
                $pdo->prepare(
                    'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?,
                     last_client_ip = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
                )->execute([$tunnelMode, $random, $appVersion, $clientIp, $now, $now, $deviceId]);
            }
            return;
        }
        $pdo->prepare(
            'INSERT INTO remote_access_devices (device_id, company_id, whitelisted, opt_in_mode, last_random,
                app_version, device_meta_json, last_client_ip, last_seen_at, created_at, updated_at)
             VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?)'
        )->execute([
            $deviceId,
            Rbac::defaultCompanyId(),
            $tunnelMode,
            $random,
            $appVersion,
            $metaJson,
            $clientIp !== '' ? $clientIp : null,
            $now,
            $now,
            $now,
        ]);
        self::logEvent($deviceId, null, null, 'device_seen', 'first_poll', ['opt_in' => $tunnelMode], $clientIp);
    }

    /** @param array<string, mixed> $deviceMeta */
    private static function encodeDeviceMeta(array $deviceMeta): ?string {
        if ($deviceMeta === []) {
            return null;
        }
        $allowed = ['name', 'manufacturer', 'brand', 'model', 'device', 'product', 'sdk_int', 'release', 'abis', 'lan_ip', 'wan_ip'];
        $clean = [];
        foreach ($allowed as $key) {
            if (!array_key_exists($key, $deviceMeta)) {
                continue;
            }
            $val = $deviceMeta[$key];
            if ($key === 'sdk_int') {
                $clean[$key] = (int) $val;
            } elseif ($key === 'abis' && is_array($val)) {
                $clean[$key] = array_values(array_filter(array_map('strval', $val)));
            } elseif (is_scalar($val)) {
                $s = trim((string) $val);
                if ($s !== '') {
                    $clean[$key] = $s;
                }
            }
        }
        if ($clean === []) {
            return null;
        }
        $json = json_encode($clean, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        return $json === false ? null : $json;
    }

    /** @return array<string, mixed>|null */
    private static function findDevice(string $deviceId): ?array {
        $st = Database::pdo()->prepare('SELECT * FROM remote_access_devices WHERE device_id = ? LIMIT 1');
        $st->execute([$deviceId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $row : null;
    }

    /** @return array<string, mixed>|null */
    private static function findConnectableSession(string $deviceId, string $random): ?array {
        $pdo = Database::pdo();
        $st = $pdo->prepare(
            "SELECT * FROM remote_access_sessions
             WHERE device_id = ? AND status IN ('pending','active')
             ORDER BY id DESC LIMIT 5"
        );
        $st->execute([$deviceId]);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);
        if (!is_array($rows)) {
            return null;
        }
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $exp = (int) ($row['expires_at'] ?? 0);
            if ($exp > 0 && $exp < time()) {
                self::closeSession((string) $row['session_id'], self::STATUS_CLOSED_TIMEOUT, 'expired');
                continue;
            }
            return $row;
        }
        return null;
    }

    /** @param array<string, mixed> $session */
    private static function buildConnectResponse(array $session, string $deviceId, string $clientIp): array {
        $tunnel = (string) ($session['tunnel'] ?? 'wireguard');
        if ($tunnel === 'ssh_reverse') {
            return self::buildRsshConnectResponse($session, $deviceId, $clientIp);
        }
        return self::buildWireGuardConnectResponse($session, $deviceId, $clientIp);
    }

    /** @param array<string, mixed> $session */
    private static function buildWireGuardConnectResponse(array $session, string $deviceId, string $clientIp): array {
        $sessionId = (string) ($session['session_id'] ?? '');
        $tunnel = (string) ($session['tunnel'] ?? 'wireguard');
        if ($tunnel !== 'wireguard') {
            return self::raEnvelope('deny', ['reason' => 'tunnel_not_supported', 'tunnel' => $tunnel]);
        }

        $keys = [];
        try {
            $keys = self::ensureSessionWireGuardKeys($sessionId, $session);
        } catch (Throwable $e) {
            error_log('RemoteAccess connect keys: ' . $e->getMessage());
            return self::raEnvelope('deny', ['reason' => 'key_generation_failed']);
        }
        if (str_starts_with($keys['client_public'] ?? '', 'dev-placeholder-pub-')
            && (bool) cfg('remote_access.require_wg_tools', false)) {
            return self::raEnvelope('deny', ['reason' => 'wg_tools_required']);
        }
        $endpoint = (string) ($session['endpoint'] ?? WireGuardAddressPool::defaultEndpoint());
        $expiresAt = (int) ($session['expires_at'] ?? (time() + 3600));

        self::markSessionActive($sessionId);
        self::logEvent($deviceId, $sessionId, null, 'tunnel_connect', 'wireguard', null, $clientIp);

        return self::raEnvelope('connect', [
            'session_id' => $sessionId,
            'endpoint' => $endpoint,
            'tunnel' => 'wireguard',
            'credentials' => [
                'mode' => 'ephemeral_pubkey',
                'interface_private_key' => $keys['client_private'],
                'interface_address' => $keys['client_address'],
                'peer_public_key' => $keys['server_public'],
                'peer_endpoint' => $endpoint,
                'peer_allowed_ips' => $keys['allowed_ips'],
            ],
            'expires_at' => $expiresAt,
        ]);
    }

    /** @param array<string, mixed> $session */
    private static function buildRsshConnectResponse(array $session, string $deviceId, string $clientIp): array {
        $sessionId = (string) ($session['session_id'] ?? '');
        $creds = self::ensureSessionRsshCredentials($sessionId, $session);
        $expiresAt = (int) ($session['expires_at'] ?? (time() + 3600));
        $endpoint = RsshSessionProvisioner::endpointFromSession($session);

        self::markSessionActive($sessionId);
        self::logEvent($deviceId, $sessionId, null, 'tunnel_connect', 'ssh_reverse', null, $clientIp);

        return self::raEnvelope('connect', [
            'session_id' => $sessionId,
            'endpoint' => $endpoint,
            'tunnel' => 'ssh_reverse',
            'credentials' => [
                'mode' => 'password',
                'bastion_host' => $creds['bastion_host'],
                'bastion_port' => $creds['bastion_port'],
                'username' => $creds['username'],
                'password' => $creds['password'],
                'remote_bind_port' => $creds['remote_bind_port'],
                'local_forward_host' => '127.0.0.1',
                'local_forward_port' => $creds['local_forward_port'],
            ],
            'expires_at' => $expiresAt,
        ]);
    }

    /** @param array<string, mixed> $session @return array{bastion_host:string,bastion_port:int,username:string,password:string,remote_bind_port:int,local_forward_port:int} */
    private static function ensureSessionRsshCredentials(string $sessionId, array $session): array {
        $user = trim((string) ($session['rssh_username'] ?? ''));
        $secret = trim((string) ($session['rssh_secret'] ?? ''));
        $remote = (int) ($session['rssh_remote_port'] ?? 0);
        $local = (int) ($session['rssh_local_port'] ?? 0);
        $host = trim((string) ($session['rssh_bastion_host'] ?? ''));
        $port = (int) ($session['rssh_bastion_port'] ?? 0);
        if ($user !== '' && $secret !== '' && $remote > 0) {
            return [
                'bastion_host' => $host !== '' ? $host : (string) cfg('remote_access.rssh.bastion_host', 'ra.apps.f0xx.org'),
                'bastion_port' => $port > 0 ? $port : (int) cfg('remote_access.rssh.bastion_port', 443),
                'username' => $user,
                'password' => $secret,
                'remote_bind_port' => $remote,
                'local_forward_port' => $local > 0 ? $local : (int) cfg('remote_access.rssh.local_forward_port', 8022),
            ];
        }
        $alloc = RsshSessionProvisioner::allocate($sessionId);
        $bastion = RsshBastionProvisioner::addUser($alloc['username'], $alloc['password']);
        if (!$bastion['ok']) {
            throw new RuntimeException((string) ($bastion['error'] ?? 'bastion_user_add_failed'));
        }
        Database::pdo()->prepare(
            'UPDATE remote_access_sessions SET rssh_bastion_host = ?, rssh_bastion_port = ?, rssh_username = ?, rssh_secret = ?, rssh_remote_port = ?, rssh_local_port = ? WHERE session_id = ?'
        )->execute([
            $alloc['bastion_host'],
            $alloc['bastion_port'],
            $alloc['username'],
            $alloc['password'],
            $alloc['remote_bind_port'],
            $alloc['local_forward_port'],
            $sessionId,
        ]);
        return $alloc;
    }

    /** @param array<string, mixed> $session @return array{client_private:string,client_address:string,server_public:string,allowed_ips:string,client_public:string} */
    private static function ensureSessionWireGuardKeys(string $sessionId, array $session): array {
        $priv = trim((string) ($session['wg_client_private_key'] ?? ''));
        $pub = trim((string) ($session['wg_client_public_key'] ?? ''));
        $addr = trim((string) ($session['wg_client_address'] ?? ''));
        $serverPub = trim((string) ($session['wg_server_public_key'] ?? ''));
        $allowed = trim((string) ($session['wg_peer_allowed_ips'] ?? ''));
        if ($priv !== '' && $addr !== '' && $serverPub !== '') {
            if ($pub === '') {
                $pub = WireGuardPeerProvisioner::publicKeyFromPrivate($priv);
            }
            if (!WireGuardPeerProvisioner::addClientPeer($pub, $addr)) {
                error_log('RemoteAccess: wg peer re-apply failed for session ' . $sessionId);
            }
            return [
                'client_private' => $priv,
                'client_public' => $pub,
                'client_address' => $addr,
                'server_public' => $serverPub,
                'allowed_ips' => $allowed !== '' ? $allowed : WireGuardAddressPool::serverAllowedIps(),
            ];
        }
        $pair = self::generateWireGuardKeyPair();
        $priv = $pair['private'];
        $pub = $pair['public'];
        if ($pub === '' || str_starts_with($pub, 'dev-placeholder-pub-')) {
            $pub = WireGuardPeerProvisioner::publicKeyFromPrivate($priv);
        }
        $clientIp = WireGuardAddressPool::allocateClientAddress($sessionId);
        $serverPub = (string) cfg('remote_access.wg_server_public_key', '');
        if ($serverPub === '') {
            $serverPair = self::generateWireGuardKeyPair();
            $serverPub = $serverPair['public'];
        }
        $allowed = WireGuardAddressPool::serverAllowedIps();
        Database::pdo()->prepare(
            'UPDATE remote_access_sessions SET wg_client_private_key = ?, wg_client_public_key = ?, wg_client_address = ?, wg_server_public_key = ?, wg_peer_allowed_ips = ? WHERE session_id = ?'
        )->execute([$priv, $pub, $clientIp, $serverPub, $allowed, $sessionId]);

        if (!WireGuardPeerProvisioner::addClientPeer($pub, $clientIp)) {
            error_log('RemoteAccess: wg peer add failed for session ' . $sessionId);
        }

        return [
            'client_private' => $priv,
            'client_public' => $pub,
            'client_address' => $clientIp,
            'server_public' => $serverPub,
            'allowed_ips' => $allowed,
        ];
    }

    /** @return array{private:string,public:string} */
    public static function generateWireGuardKeyPair(): array {
        $priv = trim((string) @shell_exec('wg genkey 2>/dev/null') ?: '');
        if ($priv !== '') {
            $pub = trim((string) @shell_exec('echo ' . escapeshellarg($priv) . ' | wg pubkey 2>/dev/null') ?: '');
            if ($pub !== '') {
                return ['private' => $priv, 'public' => $pub];
            }
        }
        if ((bool) cfg('remote_access.require_wg_tools', false)) {
            throw new RuntimeException('wireguard-tools unavailable (wg genkey)');
        }
        $raw = random_bytes(32);
        $priv = rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
        return ['private' => $priv, 'public' => 'dev-placeholder-pub-' . substr(hash('sha256', $priv), 0, 43)];
    }

    private static function markSessionActive(string $sessionId): void {
        $now = self::nowSql();
        Database::pdo()->prepare(
            "UPDATE remote_access_sessions SET status = 'active', last_activity_at = ? WHERE session_id = ?"
        )->execute([$now, $sessionId]);
    }

    public static function closeActiveSessionsForDevice(string $deviceId, string $reason): void {
        $st = Database::pdo()->prepare(
            "SELECT session_id FROM remote_access_sessions WHERE device_id = ? AND status IN ('pending','active')"
        );
        $st->execute([$deviceId]);
        foreach ($st->fetchAll(PDO::FETCH_COLUMN) as $sid) {
            self::closeSession((string) $sid, self::STATUS_CLOSED, $reason);
        }
    }

    public static function closeSession(string $sessionId, string $status, string $reason): void {
        $st = Database::pdo()->prepare(
            'SELECT tunnel, wg_client_public_key, rssh_username FROM remote_access_sessions WHERE session_id = ? LIMIT 1'
        );
        $st->execute([$sessionId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if (is_array($row)) {
            if (($row['tunnel'] ?? 'wireguard') === 'wireguard') {
                $pub = $row['wg_client_public_key'] ?? '';
                if (is_string($pub) && $pub !== '') {
                    WireGuardPeerProvisioner::removeClientPeer($pub);
                }
            } elseif (($row['tunnel'] ?? '') === 'ssh_reverse') {
                RsshBastionProvisioner::removeUser((string) ($row['rssh_username'] ?? ''));
            }
        }
        $now = self::nowSql();
        Database::pdo()->prepare(
            'UPDATE remote_access_sessions SET status = ?, close_reason = ?, closed_at = ?, last_activity_at = ? WHERE session_id = ?'
        )->execute([$status, $reason, $now, $now, $sessionId]);
    }

    /** @return array{ok:bool,session_id?:string} */
    public static function openSession(string $deviceId, int $operatorUserId, ?int $companyId = null): array {
        self::ensureSchema();
        $device = self::findDevice($deviceId);
        if (!$device || !(int) ($device['whitelisted'] ?? 0)) {
            return ['ok' => false, 'error' => 'device_not_whitelisted'];
        }
        $deviceCompanyId = (int) ($device['company_id'] ?? 0);
        if (!Rbac::canAccessCompany($deviceCompanyId) || !Rbac::can('remote_access_operate', $deviceCompanyId)) {
            return ['ok' => false, 'error' => 'forbidden'];
        }
        if ((string) ($device['opt_in_mode'] ?? self::OPT_NONE) === self::OPT_NONE) {
            return ['ok' => false, 'error' => 'device_not_opted_in'];
        }
        $optIn = (string) ($device['opt_in_mode'] ?? self::OPT_NONE);
        if (!in_array($optIn, [self::OPT_WIREGUARD, self::OPT_RSSH], true)) {
            return ['ok' => false, 'error' => 'device_not_opted_in'];
        }
        self::closeActiveSessionsForDevice($deviceId, 'superseded');
        $sessionId = self::uuid4();
        $companyId = $companyId ?? (int) ($device['company_id'] ?? Rbac::defaultCompanyId());
        $tunnel = $optIn === self::OPT_RSSH ? 'ssh_reverse' : 'wireguard';
        if ($tunnel === 'ssh_reverse') {
            $host = (string) cfg('remote_access.rssh.bastion_host', 'ra.apps.f0xx.org');
            $port = (int) cfg('remote_access.rssh.bastion_port', 443);
            $endpoint = $port === 443 ? $host : $host . ':' . $port;
        } else {
            $endpoint = WireGuardAddressPool::defaultEndpoint();
        }
        $expiresAt = time() + (int) cfg('remote_access.session_ttl_s', 3600);
        $now = self::nowSql();
        Database::pdo()->prepare(
            'INSERT INTO remote_access_sessions (session_id, device_id, company_id, operator_user_id, status, tunnel, endpoint, expires_at, last_activity_at, created_at)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
        )->execute([
            $sessionId,
            $deviceId,
            $companyId,
            $operatorUserId,
            self::STATUS_PENDING,
            $tunnel,
            $endpoint,
            $expiresAt,
            $now,
            $now,
        ]);
        self::logEvent($deviceId, $sessionId, $operatorUserId, 'session_open', 'operator_requested', null, '');
        return ['ok' => true, 'session_id' => $sessionId];
    }

    /** @return list<array<string, mixed>> */
    public static function listDevices(?int $companyId = null): array {
        self::ensureSchema();
        $sql = 'SELECT * FROM remote_access_devices';
        $params = [];
        if (!Rbac::isGlobalAdmin()) {
            $allowed = Rbac::allowedCompanyIds();
            if ($allowed === []) {
                return [];
            }
            if ($allowed !== null) {
                $placeholders = implode(',', array_fill(0, count($allowed), '?'));
                $sql .= ' WHERE company_id IN (' . $placeholders . ')';
                $params = $allowed;
            }
        } elseif ($companyId !== null) {
            $sql .= ' WHERE company_id = ?';
            $params[] = $companyId;
        }
        $sql .= Database::isMysql()
            ? ' ORDER BY (last_seen_at IS NULL), last_seen_at DESC, device_id ASC'
            : ' ORDER BY datetime(COALESCE(last_seen_at, \'1970-01-01\')) DESC, device_id ASC';
        $st = Database::pdo()->prepare($sql);
        $st->execute($params);
        return $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    /** @return list<array<string, mixed>> */
    public static function listSessions(string $statusFilter = ''): array {
        self::ensureSchema();
        $sql = 'SELECT s.*, d.opt_in_mode, d.app_version AS device_app_version FROM remote_access_sessions s
                LEFT JOIN remote_access_devices d ON d.device_id = s.device_id';
        $where = [];
        $params = [];
        if ($statusFilter !== '') {
            $where[] = 's.status = ?';
            $params[] = $statusFilter;
        }
        self::appendCompanyScopeSql('s.company_id', $where, $params);
        if ($where !== []) {
            $sql .= ' WHERE ' . implode(' AND ', $where);
        }
        $sql .= ' ORDER BY s.id DESC LIMIT 200';
        $st = Database::pdo()->prepare($sql);
        $st->execute($params);
        return $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    /** @return list<array<string, mixed>> */
    public static function listEvents(int $limit = 100): array {
        self::ensureSchema();
        $sql = 'SELECT e.* FROM remote_access_events e';
        $where = [];
        $params = [];
        if (!Rbac::isGlobalAdmin()) {
            $allowed = Rbac::allowedCompanyIds();
            if ($allowed === []) {
                return [];
            }
            if ($allowed !== null) {
                $sql .= ' INNER JOIN remote_access_devices d ON d.device_id = e.device_id';
                self::appendCompanyScopeSql('d.company_id', $where, $params);
            }
        }
        if ($where !== []) {
            $sql .= ' WHERE ' . implode(' AND ', $where);
        }
        $sql .= ' ORDER BY e.id DESC LIMIT ?';
        $params[] = max(1, min(500, $limit));
        $st = Database::pdo()->prepare($sql);
        foreach ($params as $i => $v) {
            $st->bindValue($i + 1, $v, is_int($v) ? PDO::PARAM_INT : PDO::PARAM_STR);
        }
        $st->execute();
        return $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public static function setDeviceWhitelist(string $deviceId, bool $whitelisted, ?string $notes = null): bool {
        self::ensureSchema();
        $device = self::findDevice($deviceId);
        if (!$device) {
            $companyId = Rbac::activeCompanyId() ?? Rbac::defaultCompanyId();
            if (!Rbac::can('remote_access_admin', $companyId)) {
                return false;
            }
            Database::pdo()->prepare(
                'INSERT INTO remote_access_devices (device_id, company_id, whitelisted, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
            )->execute([
                $deviceId,
                $companyId,
                $whitelisted ? 1 : 0,
                $notes,
                self::nowSql(),
                self::nowSql(),
            ]);
            return true;
        }
        $companyId = (int) ($device['company_id'] ?? 0);
        if (!Rbac::canAccessCompany($companyId) || !Rbac::can('remote_access_admin', $companyId)) {
            return false;
        }
        Database::pdo()->prepare(
            'UPDATE remote_access_devices SET whitelisted = ?, notes = COALESCE(?, notes), updated_at = ? WHERE device_id = ?'
        )->execute([$whitelisted ? 1 : 0, $notes, self::nowSql(), $deviceId]);
        return true;
    }

    /** @return list<string> */
    public static function activeWireGuardSessionPublicKeys(): array {
        $peers = self::activeWireGuardSessionPeers();
        $keys = [];
        foreach ($peers as $row) {
            $pub = trim((string) ($row['public_key'] ?? ''));
            if ($pub !== '') {
                $keys[] = $pub;
            }
        }
        return $keys;
    }

    /**
     * Pending/active WG sessions with client pubkey + tunnel address (for wg set reconcile).
     *
     * @return list<array{session_id: string, public_key: string, client_address: string}>
     */
    public static function activeWireGuardSessionPeers(): array {
        self::ensureSchema();
        $st = Database::pdo()->query(
            "SELECT session_id, wg_client_public_key, wg_client_address FROM remote_access_sessions
             WHERE status IN ('pending','active') AND tunnel = 'wireguard'
             AND wg_client_public_key IS NOT NULL AND wg_client_public_key != ''
             AND wg_client_address IS NOT NULL AND wg_client_address != ''"
        );
        if ($st === false) {
            return [];
        }
        $rows = [];
        foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $row) {
            if (!is_array($row)) {
                continue;
            }
            $pub = trim((string) ($row['wg_client_public_key'] ?? ''));
            $addr = trim((string) ($row['wg_client_address'] ?? ''));
            if ($pub === '' || $addr === '') {
                continue;
            }
            $rows[] = [
                'session_id' => (string) ($row['session_id'] ?? ''),
                'public_key' => $pub,
                'client_address' => $addr,
            ];
        }
        return $rows;
    }

    /**
     * Ensure wg0 has allowed-ips for every pending/active WG session (idempotent).
     *
     * @return list<array{session_id: string, public_key: string, client_address: string, ok: bool}>
     */
    public static function reconcileActiveWireGuardPeers(bool $dryRun = false): array {
        $results = [];
        foreach (self::activeWireGuardSessionPeers() as $row) {
            $ok = true;
            if (!$dryRun) {
                $ok = WireGuardPeerProvisioner::addClientPeer(
                    $row['public_key'],
                    $row['client_address']
                );
            }
            $results[] = [
                'session_id' => $row['session_id'],
                'public_key' => $row['public_key'],
                'client_address' => $row['client_address'],
                'ok' => $ok,
            ];
        }
        return $results;
    }

    /**
     * Drop wg0 peers that are not referenced by pending/active sessions.
     *
     * @return list<array{public_key: string, allowed_ips: string, reason: string}>
     */
    public static function pruneOrphanWireGuardPeers(bool $dryRun = false): array {
        return WireGuardPeerProvisioner::pruneOrphanPeers(
            self::activeWireGuardSessionPublicKeys(),
            $dryRun
        );
    }

    public static function purgeStale(int $maxAgeHours = 24): int {
        self::ensureSchema();
        $cutoff = time() - max(1, $maxAgeHours) * 3600;
        $pdo = Database::pdo();
        if (Database::isMysql()) {
            $st = $pdo->prepare(
                "SELECT session_id FROM remote_access_sessions WHERE status IN ('inactive','closed','closed_timeout','pending','active')
                 AND ((expires_at IS NOT NULL AND expires_at < ?) OR (last_activity_at IS NOT NULL AND UNIX_TIMESTAMP(last_activity_at) < ?))"
            );
            $st->execute([time(), $cutoff]);
        } else {
            $cutoffIso = gmdate('Y-m-d H:i:s', $cutoff);
            $st = $pdo->prepare(
                "SELECT session_id FROM remote_access_sessions WHERE status IN ('inactive','closed','closed_timeout','pending','active')
                 AND ((expires_at IS NOT NULL AND expires_at < ?) OR (last_activity_at IS NOT NULL AND last_activity_at < ?))"
            );
            $st->execute([time(), $cutoffIso]);
        }
        $n = 0;
        foreach ($st->fetchAll(PDO::FETCH_COLUMN) as $sid) {
            self::closeSession((string) $sid, self::STATUS_CLOSED_TIMEOUT, 'purge_stale');
            $n++;
        }
        return $n;
    }

    public static function logEvent(
        string $deviceId,
        ?string $sessionId,
        ?int $actorUserId,
        string $action,
        ?string $reason,
        ?array $meta,
        string $clientIp
    ): void {
        self::ensureSchema();
        Database::pdo()->prepare(
            'INSERT INTO remote_access_events (session_id, device_id, actor_user_id, action, reason, meta_json, client_ip, created_at)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
        )->execute([
            $sessionId,
            $deviceId,
            $actorUserId,
            $action,
            $reason,
            $meta !== null ? json_encode($meta, JSON_UNESCAPED_UNICODE) : null,
            $clientIp !== '' ? $clientIp : null,
            self::nowSql(),
        ]);
    }

    /** @param array<string, mixed> $extra */
    private static function raEnvelope(string $action, array $extra = []): array {
        return array_merge([
            'type' => 'ra',
            'action' => $action,
            'server_date' => time(),
        ], $extra);
    }

    private static function nowSql(): string {
        return Database::isMysql() ? date('Y-m-d H:i:s') : gmdate('Y-m-d H:i:s');
    }

    /** @return array{devices: list<array<string, mixed>>, active_sessions: list<array<string, mixed>>, inactive_sessions: list<array<string, mixed>>, recent_events: list<array<string, mixed>>} */
    public static function buildDashboardPayload(string $issuesBase, string $projectBase): array {
        self::ensureSchema();
        $sessions = self::listSessions('');
        $active = array_values(array_filter(
            $sessions,
            static fn($s) => in_array($s['status'] ?? '', ['pending', 'active'], true)
        ));
        $inactive = array_values(array_filter(
            $sessions,
            static fn($s) => !in_array($s['status'] ?? '', ['pending', 'active'], true)
        ));

        return [
            'devices' => self::listDevicesForDashboard($issuesBase, $projectBase),
            'active_sessions' => array_map([self::class, 'enrichSessionRow'], $active),
            'inactive_sessions' => array_map([self::class, 'enrichSessionRow'], array_slice($inactive, 0, 50)),
            'recent_events' => self::listEvents(30),
        ];
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private static function enrichSessionRow(array $row): array {
        if (($row['tunnel'] ?? '') === 'ssh_reverse') {
            $cmds = RsshBastionProvisioner::operatorCommands($row);
            if ($cmds !== []) {
                $row['rssh_operator'] = $cmds;
            }
            $remote = (int) ($row['rssh_remote_port'] ?? 0);
            if ($remote > 0) {
                $row['rssh_remote_bind'] = '127.0.0.1:' . $remote;
            }
        }
        return $row;
    }

    /** @return list<array<string, mixed>> */
    public static function listDevicesForDashboard(string $issuesBase, string $projectBase): array {
        $devices = self::listDevices();
        if ($devices === []) {
            return [];
        }
        $deviceIds = array_values(array_filter(array_map(
            static fn($row) => (string) ($row['device_id'] ?? ''),
            $devices
        )));
        $sessionRows = self::batchActiveSessionRows($deviceIds);
        $wgPeers = WireGuardTrafficStats::peerMap();
        $out = [];
        foreach ($devices as $row) {
            $deviceId = (string) ($row['device_id'] ?? '');
            $out[] = self::enrichDeviceRow(
                $row,
                $issuesBase,
                $projectBase,
                $sessionRows[$deviceId] ?? null,
                $wgPeers
            );
        }
        return $out;
    }

    /** @param list<string> $deviceIds @return array<string, array<string, mixed>> */
    private static function batchActiveSessionRows(array $deviceIds): array {
        $deviceIds = array_values(array_unique(array_filter(array_map('strval', $deviceIds))));
        if ($deviceIds === []) {
            return [];
        }
        $placeholders = implode(',', array_fill(0, count($deviceIds), '?'));
        $st = Database::pdo()->prepare(
            "SELECT device_id, tunnel, wg_client_address, wg_client_public_key
             FROM remote_access_sessions
             WHERE status IN ('pending', 'active') AND device_id IN ($placeholders)
             ORDER BY id DESC"
        );
        $st->execute($deviceIds);
        $map = [];
        foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $did = (string) ($row['device_id'] ?? '');
            if ($did !== '' && !isset($map[$did])) {
                $map[$did] = $row;
            }
        }
        return $map;
    }

    /** @param array<string, mixed> $row @param array<string, mixed>|null $sessionRow @param array<string, array{rx_bytes: int, tx_bytes: int, latest_handshake: int, endpoint: string}>|null $wgPeers */
    private static function enrichDeviceRow(
        array $row,
        string $issuesBase,
        string $projectBase,
        ?array $sessionRow = null,
        ?array $wgPeers = null
    ): array {
        $deviceId = (string) ($row['device_id'] ?? '');
        $companyId = (int) ($row['company_id'] ?? 1);
        $storedMeta = self::decodeDeviceMeta($row['device_meta_json'] ?? null);
        $ctx = self::lookupDeviceContextFromMeta($deviceId, $companyId, $storedMeta);
        $row['needs_whitelist'] = self::deviceNeedsWhitelist($row);
        $row['status_label'] = self::deviceStatusLabel($row);
        $row['device_name'] = $ctx['device_name'] ?? '';
        $row['device_display'] = $ctx['device_display'] ?? '';
        $row['manufacturer'] = $ctx['manufacturer'] ?? '';
        $row['brand'] = $ctx['brand'] ?? '';
        $row['model'] = $ctx['model'] ?? '';
        $row['product'] = $ctx['product'] ?? '';
        $row['hardware_device'] = $ctx['hardware_device'] ?? '';
        $row['os_release'] = $ctx['os_release'] ?? '';
        $row['sdk_int'] = $ctx['sdk_int'] ?? null;
        $row['abis'] = $ctx['abis'] ?? [];
        $row['lan_ip'] = self::resolveDeviceLanIp($storedMeta);
        $row['poll_source_ip'] = self::resolvePollSourceIp($row);
        $row['poll_source_is_private'] = self::isPrivateIp($row['poll_source_ip']);
        $row['device_wan_ip'] = trim((string) ($storedMeta['wan_ip'] ?? ''));
        $row['vpn_route_scope'] = trim((string) ($storedMeta['vpn_route_scope'] ?? ''));
        $row['vpn_app_scope'] = trim((string) ($storedMeta['vpn_app_scope'] ?? ''));
        $net = self::sessionNetworkFromRow($sessionRow, $wgPeers);
        $row['vpn_ip'] = $net['vpn_ip'] ?? '';
        $row['vpn_public_key'] = $net['vpn_public_key'] ?? '';
        $row['wg_rx_bytes'] = $net['wg_rx_bytes'] ?? 0;
        $row['wg_tx_bytes'] = $net['wg_tx_bytes'] ?? 0;
        $row['wg_latest_handshake'] = $net['wg_latest_handshake'] ?? 0;
        $row['wg_endpoint'] = $net['wg_endpoint'] ?? '';
        $row['issue_count'] = $ctx['issue_count'] ?? 0;
        $row['graph_session_count'] = $ctx['graph_session_count'] ?? 0;
        $row['latest_issue_id'] = $ctx['latest_issue_id'] ?? null;
        $row['links'] = self::deviceLinks($deviceId, $issuesBase, $projectBase, $ctx);
        if (($row['app_version'] ?? '') === '' && ($ctx['app_version'] ?? '') !== '') {
            $row['app_version'] = $ctx['app_version'];
        }
        return $row;
    }

    /** @return array<string, mixed> */
    private static function decodeDeviceMeta(mixed $raw): array {
        if (!is_string($raw) || trim($raw) === '') {
            return [];
        }
        $decoded = json_decode($raw, true);

        return is_array($decoded) ? $decoded : [];
    }

    /** @param array<string, mixed> $row */
    private static function deviceNeedsWhitelist(array $row): bool {
        if ((int) ($row['whitelisted'] ?? 0) === 1) {
            return false;
        }
        $mode = (string) ($row['opt_in_mode'] ?? 'none');
        if ($mode === self::OPT_NONE) {
            return false;
        }
        return self::isRecentTimestamp($row['last_seen_at'] ?? null, 7 * 86400);
    }

    /** @param array<string, mixed> $row */
    private static function deviceStatusLabel(array $row): string {
        if ((int) ($row['whitelisted'] ?? 0) === 1) {
            return 'whitelisted';
        }
        if (self::deviceNeedsWhitelist($row)) {
            return 'needs_whitelist';
        }
        $mode = (string) ($row['opt_in_mode'] ?? 'none');
        if ($mode === self::OPT_NONE) {
            return 'not_opted_in';
        }
        if (!self::isRecentTimestamp($row['last_seen_at'] ?? null, 7 * 86400)) {
            return 'stale';
        }
        return 'polling';
    }

    private static function isRecentTimestamp(?string $ts, int $maxAgeSec): bool {
        if ($ts === null || trim($ts) === '') {
            return false;
        }
        $t = strtotime($ts);
        if ($t === false) {
            return false;
        }
        return (time() - $t) <= $maxAgeSec;
    }

    /** @param array<string, mixed> $row */
    private static function resolvePollSourceIp(array $row): string {
        $fromRow = trim((string) ($row['last_client_ip'] ?? ''));
        if ($fromRow !== '') {
            return self::normalizeClientIp($fromRow);
        }
        $deviceId = (string) ($row['device_id'] ?? '');
        if ($deviceId === '') {
            return '';
        }
        $st = Database::pdo()->prepare(
            "SELECT client_ip FROM remote_access_events
             WHERE device_id = ? AND client_ip IS NOT NULL AND client_ip != ''
             ORDER BY id DESC LIMIT 1"
        );
        $st->execute([$deviceId]);
        $ip = trim((string) ($st->fetchColumn() ?: ''));

        return self::normalizeClientIp($ip);
    }

    private static function isPrivateIp(string $ip): bool {
        if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return false;
        }
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return true;
        }

        return false;
    }

    /** @param array<string, mixed> $storedMeta */
    private static function resolveDeviceLanIp(array $storedMeta): string {
        return trim((string) ($storedMeta['lan_ip'] ?? ''));
    }

    private static function normalizeClientIp(string $raw): string {
        $raw = trim($raw);
        if ($raw === '') {
            return '';
        }
        if (str_contains($raw, ',')) {
            $raw = trim(explode(',', $raw)[0]);
        }

        return $raw;
    }

    /** @param array<string, mixed>|null $sessionRow @param array<string, array{rx_bytes: int, tx_bytes: int, latest_handshake: int, endpoint: string}>|null $wgPeers @return array{vpn_ip: string, vpn_public_key: string, wg_rx_bytes: int, wg_tx_bytes: int, wg_latest_handshake: int, wg_endpoint: string} */
    private static function sessionNetworkFromRow(?array $sessionRow, ?array $wgPeers): array {
        $empty = [
            'vpn_ip' => '',
            'vpn_public_key' => '',
            'wg_rx_bytes' => 0,
            'wg_tx_bytes' => 0,
            'wg_latest_handshake' => 0,
            'wg_endpoint' => '',
        ];
        if (!is_array($sessionRow)) {
            return $empty;
        }
        $tunnel = (string) ($sessionRow['tunnel'] ?? '');
        if ($tunnel !== '' && $tunnel !== 'wireguard') {
            return $empty;
        }
        $addr = trim((string) ($sessionRow['wg_client_address'] ?? ''));
        $vpnIp = $addr !== '' ? preg_replace('/\/\d+$/', '', $addr) ?? $addr : '';
        $pub = trim((string) ($sessionRow['wg_client_public_key'] ?? ''));
        $stats = ($wgPeers !== null && $pub !== '') ? ($wgPeers[$pub] ?? null) : null;
        if ($stats === null && $pub !== '') {
            $stats = WireGuardTrafficStats::forPublicKey($pub);
        }

        return [
            'vpn_ip' => is_string($vpnIp) ? trim($vpnIp) : '',
            'vpn_public_key' => $pub,
            'wg_rx_bytes' => (int) ($stats['rx_bytes'] ?? 0),
            'wg_tx_bytes' => (int) ($stats['tx_bytes'] ?? 0),
            'wg_latest_handshake' => (int) ($stats['latest_handshake'] ?? 0),
            'wg_endpoint' => (string) ($stats['endpoint'] ?? ''),
        ];
    }

    /** Fast dashboard enrichment from heartbeat meta (no reports/graph table scans). */
    private static function lookupDeviceContextFromMeta(
        string $deviceId,
        int $companyId,
        array $storedMeta = []
    ): array {
        if ($deviceId === '') {
            return [];
        }
        unset($companyId);
        $deviceName = trim((string) ($storedMeta['name'] ?? ''));
        $manufacturer = trim((string) ($storedMeta['manufacturer'] ?? ''));
        $brand = trim((string) ($storedMeta['brand'] ?? ''));
        $model = trim((string) ($storedMeta['model'] ?? ''));
        $hardwareDevice = trim((string) ($storedMeta['device'] ?? ''));
        $product = trim((string) ($storedMeta['product'] ?? ''));
        $osRelease = trim((string) ($storedMeta['release'] ?? ''));
        $sdkInt = isset($storedMeta['sdk_int']) ? (int) $storedMeta['sdk_int'] : null;
        $abis = is_array($storedMeta['abis'] ?? null) ? $storedMeta['abis'] : [];
        $appVersion = trim((string) ($storedMeta['app_version'] ?? ''));
        $deviceDisplay = $deviceName !== ''
            ? $deviceName
            : trim($manufacturer . ' ' . $model);

        return [
            'issue_count' => 0,
            'latest_issue_id' => null,
            'graph_session_count' => 0,
            'device_name' => $deviceName,
            'device_display' => $deviceDisplay,
            'manufacturer' => $manufacturer,
            'brand' => $brand,
            'model' => $model,
            'hardware_device' => $hardwareDevice,
            'product' => $product,
            'os_release' => $osRelease,
            'sdk_int' => $sdkInt,
            'abis' => $abis,
            'app_version' => $appVersion,
        ];
    }

    /** @return array{vpn_ip: string, vpn_public_key: string, wg_rx_bytes: int, wg_tx_bytes: int, wg_latest_handshake: int, wg_endpoint: string} */
    private static function lookupDeviceSessionNetwork(string $deviceId): array {
        if ($deviceId === '') {
            return [
                'vpn_ip' => '',
                'vpn_public_key' => '',
                'wg_rx_bytes' => 0,
                'wg_tx_bytes' => 0,
                'wg_latest_handshake' => 0,
                'wg_endpoint' => '',
            ];
        }
        $st = Database::pdo()->prepare(
            "SELECT tunnel, wg_client_address, wg_client_public_key
             FROM remote_access_sessions
             WHERE device_id = ? AND status IN ('pending', 'active')
             ORDER BY id DESC LIMIT 1"
        );
        $st->execute([$deviceId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);

        return self::sessionNetworkFromRow(is_array($row) ? $row : null, null);
    }

    /** @param array<string, mixed> $storedMeta @return array<string, mixed> */
    private static function lookupDeviceContext(string $deviceId, int $companyId, array $storedMeta = []): array {
        if ($deviceId === '') {
            return [];
        }
        $pdo = Database::pdo();
        $deviceName = trim((string) ($storedMeta['name'] ?? ''));
        $manufacturer = trim((string) ($storedMeta['manufacturer'] ?? ''));
        $brand = trim((string) ($storedMeta['brand'] ?? ''));
        $model = trim((string) ($storedMeta['model'] ?? ''));
        $hardwareDevice = trim((string) ($storedMeta['device'] ?? ''));
        $product = trim((string) ($storedMeta['product'] ?? ''));
        $osRelease = trim((string) ($storedMeta['release'] ?? ''));
        $sdkInt = isset($storedMeta['sdk_int']) ? (int) $storedMeta['sdk_int'] : null;
        $abis = is_array($storedMeta['abis'] ?? null) ? $storedMeta['abis'] : [];
        $appVersion = '';

        $issueCount = 0;
        $latestIssueId = null;
        $graphCount = 0;

        if (Database::tableExists($pdo, 'graph_sessions')) {
            $gSt = $pdo->prepare(
                'SELECT app_version, sdk_int FROM graph_sessions WHERE device_id = ? ORDER BY started_at_ms DESC LIMIT 1'
            );
            $gSt->execute([$deviceId]);
            $gRow = $gSt->fetch(PDO::FETCH_ASSOC);
            if (is_array($gRow)) {
                if ($appVersion === '') {
                    $appVersion = trim((string) ($gRow['app_version'] ?? ''));
                }
                if ($sdkInt === null && isset($gRow['sdk_int'])) {
                    $sdkInt = (int) $gRow['sdk_int'];
                }
            }
            $cntSt = $pdo->prepare('SELECT COUNT(*) FROM graph_sessions WHERE device_id = ?');
            $cntSt->execute([$deviceId]);
            $graphCount = (int) $cntSt->fetchColumn();
        }

        if (Database::tableExists($pdo, 'reports')) {
            $reportsDeviceId = self::resolveDeviceDbId($deviceId, $companyId);
            if ($reportsDeviceId !== null) {
                $cntSt = $pdo->prepare(
                    'SELECT COUNT(*) FROM reports r WHERE r.company_id = ? AND r.device_id = ?'
                );
                $cntSt->execute([$companyId, $reportsDeviceId]);
                $issueCount = (int) $cntSt->fetchColumn();

                $latestSt = $pdo->prepare(
                    'SELECT r.id, r.device_model, r.app_version, r.payload_json
                     FROM reports r
                     WHERE r.company_id = ? AND r.device_id = ?
                     ORDER BY r.received_at_ms DESC LIMIT 1'
                );
                $latestSt->execute([$companyId, $reportsDeviceId]);
                $latest = $latestSt->fetch(PDO::FETCH_ASSOC);
            } else {
                // Avoid payload_json LIKE scans (full table) on unregistered devices.
                $issueCount = 0;
                $latest = null;
            }
            if (is_array($latest)) {
                $latestIssueId = (int) ($latest['id'] ?? 0) ?: null;
                if ($appVersion === '') {
                    $appVersion = trim((string) ($latest['app_version'] ?? ''));
                }
                $payload = json_decode((string) ($latest['payload_json'] ?? '{}'), true);
                if (is_array($payload)) {
                    $device = is_array($payload['device'] ?? null) ? $payload['device'] : [];
                    if ($manufacturer === '') {
                        $manufacturer = trim((string) ($device['manufacturer'] ?? ''));
                    }
                    if ($brand === '') {
                        $brand = trim((string) ($device['brand'] ?? ''));
                    }
                    if ($model === '') {
                        $model = trim((string) ($device['model'] ?? $latest['device_model'] ?? ''));
                    }
                    if ($hardwareDevice === '') {
                        $hardwareDevice = trim((string) ($device['device'] ?? ''));
                    }
                    if ($product === '') {
                        $product = trim((string) ($device['product'] ?? ''));
                    }
                    if ($osRelease === '') {
                        $osRelease = trim((string) ($device['release'] ?? ''));
                    }
                    if ($sdkInt === null && isset($device['sdk_int'])) {
                        $sdkInt = (int) $device['sdk_int'];
                    }
                    if ($abis === [] && is_array($device['abis'] ?? null)) {
                        $abis = $device['abis'];
                    }
                    if ($appVersion === '' && is_array($payload['app'] ?? null)) {
                        $appVersion = trim((string) ($payload['app']['version_name'] ?? ''));
                    }
                }
            }
        }

        $deviceDisplay = $deviceName !== ''
            ? $deviceName
            : trim($manufacturer . ' ' . $model);

        return [
            'issue_count' => $issueCount,
            'latest_issue_id' => $latestIssueId,
            'graph_session_count' => $graphCount,
            'device_name' => $deviceName,
            'device_display' => $deviceDisplay,
            'manufacturer' => $manufacturer,
            'brand' => $brand,
            'model' => $model,
            'hardware_device' => $hardwareDevice,
            'product' => $product,
            'os_release' => $osRelease,
            'sdk_int' => $sdkInt,
            'abis' => $abis,
            'app_version' => $appVersion,
        ];
    }

    private static function resolveDeviceDbId(string $deviceId, int $companyId): ?int {
        static $cache = [];
        $key = $companyId . ':' . $deviceId;
        if (array_key_exists($key, $cache)) {
            return $cache[$key];
        }
        $pdo = Database::pdo();
        if (!Database::tableExists($pdo, 'devices')) {
            $cache[$key] = null;

            return null;
        }
        $st = $pdo->prepare(
            'SELECT id FROM devices WHERE company_id = ? AND external_id = ? LIMIT 1'
        );
        $st->execute([$companyId, $deviceId]);
        $id = $st->fetchColumn();
        $cache[$key] = ($id !== false && $id !== null && $id !== '') ? (int) $id : null;

        return $cache[$key];
    }

    /** @param array<string, mixed> $ctx @return list<array{label: string, href: string}> */
    private static function deviceLinks(string $deviceId, string $issuesBase, string $projectBase, array $ctx): array {
        if ($deviceId === '') {
            return [];
        }
        $links = [];
        $issueCount = (int) ($ctx['issue_count'] ?? 0);
        $issuesLabel = $issueCount > 0 ? 'Issues (' . $issueCount . ')' : 'Issues';
        $links[] = [
            'label' => $issuesLabel,
            'href' => $issuesBase . '/?view=reports&q=' . rawurlencode($deviceId),
        ];
        if (!empty($ctx['latest_issue_id'])) {
            $links[] = [
                'label' => 'Latest issue #' . (int) $ctx['latest_issue_id'],
                'href' => $issuesBase . '/?view=report&id=' . (int) $ctx['latest_issue_id'],
            ];
        }
        $graphCount = (int) ($ctx['graph_session_count'] ?? 0);
        if ($graphCount > 0) {
            $links[] = [
                'label' => 'Graph sessions (' . $graphCount . ')',
                'href' => rtrim($projectBase, '/') . '/graphs/',
            ];
        }
        return $links;
    }

    private static function uuid4(): string {
        $b = random_bytes(16);
        $b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
        $b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
        $h = bin2hex($b);
        return substr($h, 0, 8) . '-' . substr($h, 8, 4) . '-' . substr($h, 12, 4)
            . '-' . substr($h, 16, 4) . '-' . substr($h, 20, 12);
    }
}
