"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const config_1 = require("./config");
const logger_1 = require("./logger");
const client_1 = require("./websocket/client");
const system_1 = require("./collectors/system");
const process_1 = require("./collectors/process");
const productivity_1 = require("./collectors/productivity");
const network_1 = require("./collectors/network");
const system_events_1 = require("./collectors/system-events");
const software_1 = require("./collectors/software");
const login_sessions_1 = require("./collectors/login-sessions");
const browser_history_1 = require("./collectors/browser-history");
const probe_runner_1 = require("./probes/probe-runner");
const upgrade_1 = require("./services/upgrade");
const watchdog_1 = require("./services/watchdog");
const location_1 = require("./services/location");
const security_scanner_1 = require("./collectors/security-scanner");
const xdr_scanner_1 = require("./collectors/xdr-scanner");
const network_interfaces_1 = require("./collectors/network-interfaces");
const version_1 = require("./version");
const config_ui_1 = require("./config-ui");
const routeros_api_1 = require("./utils/routeros-api");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const dns_1 = __importDefault(require("dns"));
const net_1 = __importDefault(require("net"));
const os_1 = __importDefault(require("os"));
const crypto_1 = __importDefault(require("crypto"));
const semver_1 = __importDefault(require("semver"));
const si = __importStar(require("systeminformation"));
const url_1 = require("url");
const app_paths_1 = require("./utils/app-paths");
const linux_desktop_1 = require("./utils/linux-desktop");
const VERSION = version_1.AGENT_VERSION;
const DEFAULT_TENANT_FEATURES = {
    productivityMonitoring: true,
    browserMonitoring: true,
    screenshots: true,
    processMonitoring: true,
    systemEvents: true,
    networkMonitoring: true,
    remoteAccess: true,
    softwareManagement: true,
    speedtest: true,
    probes: true,
};
class AppStatsAgent {
    constructor() {
        this.softwarePolicies = [];
        this.xdrEnabled = false;
        this.xdrScanInterval = null;
        this.tenantFeatures = { ...DEFAULT_TENANT_FEATURES };
        this.intervals = [];
        this.probeTimers = new Map();
        this.probeInFlight = new Set();
        this.probeResultBuffers = new Map();
        this.probeFlushTimer = null;
        this.liveMonitoring = false;
        this.shuttingDown = false;
        this.configUIServer = null;
        this.logBuffer = [];
        this.speedtestInstallAttempted = false;
        this.speedtestLastInstallAttemptMs = 0;
        this.wingetInstallAttempted = false;
        this.wingetLastInstallAttemptMs = 0;
        this.rustdeskAutoInstallAttempted = false;
        this.rustdeskLastInstallAttemptMs = 0;
        this.rustdeskServicePrepared = false;
        this.rustdeskCachedId = '';
        this.rustdeskCachedIdAt = 0;
        this.wireguardInstallAttempted = false;
        this.wireguardLastInstallAttemptMs = 0;
        this.wireguardDesired = false;
        this.wireguardLastPayload = null;
        this.wireguardRecoveryTimer = null;
        this.wireguardRecoveryInProgress = false;
        this.wireguardLastRecoveryAttemptMs = 0;
        this.networkFingerprint = '';
        this.publicIpTelemetry = null;
        this.publicIpLastRefreshAt = 0;
        this.publicIpRefreshInFlight = null;
        this.softwareBlockCooldown = new Map();
        this.softwareBlockNoticeCooldown = new Map();
        this.collectorPauseTimer = null;
        this.singletonLockFd = null;
        this.singletonLockPath = '';
        this.singletonLockOwned = false;
        // WireGuard state — persisted across handler calls
        this.wgActive = false; // true once tunnel is up and WS switched to WG URL
        this.wgHealthMonitor = null;
        this.wgDeployUrl = ''; // original HTTP URL used to deploy the agent
        this.wgDeployWsUrl = ''; // original WS URL used to deploy the agent
        // ---- Remote Session Handlers ----
        this.activeSessions = new Map();
        this.remoteShellProcesses = new Map();
        this.relaySockets = new Map();
        this.relayCloseSuppressed = new Set();
        this.emRemoteFrameTimers = new Map();
        this.emRemoteFrameInFlight = new Set();
        this.emRemoteFrameFailureLogAt = new Map();
        /**
         * Describes what this agent did to a local service so it can be cleaned up
         * when the session ends.
         */
        this.remoteServiceState = new Map();
        this.wsClient = new client_1.AgentWSClient();
        this.systemCollector = new system_1.SystemCollector();
        this.processCollector = new process_1.ProcessCollector();
        this.productivityCollector = new productivity_1.ProductivityCollector();
        this.networkCollector = new network_1.NetworkCollector();
        this.systemEventsCollector = new system_events_1.SystemEventsCollector();
        this.softwareCollector = new software_1.SoftwareCollector();
        this.loginSessionCollector = new login_sessions_1.LoginSessionCollector();
        this.browserHistoryCollector = new browser_history_1.BrowserHistoryCollector();
        this.probeRunner = new probe_runner_1.ProbeRunner();
        this.upgradeService = new upgrade_1.UpgradeService(this.wsClient);
        this.watchdog = new watchdog_1.WatchdogService({ enableOsWatchdog: true }, logger_1.logger);
        this.securityScanner = new security_scanner_1.SecurityScanner();
        this.xdrScanner = new xdr_scanner_1.XdrScanner();
        this.netIfaceCollector = new network_interfaces_1.NetworkInterfaceCollector();
        this.locationCollector = new location_1.AgentLocationCollector();
    }
    normalizeTransportMode(value) {
        const raw = String(value || '').trim().toLowerCase();
        if (raw === 'force_wireguard' || raw === 'force-wireguard')
            return 'force_wireguard';
        if (raw === 'force_wss' || raw === 'force-wss')
            return 'force_wss';
        return 'auto';
    }
    normalizeChannelPriority(value) {
        const defaults = ['wireguard', 'primary', 'secondary'];
        const rawItems = Array.isArray(value)
            ? value.map((v) => String(v || '').trim().toLowerCase())
            : String(value || '').split(',').map((v) => v.trim().toLowerCase()).filter(Boolean);
        const normalized = [];
        for (const item of rawItems) {
            if ((item === 'wireguard' || item === 'primary' || item === 'secondary') && !normalized.includes(item)) {
                normalized.push(item);
            }
        }
        for (const fallback of defaults) {
            if (!normalized.includes(fallback))
                normalized.push(fallback);
        }
        return normalized;
    }
    deriveWsUrlFromHttp(httpUrl, fallbackWsUrl) {
        try {
            const parsed = new url_1.URL(String(httpUrl || '').trim());
            const wsProto = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
            let wsPort = '';
            try {
                const fallback = new url_1.URL(String(fallbackWsUrl || '').trim());
                wsPort = fallback.port || '';
            }
            catch { }
            if (!wsPort)
                wsPort = wsProto === 'wss:' ? '443' : '80';
            return `${wsProto}//${parsed.hostname}${wsPort ? `:${wsPort}` : ''}`;
        }
        catch {
            return String(fallbackWsUrl || '').trim();
        }
    }
    extractDomain(urlValue) {
        try {
            return new url_1.URL(String(urlValue || '').trim()).hostname || '';
        }
        catch {
            return '';
        }
    }
    normalizeSoftwarePolicyMatchType(value) {
        const raw = String(value || '').trim().toLowerCase();
        if (raw === 'keyword' || raw === 'regex')
            return raw;
        return 'exact';
    }
    normalizeSoftwarePolicyAction(value) {
        const raw = String(value || '').trim().toLowerCase();
        if (raw === 'allow' || raw === 'log' || raw === 'warn')
            return raw;
        return 'block';
    }
    parsePolicyTimeCondition(value) {
        if (value && typeof value === 'object')
            return value;
        if (typeof value === 'string' && value.trim()) {
            try {
                const parsed = JSON.parse(value);
                return parsed && typeof parsed === 'object' ? parsed : {};
            }
            catch {
                return {};
            }
        }
        return {};
    }
    parseTimeToMinutes(value) {
        const match = String(value || '').trim().match(/^(\d{1,2}):(\d{2})$/);
        if (!match)
            return null;
        const hours = Number(match[1]);
        const minutes = Number(match[2]);
        if (!Number.isFinite(hours) || !Number.isFinite(minutes))
            return null;
        if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59)
            return null;
        return (hours * 60) + minutes;
    }
    isPolicyTimeConditionActive(conditionInput) {
        const condition = conditionInput && typeof conditionInput === 'object' ? conditionInput : {};
        if (!condition || typeof condition !== 'object')
            return true;
        if (condition.allDay !== false && !condition.startTime && !condition.endTime && !Array.isArray(condition.days)) {
            return true;
        }
        const now = new Date();
        if (Array.isArray(condition.days) && condition.days.length > 0) {
            const allowedDays = condition.days
                .map((day) => Number(day))
                .filter((day) => Number.isFinite(day) && day >= 0 && day <= 6);
            if (allowedDays.length > 0 && !allowedDays.includes(now.getDay())) {
                return false;
            }
        }
        if (condition.allDay !== false) {
            return true;
        }
        const startMinutes = this.parseTimeToMinutes(condition.startTime);
        const endMinutes = this.parseTimeToMinutes(condition.endTime);
        if (startMinutes === null || endMinutes === null)
            return true;
        if (startMinutes === endMinutes)
            return true;
        const currentMinutes = (now.getHours() * 60) + now.getMinutes();
        if (startMinutes < endMinutes) {
            return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
        }
        return currentMinutes >= startMinutes || currentMinutes <= endMinutes;
    }
    normalizeSoftwarePolicies(policies) {
        const normalized = (Array.isArray(policies) ? policies : [])
            .map((policy) => ({
            id: String(policy?.id || '').trim(),
            pattern: String(policy?.pattern || '').trim(),
            matchType: this.normalizeSoftwarePolicyMatchType(policy?.matchType || policy?.match_type),
            action: this.normalizeSoftwarePolicyAction(policy?.action),
            enabled: policy?.enabled !== false,
            policyId: String(policy?.policyId || policy?.policy_id || '').trim(),
            policyName: String(policy?.policyName || policy?.policy_name || '').trim(),
            appliesTo: Array.isArray(policy?.appliesTo)
                ? policy.appliesTo
                : (Array.isArray(policy?.applies_to) ? policy.applies_to : []),
            timeCondition: this.parsePolicyTimeCondition(policy?.timeCondition || policy?.time_condition),
            title: String(policy?.title || policy?.softwareTitle || '').trim(),
            message: String(policy?.message || policy?.softwareMessage || '').trim(),
            priority: Number(policy?.priority || 100) || 100,
            sortOrder: Number(policy?.sortOrder || policy?.sort_order || 0) || 0,
            createdAt: String(policy?.createdAt || policy?.created_at || '').trim(),
        }))
            .filter((policy) => policy.id && policy.pattern && policy.enabled)
            .sort((left, right) => ((left.priority || 100) - (right.priority || 100)
            || (left.sortOrder || 0) - (right.sortOrder || 0)
            || String(left.createdAt || '').localeCompare(String(right.createdAt || ''))
            || left.id.localeCompare(right.id)));
        return normalized;
    }
    getSoftwarePolicyCandidates(proc) {
        const commandFirstToken = (() => {
            const raw = String(proc.command || '').trim();
            if (!raw)
                return '';
            const unquoted = raw.startsWith('"')
                ? raw.slice(1).split('"')[0]
                : raw.split(/\s+/)[0];
            return unquoted.trim();
        })();
        const basename = (value) => {
            const raw = String(value || '').trim();
            if (!raw)
                return '';
            try {
                return path_1.default.basename(raw);
            }
            catch {
                return raw.split(/[\\/]/).pop() || raw;
            }
        };
        return Array.from(new Set([
            String(proc.name || '').trim(),
            basename(String(proc.path || '').trim()),
            basename(commandFirstToken),
            String(proc.path || '').trim(),
            String(proc.command || '').trim(),
        ].filter(Boolean)));
    }
    matchesSoftwarePolicy(proc, policy) {
        if (!policy.enabled || !this.isPolicyTimeConditionActive(policy.timeCondition))
            return false;
        const pattern = String(policy.pattern || '').trim();
        if (!pattern)
            return false;
        const candidates = this.getSoftwarePolicyCandidates(proc);
        const lowerPattern = pattern.toLowerCase();
        if (policy.matchType === 'regex') {
            try {
                const regex = new RegExp(pattern, 'i');
                return candidates.some((candidate) => regex.test(candidate));
            }
            catch {
                return false;
            }
        }
        if (policy.matchType === 'keyword') {
            return candidates.some((candidate) => candidate.toLowerCase().includes(lowerPattern));
        }
        return candidates.some((candidate) => candidate.toLowerCase() === lowerPattern);
    }
    evaluateSoftwarePolicy(proc) {
        for (const policy of this.softwarePolicies) {
            if (!this.matchesSoftwarePolicy(proc, policy))
                continue;
            return { action: policy.action, rule: policy };
        }
        return { action: 'allow' };
    }
    async enforceSoftwarePolicies(processes) {
        if (!Array.isArray(processes) || processes.length === 0 || this.softwarePolicies.length === 0)
            return;
        const now = Date.now();
        for (const [key, expiry] of this.softwareBlockCooldown.entries()) {
            if (expiry <= now)
                this.softwareBlockCooldown.delete(key);
        }
        for (const [key, expiry] of this.softwareBlockNoticeCooldown.entries()) {
            if (expiry <= now)
                this.softwareBlockNoticeCooldown.delete(key);
        }
        for (const proc of processes) {
            const evaluation = this.evaluateSoftwarePolicy(proc);
            if (!evaluation.rule || evaluation.action !== 'block')
                continue;
            const cooldownKey = `${evaluation.rule.id}:${proc.pid}`;
            const cooldownUntil = this.softwareBlockCooldown.get(cooldownKey) || 0;
            if (cooldownUntil > now)
                continue;
            this.softwareBlockCooldown.set(cooldownKey, now + 15000);
            const success = await this.processCollector.terminate(proc.pid, 'SIGKILL');
            logger_1.logger.warn(success ? 'Blocked software process terminated' : 'Blocked software process termination failed', {
                pid: proc.pid,
                processName: proc.name,
                path: proc.path,
                command: proc.command,
                policyId: evaluation.rule.policyId || null,
                policyName: evaluation.rule.policyName || null,
                ruleId: evaluation.rule.id,
                title: evaluation.rule.title || null,
                message: evaluation.rule.message || null,
            });
            const noticeKey = `${evaluation.rule.id}:${String(proc.path || proc.name || '').trim().toLowerCase()}`;
            const noticeCooldownUntil = this.softwareBlockNoticeCooldown.get(noticeKey) || 0;
            if (noticeCooldownUntil <= now) {
                this.softwareBlockNoticeCooldown.set(noticeKey, now + 30000);
                void this.notifyBlockedSoftwareAttempt(proc, evaluation.rule, success).catch((error) => {
                    logger_1.logger.warn('Blocked software notification failed', {
                        pid: proc.pid,
                        processName: proc.name,
                        ruleId: evaluation.rule?.id || null,
                        error: String(error?.message || error || 'Unknown notification failure'),
                    });
                });
            }
        }
    }
    async notifyBlockedSoftwareAttempt(proc, rule, terminated) {
        const processName = String(proc.name || proc.path || 'This application').trim() || 'This application';
        const title = String(rule.title || 'AppStats Software Blocked').trim() || 'AppStats Software Blocked';
        const baseMessage = String(rule.message || '').trim()
            || `${processName} is blocked by your organization policy and cannot be used on this device.`;
        const notice = terminated
            ? baseMessage
            : `${baseMessage} AppStats could not terminate the process automatically.`;
        if (process.platform === 'win32') {
            // Build a Windows Toast Notification script — works cross-session from a Windows service.
            // Uses PowerShell.exe as the registered AppId so no app registration is required.
            const literalTitle = this.escapePowerShellLiteral(title);
            const literalNotice = this.escapePowerShellLiteral(notice);
            const toastScript = [
                "$ErrorActionPreference='Stop'",
                "[Windows.UI.Notifications.ToastNotificationManager,Windows.UI.Notifications,ContentType=WindowsRuntime]|Out-Null",
                "[Windows.Data.Xml.Dom.XmlDocument,Windows.Data.Xml.Dom,ContentType=WindowsRuntime]|Out-Null",
                `$title='${literalTitle}'`,
                `$msg='${literalNotice}'`,
                '$xml=New-Object Windows.Data.Xml.Dom.XmlDocument',
                '$xml.LoadXml("<toast duration=\\"long\\"><visual><binding template=\\"ToastGeneric\\"><text>$($title)</text><text>$($msg)</text></binding></visual></toast>")',
                '$appId="{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe"',
                'try{[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show([Windows.UI.Notifications.ToastNotification]::new($xml))}catch{',
                '  $ws=New-Object -ComObject WScript.Shell',
                `  $null=$ws.Popup('${literalNotice}',20,'${literalTitle}',48)`,
                '}',
            ].join('\n');
            // runPowerShellInteractiveTask delivers via schtasks /RU INTERACTIVE so it reaches the user desktop
            await this.runPowerShellInteractiveTask(toastScript, 20000);
            return;
        }
        if (process.platform === 'linux') {
            try {
                await (0, linux_desktop_1.runLinuxDesktopCommand)(`command -v notify-send >/dev/null 2>&1 && notify-send ${(0, linux_desktop_1.quoteShellArg)(title)} ${(0, linux_desktop_1.quoteShellArg)(notice)} -u critical -t 20000`, 8000);
                return;
            }
            catch {
                // fall back to wall for headless sessions
            }
            const shellSafeNotice = `${title}: ${notice}`.replace(/"/g, '\\"');
            await this.runShellCommand(`wall "${shellSafeNotice}"`, 6000);
            return;
        }
    }
    getReportingRuntimeState() {
        const normalizeEndpoint = (value) => String(value || '').trim().replace(/\/+$/, '');
        const primaryServerUrl = normalizeEndpoint(String(config_1.config.server.primaryUrl || this.wgDeployUrl || config_1.config.server.url || ''));
        const primaryWsUrl = normalizeEndpoint(String(config_1.config.server.primaryWsUrl || this.wgDeployWsUrl || config_1.config.server.wsUrl || ''));
        const secondaryServerUrl = normalizeEndpoint(String(config_1.config.server.secondaryUrl || ''));
        const secondaryWsUrl = normalizeEndpoint(String(config_1.config.server.secondaryWsUrl || ''));
        const wireguardServerUrl = normalizeEndpoint(String(config_1.config.server.wireguardUrl || ''));
        const wireguardWsUrl = normalizeEndpoint(String(config_1.config.server.wireguardWsUrl || ''));
        const currentServerUrl = normalizeEndpoint(String(config_1.config.server.url || ''));
        const currentWsUrl = normalizeEndpoint(String(config_1.config.server.wsUrl || ''));
        const transportMode = this.normalizeTransportMode(config_1.config.server.transportMode);
        const channelPriority = this.normalizeChannelPriority(config_1.config.server.channelPriority);
        let activeReportingChannel = 'unknown';
        if (this.wgActive && ((wireguardWsUrl && currentWsUrl === wireguardWsUrl) || (wireguardServerUrl && currentServerUrl === wireguardServerUrl))) {
            activeReportingChannel = 'wireguard';
        }
        else if ((secondaryWsUrl && currentWsUrl === secondaryWsUrl) || (secondaryServerUrl && currentServerUrl === secondaryServerUrl)) {
            activeReportingChannel = 'secondary';
        }
        else if ((primaryWsUrl && currentWsUrl === primaryWsUrl) || (primaryServerUrl && currentServerUrl === primaryServerUrl)) {
            activeReportingChannel = 'primary';
        }
        return {
            primaryServerUrl,
            primaryWsUrl,
            secondaryServerUrl,
            secondaryWsUrl,
            transportMode,
            channelPriority,
            wireguardActive: this.wgActive,
            wireguardServerUrl,
            wireguardWsUrl,
            activeReportingChannel,
            primaryDataPlane: activeReportingChannel === 'wireguard' ? 'wireguard' : 'internet',
            primaryDomain: this.extractDomain(primaryServerUrl),
            secondaryDomain: this.extractDomain(secondaryServerUrl),
            wireguardDomain: this.extractDomain(wireguardServerUrl),
        };
    }
    persistRuntimeReportingConfig() {
        try {
            const configFile = path_1.default.join(path_1.default.dirname(process.execPath), 'agent-config.json');
            const existing = fs_1.default.existsSync(configFile) ? JSON.parse(fs_1.default.readFileSync(configFile, 'utf8')) : {};
            fs_1.default.writeFileSync(configFile, JSON.stringify({
                ...existing,
                serverUrl: config_1.config.server.url,
                wsUrl: config_1.config.server.wsUrl,
                primaryServerUrl: config_1.config.server.primaryUrl || this.wgDeployUrl || config_1.config.server.url,
                primaryWsUrl: config_1.config.server.primaryWsUrl || this.wgDeployWsUrl || config_1.config.server.wsUrl,
                secondaryServerUrl: config_1.config.server.secondaryUrl || '',
                secondaryWsUrl: config_1.config.server.secondaryWsUrl || '',
                transportMode: this.normalizeTransportMode(config_1.config.server.transportMode),
                channelPriority: this.normalizeChannelPriority(config_1.config.server.channelPriority),
                wireguardUrl: String(config_1.config.server.wireguardUrl || '').trim(),
                wireguardWsUrl: String(config_1.config.server.wireguardWsUrl || '').trim(),
                wireguardActive: config_1.config.server.wireguardActive === true,
            }, null, 2));
        }
        catch { }
    }
    applyReportingPolicy(reconnectIfChanged) {
        const mode = this.normalizeTransportMode(config_1.config.server.transportMode);
        const priority = this.normalizeChannelPriority(config_1.config.server.channelPriority);
        config_1.config.server.transportMode = mode;
        config_1.config.server.channelPriority = priority;
        config_1.config.server.wireguardActive = this.wgActive;
        let ordered = [...priority];
        if (mode === 'force_wss') {
            ordered = ordered.filter((name) => name !== 'wireguard');
        }
        else if (mode === 'force_wireguard') {
            ordered = ['wireguard', ...ordered.filter((name) => name !== 'wireguard')];
        }
        const primaryHttp = String(config_1.config.server.primaryUrl || this.wgDeployUrl || config_1.config.server.url || '').trim();
        const primaryWs = String(config_1.config.server.primaryWsUrl || this.wgDeployWsUrl || config_1.config.server.wsUrl || '').trim();
        const secondaryHttp = String(config_1.config.server.secondaryUrl || '').trim();
        const secondaryWs = String(config_1.config.server.secondaryWsUrl || '').trim();
        const wireguardHttp = String(config_1.config.server.wireguardUrl || '').trim();
        const wireguardWs = String(config_1.config.server.wireguardWsUrl || '').trim();
        const channels = {
            wireguard: {
                httpUrl: wireguardHttp,
                wsUrl: wireguardWs,
                available: this.wgActive && !!wireguardHttp && !!wireguardWs,
            },
            primary: {
                httpUrl: primaryHttp,
                wsUrl: primaryWs,
                available: !!primaryHttp && !!primaryWs,
            },
            secondary: {
                httpUrl: secondaryHttp,
                wsUrl: secondaryWs,
                available: !!secondaryHttp && !!secondaryWs,
            },
        };
        let selected = null;
        for (const channel of ordered) {
            if (!channels[channel].available)
                continue;
            selected = channel;
            break;
        }
        if (!selected) {
            if (channels.primary.available)
                selected = 'primary';
            else if (channels.secondary.available)
                selected = 'secondary';
            else if (channels.wireguard.available)
                selected = 'wireguard';
        }
        if (!selected)
            return;
        const target = channels[selected];
        const prevHttp = String(config_1.config.server.url || '').trim();
        const prevWs = String(config_1.config.server.wsUrl || '').trim();
        config_1.config.server.url = target.httpUrl;
        config_1.config.server.wsUrl = target.wsUrl;
        this.wsClient.setPreferredWsUrl(target.wsUrl);
        if (reconnectIfChanged && (prevHttp !== target.httpUrl || prevWs !== target.wsUrl)) {
            logger_1.logger.info('Reporting channel switched', {
                channel: selected,
                mode,
                wsUrl: target.wsUrl,
            });
            this.wsClient.reconnect(target.wsUrl);
        }
    }
    syncReportingChannelFromSocketUrl(socketWsUrl) {
        const connectedWsUrl = String(socketWsUrl || '').trim();
        if (!connectedWsUrl)
            return;
        const reporting = this.getReportingRuntimeState();
        let matchedChannel = null;
        let httpUrl = '';
        if (reporting.wireguardWsUrl && connectedWsUrl === reporting.wireguardWsUrl) {
            matchedChannel = 'wireguard';
            httpUrl = reporting.wireguardServerUrl;
            this.wgActive = true;
            config_1.config.server.wireguardActive = true;
        }
        else if (reporting.secondaryWsUrl && connectedWsUrl === reporting.secondaryWsUrl) {
            matchedChannel = 'secondary';
            httpUrl = reporting.secondaryServerUrl;
        }
        else if (reporting.primaryWsUrl && connectedWsUrl === reporting.primaryWsUrl) {
            matchedChannel = 'primary';
            httpUrl = reporting.primaryServerUrl;
        }
        if (!matchedChannel)
            return;
        const prevHttp = String(config_1.config.server.url || '').trim();
        const prevWs = String(config_1.config.server.wsUrl || '').trim();
        config_1.config.server.wsUrl = connectedWsUrl;
        if (httpUrl) {
            config_1.config.server.url = httpUrl;
        }
        if (prevWs !== connectedWsUrl || (httpUrl && prevHttp !== httpUrl)) {
            logger_1.logger.info('Reporting channel synchronized', {
                channel: matchedChannel,
                wsUrl: connectedWsUrl,
            });
            this.persistRuntimeReportingConfig();
        }
    }
    applyCustomReportingConfig(customConfig) {
        if (!customConfig || typeof customConfig !== 'object')
            return;
        const cfg = customConfig;
        let reportingConfigChanged = false;
        const setServerField = (field, nextValue) => {
            const normalized = String(nextValue || '').trim();
            const current = String(config_1.config.server[field] || '').trim();
            if (!normalized || current === normalized)
                return;
            config_1.config.server[field] = normalized;
            reportingConfigChanged = true;
        };
        const setPriorityField = (nextValue) => {
            const current = this.normalizeChannelPriority(config_1.config.server.channelPriority);
            if (current.join(',') === nextValue.join(','))
                return;
            config_1.config.server.channelPriority = nextValue;
            reportingConfigChanged = true;
        };
        const primaryServerUrl = String(cfg.primaryServerUrl ?? cfg.primary_server_url ?? '').trim();
        const primaryWsUrl = String(cfg.primaryWsUrl ?? cfg.primary_ws_url ?? '').trim();
        const secondaryServerUrl = String(cfg.secondaryServerUrl ?? cfg.secondary_server_url ?? '').trim();
        const secondaryWsUrl = String(cfg.secondaryWsUrl ?? cfg.secondary_ws_url ?? '').trim();
        const modeInput = cfg.transportMode ?? cfg.transport_mode;
        const priorityInput = cfg.channelPriority ?? cfg.channel_priority;
        if (primaryServerUrl) {
            setServerField('primaryUrl', primaryServerUrl.replace(/\/+$/, ''));
            if (!primaryWsUrl) {
                setServerField('primaryWsUrl', this.deriveWsUrlFromHttp(config_1.config.server.primaryUrl, config_1.config.server.primaryWsUrl || this.wgDeployWsUrl || config_1.config.server.wsUrl));
            }
        }
        if (primaryWsUrl) {
            setServerField('primaryWsUrl', primaryWsUrl);
        }
        if (secondaryServerUrl) {
            setServerField('secondaryUrl', secondaryServerUrl.replace(/\/+$/, ''));
            if (!secondaryWsUrl) {
                setServerField('secondaryWsUrl', this.deriveWsUrlFromHttp(config_1.config.server.secondaryUrl, config_1.config.server.secondaryWsUrl || config_1.config.server.primaryWsUrl || config_1.config.server.wsUrl));
            }
        }
        if (secondaryWsUrl) {
            setServerField('secondaryWsUrl', secondaryWsUrl);
        }
        if (modeInput !== undefined && modeInput !== null && String(modeInput).trim() !== '') {
            const nextMode = this.normalizeTransportMode(modeInput);
            const currentMode = this.normalizeTransportMode(config_1.config.server.transportMode);
            if (nextMode !== currentMode) {
                config_1.config.server.transportMode = nextMode;
                reportingConfigChanged = true;
            }
        }
        if (priorityInput !== undefined && priorityInput !== null && String(priorityInput).trim() !== '') {
            setPriorityField(this.normalizeChannelPriority(priorityInput));
        }
        this.applyReportingPolicy(reportingConfigChanged);
        if (reportingConfigChanged) {
            this.persistRuntimeReportingConfig();
        }
    }
    parseCollectorInterval(value, min, max) {
        const raw = Number(value);
        if (!Number.isFinite(raw) || raw <= 0)
            return null;
        // UI/config stores collector intervals in seconds, while runtime uses milliseconds.
        // Accept both legacy ms values and new seconds values.
        const interpretedSeconds = (raw > max && raw <= (max * 1000)) ? (raw / 1000) : raw;
        const boundedSeconds = Math.max(min, Math.min(max, Math.floor(interpretedSeconds)));
        return boundedSeconds * 1000;
    }
    getSingletonLockCandidates() {
        const configuredPath = String(process.env.APPSTATS_SINGLETON_LOCK_PATH || '').trim();
        const fallbackPath = process.platform === 'win32'
            ? path_1.default.join(String(process.env.ProgramData || 'C:\\ProgramData').trim() || 'C:\\ProgramData', 'AppStats', 'appstats-agent.lock')
            : '/tmp/appstats-agent.lock';
        const tempPath = path_1.default.join(os_1.default.tmpdir(), 'appstats-agent.lock');
        return Array.from(new Set([configuredPath, fallbackPath, tempPath].map((v) => String(v || '').trim()).filter(Boolean)));
    }
    readLockPid(lockPath) {
        const metadata = this.readLockMetadata(lockPath);
        return metadata.pid;
    }
    readLockMetadata(lockPath) {
        try {
            const raw = String(fs_1.default.readFileSync(lockPath, 'utf8') || '').trim();
            if (!raw)
                return { pid: null, version: '', hostname: '' };
            try {
                const parsed = JSON.parse(raw);
                const pidFromJson = Number(parsed?.pid);
                return {
                    pid: Number.isFinite(pidFromJson) && pidFromJson > 0 ? pidFromJson : null,
                    version: String(parsed?.version || '').trim(),
                    hostname: String(parsed?.hostname || '').trim(),
                };
            }
            catch { }
            const pid = Number(raw.split(/\s+/)[0]);
            return {
                pid: Number.isFinite(pid) && pid > 0 ? pid : null,
                version: '',
                hostname: '',
            };
        }
        catch {
            return { pid: null, version: '', hostname: '' };
        }
    }
    shouldReplaceLockedAgent(lockPath, metadata) {
        const pid = Number(metadata.pid);
        if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid)
            return false;
        const lockedVersion = String(metadata.version || '').trim();
        if (!semver_1.default.valid(VERSION) || !semver_1.default.valid(lockedVersion))
            return false;
        if (!semver_1.default.gt(VERSION, lockedVersion))
            return false;
        logger_1.logger.warn(`Newer AppStats agent v${VERSION} detected older locked instance v${lockedVersion}; replacing pid ${pid}`, {
            lockPath,
            currentVersion: VERSION,
            lockedVersion,
            lockedPid: pid,
            lockedHostname: metadata.hostname || undefined,
        });
        return true;
    }
    blockingSleep(ms) {
        const delayMs = Math.max(0, Math.floor(ms));
        if (delayMs <= 0)
            return;
        try {
            Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);
            return;
        }
        catch { }
        const until = Date.now() + delayMs;
        while (Date.now() < until) {
            // Busy wait only as a last-resort fallback.
        }
    }
    terminateLockedAgent(metadata) {
        const pid = Number(metadata.pid);
        if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid)
            return false;
        const waitForExit = (timeoutMs) => {
            const deadline = Date.now() + Math.max(250, timeoutMs);
            while (Date.now() < deadline) {
                if (!this.isProcessAlive(pid))
                    return true;
                this.blockingSleep(200);
            }
            return !this.isProcessAlive(pid);
        };
        try {
            process.kill(pid, 'SIGTERM');
        }
        catch (error) {
            logger_1.logger.warn(`Failed to terminate locked AppStats agent pid ${pid}: ${error?.message || String(error)}`);
            return false;
        }
        if (waitForExit(3000))
            return true;
        try {
            process.kill(pid, 'SIGKILL');
        }
        catch (error) {
            logger_1.logger.warn(`Failed to force-kill locked AppStats agent pid ${pid}: ${error?.message || String(error)}`);
            return false;
        }
        return waitForExit(2000);
    }
    isProcessAlive(pid) {
        const targetPid = Number(pid);
        if (!Number.isFinite(targetPid) || targetPid <= 0)
            return false;
        if (targetPid === process.pid)
            return true;
        try {
            process.kill(targetPid, 0);
            return true;
        }
        catch (error) {
            return String(error?.code || '').toUpperCase() === 'EPERM';
        }
    }
    writeLockFile(fd) {
        const payload = JSON.stringify({
            pid: process.pid,
            hostname: config_1.config.agent.hostname,
            version: VERSION,
            startedAt: new Date().toISOString(),
        });
        fs_1.default.ftruncateSync(fd, 0);
        fs_1.default.writeSync(fd, `${payload}\n`, 0, 'utf8');
        fs_1.default.fsyncSync(fd);
    }
    acquireSingletonLock() {
        const candidates = this.getSingletonLockCandidates();
        for (const lockPath of candidates) {
            try {
                fs_1.default.mkdirSync(path_1.default.dirname(lockPath), { recursive: true });
            }
            catch { }
            const attemptAcquire = () => {
                const fd = fs_1.default.openSync(lockPath, 'wx');
                this.singletonLockFd = fd;
                this.singletonLockPath = lockPath;
                this.singletonLockOwned = true;
                this.writeLockFile(fd);
                logger_1.logger.info(`Singleton lock acquired: ${lockPath}`);
                return true;
            };
            try {
                if (attemptAcquire())
                    return true;
            }
            catch (error) {
                const code = String(error?.code || '').toUpperCase();
                if (code === 'EEXIST') {
                    const existingMetadata = this.readLockMetadata(lockPath);
                    const existingPid = existingMetadata.pid;
                    if (existingPid && this.isProcessAlive(existingPid) && existingPid !== process.pid) {
                        if (this.shouldReplaceLockedAgent(lockPath, existingMetadata) && this.terminateLockedAgent(existingMetadata)) {
                            try {
                                fs_1.default.unlinkSync(lockPath);
                            }
                            catch { }
                            try {
                                if (attemptAcquire())
                                    return true;
                            }
                            catch {
                                // Continue through normal duplicate handling below.
                            }
                        }
                        logger_1.logger.error(`Another AppStats agent process is already running (pid ${existingPid}); refusing duplicate start.`);
                        return false;
                    }
                    try {
                        fs_1.default.unlinkSync(lockPath);
                    }
                    catch { }
                    try {
                        if (attemptAcquire())
                            return true;
                    }
                    catch {
                        // Continue to next candidate.
                    }
                    continue;
                }
                logger_1.logger.warn(`Failed to use singleton lock path '${lockPath}': ${error?.message || String(error)}`);
            }
        }
        logger_1.logger.warn('Singleton lock could not be established; continuing without local duplicate guard.');
        return true;
    }
    releaseSingletonLock() {
        if (!this.singletonLockOwned)
            return;
        const lockPath = this.singletonLockPath;
        const fd = this.singletonLockFd;
        this.singletonLockOwned = false;
        this.singletonLockFd = null;
        this.singletonLockPath = '';
        if (fd !== null) {
            try {
                fs_1.default.closeSync(fd);
            }
            catch { }
        }
        if (!lockPath)
            return;
        try {
            const pidFromFile = this.readLockPid(lockPath);
            if (!pidFromFile || pidFromFile === process.pid) {
                fs_1.default.unlinkSync(lockPath);
            }
        }
        catch { }
    }
    applyCustomCollectorIntervals(customConfig) {
        if (!customConfig || typeof customConfig !== 'object')
            return false;
        const cfg = customConfig;
        let changed = false;
        const apply = (key, value, min, max) => {
            const next = this.parseCollectorInterval(value, min, max);
            if (next === null)
                return;
            if (config_1.config.intervals[key] !== next) {
                config_1.config.intervals[key] = next;
                changed = true;
            }
        };
        apply('system', cfg.system_interval ?? cfg.systemInterval, 5, 3600);
        apply('process', cfg.process_interval ?? cfg.processInterval, 10, 3600);
        apply('network', cfg.network_interval ?? cfg.networkInterval, 5, 3600);
        apply('wifi', cfg.wifi_interval ?? cfg.wifiInterval, 5, 3600);
        return changed;
    }
    normalizeTenantFeatures(input) {
        const raw = (input && typeof input === 'object') ? input : {};
        const toBool = (value, fallback) => {
            if (value === undefined || value === null || value === '')
                return fallback;
            if (typeof value === 'boolean')
                return value;
            if (typeof value === 'number')
                return value !== 0;
            const normalized = String(value).trim().toLowerCase();
            if (['0', 'false', 'no', 'off', 'disabled'].includes(normalized))
                return false;
            if (['1', 'true', 'yes', 'on', 'enabled'].includes(normalized))
                return true;
            return fallback;
        };
        return {
            productivityMonitoring: toBool(raw.productivityMonitoring, DEFAULT_TENANT_FEATURES.productivityMonitoring),
            browserMonitoring: toBool(raw.browserMonitoring, DEFAULT_TENANT_FEATURES.browserMonitoring),
            screenshots: toBool(raw.screenshots, DEFAULT_TENANT_FEATURES.screenshots),
            processMonitoring: toBool(raw.processMonitoring, DEFAULT_TENANT_FEATURES.processMonitoring),
            systemEvents: toBool(raw.systemEvents, DEFAULT_TENANT_FEATURES.systemEvents),
            networkMonitoring: toBool(raw.networkMonitoring, DEFAULT_TENANT_FEATURES.networkMonitoring),
            remoteAccess: toBool(raw.remoteAccess, DEFAULT_TENANT_FEATURES.remoteAccess),
            softwareManagement: toBool(raw.softwareManagement, DEFAULT_TENANT_FEATURES.softwareManagement),
            speedtest: toBool(raw.speedtest, DEFAULT_TENANT_FEATURES.speedtest),
            probes: toBool(raw.probes, DEFAULT_TENANT_FEATURES.probes),
        };
    }
    applyTenantFeatureConfig(input) {
        const next = this.normalizeTenantFeatures(input);
        const current = this.tenantFeatures;
        const changed = Object.keys(DEFAULT_TENANT_FEATURES).some((key) => (current[key] !== next[key]));
        if (changed) {
            this.tenantFeatures = next;
            logger_1.logger.info('Updated tenant feature runtime state', next);
        }
        return changed;
    }
    clearCollectorPauseTimer() {
        if (this.collectorPauseTimer) {
            clearTimeout(this.collectorPauseTimer);
            this.collectorPauseTimer = null;
        }
    }
    pauseCollectorsForOffline(reason) {
        if (this.intervals.length < 1)
            return;
        this.intervals.forEach((interval) => clearInterval(interval));
        this.intervals = [];
        logger_1.logger.warn('Paused collectors while agent is offline', { reason });
    }
    scheduleCollectorPauseIfOffline(reason, delayMs = 45000) {
        this.clearCollectorPauseTimer();
        const waitMs = Math.max(5000, Math.min(5 * 60 * 1000, Math.floor(delayMs || 45000)));
        this.collectorPauseTimer = setTimeout(() => {
            this.collectorPauseTimer = null;
            if (this.wsClient.isConnected())
                return;
            this.pauseCollectorsForOffline(reason);
        }, waitMs);
    }
    setWireGuardDesiredState(enabled, payload) {
        this.wireguardDesired = enabled === true;
        if (!this.wireguardDesired) {
            this.wireguardLastPayload = null;
            if (this.wireguardRecoveryTimer) {
                clearTimeout(this.wireguardRecoveryTimer);
                this.wireguardRecoveryTimer = null;
            }
            this.wireguardRecoveryInProgress = false;
            return;
        }
        const serverConfig = payload?.serverConfig || payload?.server_config;
        const peerConfig = payload?.peerConfig || payload?.peer_config;
        if (serverConfig && peerConfig) {
            this.wireguardLastPayload = { enabled: true, serverConfig, peerConfig };
        }
    }
    scheduleWireGuardRecovery(reason, delayMs = 15000) {
        if (!this.wireguardDesired || !this.wireguardLastPayload)
            return;
        if (this.wireguardRecoveryTimer || this.wireguardRecoveryInProgress)
            return;
        const cooldownMs = 45000;
        const now = Date.now();
        if ((now - this.wireguardLastRecoveryAttemptMs) < cooldownMs)
            return;
        const waitMs = Math.max(5000, Math.min(120000, Math.floor(delayMs || 15000)));
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        logger_1.logger.warn(`WireGuard recovery scheduled (${reason}) in ${Math.round(waitMs / 1000)}s`);
        this.wsClient.sendMetrics('wireguard:status', {
            agentId,
            status: 'recovering',
            message: `${reason} (retry in ${Math.round(waitMs / 1000)}s)`,
        });
        this.wireguardRecoveryTimer = setTimeout(async () => {
            this.wireguardRecoveryTimer = null;
            if (!this.wireguardDesired || !this.wireguardLastPayload || this.wireguardRecoveryInProgress)
                return;
            this.wireguardRecoveryInProgress = true;
            this.wireguardLastRecoveryAttemptMs = Date.now();
            try {
                await this.handleWireGuardConfig({
                    ...this.wireguardLastPayload,
                    recoveryReason: reason,
                });
            }
            catch (error) {
                logger_1.logger.warn(`WireGuard recovery attempt failed: ${error?.message || String(error)}`);
            }
            finally {
                this.wireguardRecoveryInProgress = false;
            }
        }, waitMs);
    }
    onNetworkStateChanged(addresses) {
        const ipv4s = Array.from(new Set((Array.isArray(addresses) ? addresses : [])
            .map((value) => String(value || '').trim())
            .filter((value) => /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value))
            .filter((value) => !value.startsWith('127.') && !value.startsWith('169.254.')))).sort();
        if (ipv4s.length < 1)
            return;
        const signature = ipv4s.join('|');
        if (!this.networkFingerprint) {
            this.networkFingerprint = signature;
            return;
        }
        if (this.networkFingerprint === signature)
            return;
        const previous = this.networkFingerprint;
        this.networkFingerprint = signature;
        logger_1.logger.info('Network change detected', { previous, current: signature });
        if (!this.wireguardDesired)
            return;
        this.scheduleWireGuardRecovery('network-change-detected', this.wgActive ? 15000 : 5000);
    }
    async start() {
        logger_1.logger.info(`AppStats Agent v${VERSION} starting...`);
        logger_1.logger.info(`Hostname: ${config_1.config.agent.hostname}`);
        logger_1.logger.info(`Platform: ${config_1.config.agent.platform} ${config_1.config.agent.arch}`);
        logger_1.logger.info(`Server: ${config_1.config.server.wsUrl}`);
        if (!this.acquireSingletonLock()) {
            process.exit(0);
            return;
        }
        process.once('exit', () => this.releaseSingletonLock());
        // ---- Persist the original deploy URL (written once, never overwritten by WG switch) ----
        // This is the URL we always fall back to if the WireGuard tunnel fails.
        this.persistDeployUrl();
        config_1.config.server.primaryUrl = this.wgDeployUrl;
        config_1.config.server.primaryWsUrl = this.wgDeployWsUrl;
        config_1.config.server.transportMode = this.normalizeTransportMode(config_1.config.server.transportMode);
        config_1.config.server.channelPriority = this.normalizeChannelPriority(config_1.config.server.channelPriority);
        config_1.config.server.wireguardUrl = String(config_1.config.server.wireguardUrl || '').trim();
        config_1.config.server.wireguardWsUrl = String(config_1.config.server.wireguardWsUrl || '').trim();
        config_1.config.server.wireguardActive = this.wgActive;
        // Always connect via deploy URL on startup, even if config.json has WG URLs.
        // The WG switch code will switch to the tunnel URL after confirming a handshake.
        if (this.wgDeployWsUrl && this.wgDeployWsUrl !== config_1.config.server.wsUrl) {
            logger_1.logger.info(`Startup: overriding WS URL from ${config_1.config.server.wsUrl} to deploy URL ${this.wgDeployWsUrl}`);
            config_1.config.server.url = this.wgDeployUrl;
            config_1.config.server.wsUrl = this.wgDeployWsUrl;
        }
        this.applyReportingPolicy(false);
        // Intercept logger to buffer log messages for the config UI
        this.setupLogCapture();
        this.setupEventHandlers();
        this.watchdog.start();
        this.upgradeService.sanitizeLocalCache('startup').catch((e) => {
            logger_1.logger.debug(`Startup cache sanitization skipped: ${e?.message || String(e)}`);
        });
        this.wsClient.connect();
        if (process.platform === 'linux') {
            void this.bootstrapRustDeskLinuxDesktop().catch((error) => {
                logger_1.logger.debug('RustDesk Linux desktop bootstrap skipped', {
                    error: this.summarizeRustDeskError(error),
                });
            });
        }
        // Start the local configuration UI (accessible at http://localhost:9898)
        try {
            this.configUIServer = (0, config_ui_1.startConfigUI)({
                getStatus: () => this.getAgentStatus(),
                getConfig: () => this.getAgentConfig(),
                updateConfig: (updates) => this.updateAgentConfig(updates),
                restart: () => this.restartAgent(),
                getLogs: (lines) => this.getRecentLogs(lines),
            });
            logger_1.logger.info('Agent Config UI started on http://localhost:9898');
        }
        catch (e) {
            logger_1.logger.warn(`Could not start Config UI: ${e.message}`);
        }
        // Graceful shutdown
        process.on('SIGINT', () => this.shutdown());
        process.on('SIGTERM', () => this.shutdown());
        process.on('uncaughtException', (err) => {
            logger_1.logger.error('Uncaught exception:', err);
        });
        process.on('unhandledRejection', (err) => {
            logger_1.logger.error('Unhandled rejection:', err);
        });
    }
    // ---- Config UI Interface Methods ----
    setupLogCapture() {
        // Buffer last 500 log lines for the config UI
        const origInfo = logger_1.logger.info.bind(logger_1.logger);
        const origWarn = logger_1.logger.warn.bind(logger_1.logger);
        const origError = logger_1.logger.error.bind(logger_1.logger);
        const addLog = (level, msg) => {
            const line = `[${new Date().toISOString()}] [${level}] ${msg}`;
            this.logBuffer.push(line);
            if (this.logBuffer.length > 500)
                this.logBuffer.shift();
        };
        logger_1.logger.info = ((msg, ...args) => { addLog('INFO', msg); return origInfo(msg, ...args); });
        logger_1.logger.warn = ((msg, ...args) => { addLog('WARN', msg); return origWarn(msg, ...args); });
        logger_1.logger.error = ((msg, ...args) => { addLog('ERROR', msg); return origError(msg, ...args); });
    }
    getAgentStatus() {
        const reporting = this.getReportingRuntimeState();
        const effectiveUrlFilters = this.networkCollector.getURLFilters().map((filter) => ({
            id: String(filter?.id || '').trim(),
            pattern: String(filter?.pattern || '').trim(),
            type: String(filter?.matchType || filter?.type || 'domain').trim().toLowerCase() || 'domain',
            action: String(filter?.action || 'block').trim().toLowerCase() || 'block',
            category: String(filter?.category || '').trim(),
            enabled: filter?.enabled !== false,
            appliesTo: Array.isArray(filter?.appliesTo)
                ? filter.appliesTo
                : (Array.isArray(filter?.applies_to) ? filter.applies_to : []),
            blockedPageEnabled: filter?.blockedPageEnabled !== false,
            blockedPageTitle: String(filter?.blockedPageTitle || '').trim(),
            blockedPageMessage: String(filter?.blockedPageMessage || '').trim(),
            blockedPageRedirectUrl: String(filter?.blockedPageRedirectUrl || '').trim(),
        })).filter((filter) => filter.id && filter.pattern);
        return {
            agentId: config_1.config.agent.hostname,
            agentHostname: config_1.config.agent.hostname,
            hostname: config_1.config.agent.hostname,
            version: VERSION,
            serverUrl: config_1.config.server.url,
            wsUrl: config_1.config.server.wsUrl,
            token: config_1.config.server.token,
            primaryServerUrl: reporting.primaryServerUrl,
            primaryWsUrl: reporting.primaryWsUrl,
            secondaryServerUrl: reporting.secondaryServerUrl,
            secondaryWsUrl: reporting.secondaryWsUrl,
            transportMode: reporting.transportMode,
            channelPriority: reporting.channelPriority,
            wireguardActive: reporting.wireguardActive,
            wireguardServerUrl: reporting.wireguardServerUrl,
            wireguardWsUrl: reporting.wireguardWsUrl,
            activeReportingChannel: reporting.activeReportingChannel,
            primaryDataPlane: reporting.primaryDataPlane,
            primaryDomain: reporting.primaryDomain,
            secondaryDomain: reporting.secondaryDomain,
            wireguardDomain: reporting.wireguardDomain,
            connected: this.wsClient.isConnected?.() || false,
            uptime: process.uptime(),
            pid: process.pid,
            platform: config_1.config.agent.platform,
            arch: config_1.config.agent.arch,
            urlFilters: effectiveUrlFilters,
            watchdog: this.watchdog.getStatus(),
        };
    }
    getAgentConfig() {
        const configPath = process.env.APPSTATS_CONFIG || path_1.default.join(process.cwd(), 'agent-config.json');
        let existing = {};
        try {
            if (fs_1.default.existsSync(configPath)) {
                existing = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
            }
        }
        catch { }
        const reporting = this.getReportingRuntimeState();
        const heartbeatIntervalRaw = Number(existing.heartbeatInterval);
        const heartbeatInterval = Number.isFinite(heartbeatIntervalRaw) && heartbeatIntervalRaw > 0
            ? heartbeatIntervalRaw
            : 30000;
        return {
            ...existing,
            serverUrl: reporting.primaryServerUrl || existing.serverUrl || existing.server?.url || '',
            wsUrl: reporting.primaryWsUrl || existing.wsUrl || existing.server?.wsUrl || '',
            primaryServerUrl: reporting.primaryServerUrl || existing.primaryServerUrl || existing.server?.primaryUrl || '',
            primaryWsUrl: reporting.primaryWsUrl || existing.primaryWsUrl || existing.server?.primaryWsUrl || '',
            secondaryServerUrl: reporting.secondaryServerUrl || existing.secondaryServerUrl || existing.server?.secondaryUrl || '',
            secondaryWsUrl: reporting.secondaryWsUrl || existing.secondaryWsUrl || existing.server?.secondaryWsUrl || '',
            transportMode: reporting.transportMode,
            channelPriority: reporting.channelPriority,
            wireguardActive: reporting.wireguardActive,
            wireguardServerUrl: reporting.wireguardServerUrl,
            wireguardWsUrl: reporting.wireguardWsUrl,
            activeReportingChannel: reporting.activeReportingChannel,
            primaryDataPlane: reporting.primaryDataPlane,
            primaryDomain: reporting.primaryDomain,
            secondaryDomain: reporting.secondaryDomain,
            wireguardDomain: reporting.wireguardDomain,
            token: existing.token || existing.server?.token || config_1.config.server.token,
            tags: Array.isArray(existing.tags) ? existing.tags : (Array.isArray(existing.agent?.tags) ? existing.agent.tags : []),
            groups: Array.isArray(existing.groups) ? existing.groups : (Array.isArray(existing.agent?.groups) ? existing.agent.groups : []),
            collectors: existing.collectors || {
                system: { enabled: true, interval: config_1.config.intervals.system },
                process: { enabled: true, interval: config_1.config.intervals.process },
                productivity: { enabled: true, interval: config_1.config.intervals.productivity },
                network: { enabled: true, interval: config_1.config.intervals.network },
                wifi: { enabled: true, interval: config_1.config.intervals.wifi },
                activity: { enabled: true },
            },
            heartbeatInterval,
        };
    }
    updateAgentConfig(updates) {
        const configPath = process.env.APPSTATS_CONFIG || path_1.default.join(process.cwd(), 'agent-config.json');
        let existing = {};
        try {
            if (fs_1.default.existsSync(configPath)) {
                existing = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
            }
        }
        catch { }
        const merged = { ...existing, ...updates };
        fs_1.default.writeFileSync(configPath, JSON.stringify(merged, null, 2), 'utf-8');
        logger_1.logger.info(`Configuration file updated at ${configPath}`);
    }
    restartAgent() {
        logger_1.logger.info('Agent restart requested via Config UI');
        setTimeout(() => {
            process.exit(0); // The service manager (systemd/node-windows) will restart it
        }, 1000);
    }
    getRecentLogs(lines) {
        const count = lines || 100;
        return this.logBuffer.slice(-count);
    }
    setupEventHandlers() {
        this.wsClient.on('registered', (agentId, serverConfig) => {
            logger_1.logger.info(`Registered with server as: ${agentId}`);
            this.clearCollectorPauseTimer();
            this.startCollectors();
            this.applyServerConfig(serverConfig);
        });
        this.wsClient.on('config-update', (serverConfig) => {
            this.applyServerConfig(serverConfig);
        });
        this.wsClient.on('process-terminate', async (payload) => {
            const { pid, signal, name } = payload;
            logger_1.logger.info(`Terminate request: PID ${pid} (${name}) with ${signal}`);
            const success = await this.processCollector.terminate(pid, signal);
            this.wsClient.sendMetrics('process:terminate:result', {
                pid, name, signal, success,
                timestamp: new Date().toISOString(),
            });
        });
        this.wsClient.on('monitoring-toggle', (payload) => {
            this.liveMonitoring = payload.enabled;
            logger_1.logger.info(`Live monitoring ${this.liveMonitoring ? 'enabled' : 'disabled'}`);
            if (this.liveMonitoring) {
                this.startLiveMonitoring();
            }
        });
        this.wsClient.on('probe-start', (payload) => {
            this.startProbe(payload);
        });
        this.wsClient.on('probe-stop', (payload) => {
            this.stopProbe(payload.probeId);
        });
        this.wsClient.on('upgrade-command', (payload) => {
            if (payload?.action === 'shutdown') {
                const reason = String(payload?.reason || '').trim();
                const duplicateShutdown = /duplicate|superseded|replaced/i.test(reason);
                if (duplicateShutdown) {
                    logger_1.logger.warn(`Ignoring duplicate-instance shutdown command and forcing reconnect: ${reason || 'no reason provided'}`);
                    try {
                        this.wsClient.reconnect();
                    }
                    catch (error) {
                        logger_1.logger.warn(`Failed to force reconnect after duplicate shutdown command: ${error?.message || String(error)}`);
                    }
                    return;
                }
                logger_1.logger.warn(`Received server shutdown command: ${reason || 'duplicate instance handling'}`);
                this.shutdown().finally(() => process.exit(0));
                return;
            }
            this.upgradeService.handleUpgradeCommand(payload);
        });
        this.wsClient.on('upgrade-available', (payload) => {
            this.upgradeService.handleUpgradeAvailable(payload);
        });
        this.wsClient.on('traffic-rules-update', (payload) => {
            this.networkCollector.updateTrafficRules(payload.rules || []);
        });
        this.wsClient.on('traffic-filters-update', (payload) => {
            this.networkCollector.updateURLFilters(payload.filters || []);
        });
        this.wsClient.on('request-system-info', async () => {
            const metrics = await this.systemCollector.collect();
            try {
                const loc = await this.locationCollector.collect();
                if (loc)
                    metrics.location = loc;
            }
            catch { }
            this.attachPublicIpTelemetry(metrics);
            this.wsClient.sendMetrics('system:metrics', metrics);
        });
        this.wsClient.on('request-processes', async () => {
            const procs = await this.processCollector.collect();
            this.wsClient.sendMetrics('process:snapshot', {
                processes: procs.slice(0, 100),
                totalCount: procs.length,
            });
        });
        this.wsClient.on('request-software', async () => {
            await this.sendSoftwareSnapshot('server-request');
        });
        this.wsClient.on('software-uninstall', async (payload) => {
            await this.handleSoftwareUninstall(payload);
        });
        this.wsClient.on('software-install', async (payload) => {
            await this.handleSoftwareInstall(payload);
        });
        this.wsClient.on('software-update', async (payload) => {
            await this.handleSoftwareUpdate(payload);
        });
        this.wsClient.on('mikrotik-command', async (payload) => {
            await this.handleMikroTikCommand(payload);
        });
        this.wsClient.on('mikrotik-discover', async (payload) => {
            await this.handleMikroTikDiscover(payload);
        });
        this.wsClient.on('maintenance-run', async (payload) => {
            await this.handleMaintenanceRun(payload);
        });
        this.wsClient.on('maintenance-schedule', async (payload) => {
            await this.handleMaintenanceSchedule(payload);
        });
        this.wsClient.on('extension-install', async (payload) => {
            this.installBrowserExtension(payload);
        });
        this.wsClient.on('security-scan', async (payload) => {
            this.handleSecurityScan(payload);
        });
        this.wsClient.on('xdr-scan', async (payload) => {
            this.handleXdrScan(payload);
        });
        this.wsClient.on('xdr-block', async (payload) => {
            this.handleXdrBlock(payload);
        });
        this.wsClient.on('xdr-allow', async (payload) => {
            this.handleXdrAllow(payload);
        });
        this.wsClient.on('xdr-settings', async (payload) => {
            this.handleXdrSettings(payload);
        });
        this.wsClient.on('remote-session-start', async (payload) => {
            this.handleRemoteSessionStart(payload);
        });
        this.wsClient.on('remote-session-end', async (payload) => {
            await this.handleRemoteSessionEnd(payload);
        });
        this.wsClient.on('remote-shell-input', async (payload) => {
            await this.handleRemoteShellInput(payload);
        });
        this.wsClient.on('remote-shell-control', async (payload) => {
            await this.handleRemoteShellControl(payload);
        });
        this.wsClient.on('remote-relay-start', async (payload) => {
            await this.handleRemoteRelayStart(payload);
        });
        this.wsClient.on('remote-relay-data', async (payload) => {
            await this.handleRemoteRelayData(payload);
        });
        this.wsClient.on('remote-relay-end', async (payload) => {
            await this.handleRemoteRelayEnd(payload);
        });
        this.wsClient.on('remote-emremote-input', async (payload) => {
            await this.handleEmRemoteInput(payload);
        });
        this.wsClient.on('wireguard-config', async (payload) => {
            this.handleWireGuardConfig(payload);
        });
        this.wsClient.on('speedtest-run', async (payload) => {
            this.handleSpeedtestRun(payload);
        });
        this.wsClient.on('disconnected', () => {
            logger_1.logger.warn('Lost connection to server');
            this.scheduleCollectorPauseIfOffline('websocket-disconnected', 45000);
            if (this.wireguardDesired) {
                this.scheduleWireGuardRecovery('websocket-disconnected', 20000);
            }
        });
        this.wsClient.on('duplicate-instance', (info) => {
            logger_1.logger.warn(`Duplicate instance detected by server (${info?.code || ''} ${info?.reason || ''})`);
            logger_1.logger.warn('Keeping agent process alive and waiting for reconnect recovery');
            this.scheduleCollectorPauseIfOffline('duplicate-instance', 5000);
        });
        this.wsClient.on('connected', (info) => {
            const connectedWsUrl = String(info?.url || this.wsClient.getCurrentWsUrl() || '').trim();
            this.syncReportingChannelFromSocketUrl(connectedWsUrl);
            logger_1.logger.info('Reconnected to server');
            this.clearCollectorPauseTimer();
            this.refreshPublicIpTelemetry(true).catch(() => { });
            // Push buffered activity quickly after reconnect so dashboards repopulate.
            setTimeout(() => {
                if (!this.wsClient.isConnected())
                    return;
                const records = this.productivityCollector.flushRecords();
                if (records.length > 0) {
                    this.wsClient.send({
                        type: 'productivity:batch',
                        payload: records,
                        agentId: this.wsClient.getAgentId() || undefined,
                    });
                }
                const events = this.productivityCollector.flushEvents();
                if (events.length > 0) {
                    this.wsClient.send({
                        type: 'activity:batch',
                        payload: events,
                        agentId: this.wsClient.getAgentId() || undefined,
                    });
                }
                const screenshots = this.productivityCollector.flushScreenshots();
                if (screenshots.length > 0) {
                    this.wsClient.send({
                        type: 'productivity:screenshots',
                        payload: screenshots,
                        agentId: this.wsClient.getAgentId() || undefined,
                    });
                }
                this.sendSoftwareSnapshot('reconnect').catch(() => { });
            }, 2000);
        });
    }
    startCollectors() {
        this.clearCollectorPauseTimer();
        // Clear existing intervals
        this.intervals.forEach(i => clearInterval(i));
        this.intervals = [];
        if (this.tenantFeatures.speedtest) {
            this.ensureSpeedtestCliInstalled().catch(() => { });
        }
        // System metrics collection (5-second default)
        const systemInterval = setInterval(async () => {
            try {
                const metrics = await this.systemCollector.collect();
                // Attach GPS / Windows-Location telemetry when available. The
                // collector is internally cached + rate-limited (2 min on success,
                // 45 s on failure) so calling it on every poll is cheap.
                try {
                    const loc = await this.locationCollector.collect();
                    if (loc)
                        metrics.location = loc;
                }
                catch { }
                this.attachPublicIpTelemetry(metrics);
                this.wsClient.sendMetrics('system:metrics', metrics);
            }
            catch (e) {
                // Skipped - already collecting
            }
        }, config_1.config.intervals.system);
        this.intervals.push(systemInterval);
        // Agent self-monitoring (every 60 seconds)
        let prevCpuUsage = process.cpuUsage();
        let prevCpuTime = Date.now();
        const selfMonitorInterval = setInterval(() => {
            try {
                const memUsage = process.memoryUsage();
                const currentCpuUsage = process.cpuUsage(prevCpuUsage);
                const elapsedMs = Date.now() - prevCpuTime;
                const cpuPercent = ((currentCpuUsage.user + currentCpuUsage.system) / 1000) / (elapsedMs * 10); // % of one core
                prevCpuUsage = process.cpuUsage();
                prevCpuTime = Date.now();
                this.wsClient.sendMetrics('agent:self-monitor', {
                    pid: process.pid,
                    uptime: process.uptime(),
                    memory: {
                        rss: memUsage.rss,
                        heapTotal: memUsage.heapTotal,
                        heapUsed: memUsage.heapUsed,
                        external: memUsage.external,
                        rssMB: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
                        heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
                    },
                    cpu: {
                        user: currentCpuUsage.user,
                        system: currentCpuUsage.system,
                        percent: Math.round(cpuPercent * 100) / 100,
                    },
                    version: VERSION,
                    activeProbes: this.probeTimers.size,
                    wsConnected: this.wsClient.isConnected?.() || false,
                });
            }
            catch (e) {
                // Self-monitoring is non-critical
            }
        }, 60000);
        this.intervals.push(selfMonitorInterval);
        const cacheSanitizeInterval = setInterval(() => {
            this.upgradeService.sanitizeLocalCache('daily').catch((e) => {
                logger_1.logger.debug(`Daily cache sanitization skipped: ${e?.message || String(e)}`);
            });
        }, 24 * 60 * 60 * 1000);
        this.intervals.push(cacheSanitizeInterval);
        // Process collection (10-second default)
        if (this.tenantFeatures.processMonitoring || this.softwarePolicies.length > 0) {
            const processInterval = setInterval(async () => {
                try {
                    const procs = await this.processCollector.collect();
                    await this.enforceSoftwarePolicies(procs);
                    if (this.tenantFeatures.processMonitoring) {
                        this.wsClient.sendMetrics('process:snapshot', {
                            processes: procs.slice(0, 100), // Top 100 processes
                            totalCount: procs.length,
                        });
                    }
                }
                catch (e) {
                    logger_1.logger.error('Process collection error:', e);
                }
            }, config_1.config.intervals.process);
            this.intervals.push(processInterval);
        }
        // Productivity tracking (3-second default)
        const prodInterval = setInterval(async () => {
            await this.productivityCollector.collectActiveWindow();
        }, config_1.config.intervals.productivity);
        this.intervals.push(prodInterval);
        // Flush productivity records every 30 seconds
        const prodFlushInterval = setInterval(() => {
            if (!this.wsClient.isConnected())
                return;
            const records = this.productivityCollector.flushRecords();
            if (records.length > 0) {
                this.wsClient.send({
                    type: 'productivity:batch',
                    payload: records,
                    agentId: this.wsClient.getAgentId() || undefined,
                });
            }
            const events = this.productivityCollector.flushEvents();
            if (events.length > 0) {
                this.wsClient.send({
                    type: 'activity:batch',
                    payload: events,
                    agentId: this.wsClient.getAgentId() || undefined,
                });
            }
            const screenshots = this.productivityCollector.flushScreenshots();
            if (screenshots.length > 0) {
                this.wsClient.send({
                    type: 'productivity:screenshots',
                    payload: screenshots,
                    agentId: this.wsClient.getAgentId() || undefined,
                });
            }
        }, 30000);
        this.intervals.push(prodFlushInterval);
        // Network stats interval
        if (this.tenantFeatures.networkMonitoring) {
            const networkInterval = setInterval(async () => {
                try {
                    const connections = await this.networkCollector.collectConnections();
                    const stats = await this.networkCollector.collectStats();
                    // Evaluate connections against rules
                    const evaluated = connections.map(conn => ({
                        ...conn,
                        ...this.networkCollector.evaluateConnection(conn),
                    }));
                    const blocked = evaluated.filter(c => c.action === 'block' || c.action === 'log');
                    if (blocked.length > 0) {
                        this.wsClient.sendMetrics('traffic:log', { entries: blocked });
                    }
                    // Send network stats periodically
                    this.wsClient.sendMetrics('network:stats', { stats, connectionCount: connections.length });
                }
                catch (e) {
                    logger_1.logger.error('Network collection error:', e);
                }
            }, config_1.config.intervals.network);
            this.intervals.push(networkInterval);
        }
        // System events (every 5 minutes)
        if (this.tenantFeatures.systemEvents) {
            const sysEventsInterval = setInterval(async () => {
                try {
                    const events = await this.systemEventsCollector.collect();
                    if (events.length > 0) {
                        this.wsClient.send({
                            type: 'system:events',
                            payload: events,
                            agentId: this.wsClient.getAgentId() || undefined,
                        });
                    }
                }
                catch (e) {
                    logger_1.logger.error('System events collection error:', e);
                }
            }, 300000); // 5 minutes
            this.intervals.push(sysEventsInterval);
            // Collect system events once on startup
            setTimeout(async () => {
                try {
                    const events = await this.systemEventsCollector.collect();
                    if (events.length > 0) {
                        this.wsClient.send({
                            type: 'system:events',
                            payload: events,
                            agentId: this.wsClient.getAgentId() || undefined,
                        });
                    }
                }
                catch (e) {
                    logger_1.logger.debug('Initial system events collection failed');
                }
            }, 15000);
        }
        // Installed software (every 5 minutes for faster recovery if startup send fails)
        if (this.tenantFeatures.softwareManagement) {
            const softwareInterval = setInterval(async () => {
                await this.sendSoftwareSnapshot('interval');
            }, 5 * 60000); // 5 minutes
            this.intervals.push(softwareInterval);
            // Collect software once on startup
            setTimeout(async () => {
                await this.sendSoftwareSnapshot('startup');
            }, 20000);
        }
        // Login sessions (every 2 minutes)
        const loginInterval = setInterval(async () => {
            try {
                const sessions = await this.loginSessionCollector.collect();
                if (sessions.length > 0) {
                    this.wsClient.send({
                        type: 'session:batch',
                        payload: { sessions },
                        agentId: this.wsClient.getAgentId() || undefined,
                    });
                }
            }
            catch (e) {
                logger_1.logger.error('Login session collection error:', e);
            }
        }, 120000); // 2 minutes
        this.intervals.push(loginInterval);
        // Collect login sessions once on startup
        setTimeout(async () => {
            try {
                const sessions = await this.loginSessionCollector.collect();
                if (sessions.length > 0) {
                    this.wsClient.send({
                        type: 'session:batch',
                        payload: { sessions },
                        agentId: this.wsClient.getAgentId() || undefined,
                    });
                }
            }
            catch (e) {
                logger_1.logger.debug('Initial login session collection failed');
            }
        }, 10000);
        // Browser history collection (every 2 minutes)
        if (this.tenantFeatures.browserMonitoring) {
            const browserInterval = setInterval(async () => {
                try {
                    const entries = await this.browserHistoryCollector.collect();
                    if (entries.length > 0) {
                        const visits = entries.map(e => ({
                            agentId: this.wsClient.getAgentId() || config_1.config.agent.hostname,
                            timestamp: e.visitTime,
                            url: e.url,
                            title: e.title,
                            domain: e.domain,
                            duration: e.visitDuration,
                            browser: e.browser,
                            tabId: 0,
                            isActive: false,
                            isBackgroundActivity: true,
                            backgroundRequests: Math.max(0, Number(e.visitCount || 0)),
                        }));
                        this.wsClient.send({
                            type: 'browser:batch',
                            payload: { visits },
                            agentId: this.wsClient.getAgentId() || undefined,
                        });
                    }
                }
                catch (e) {
                    logger_1.logger.error('Browser history collection error:', e);
                }
            }, 120000); // 2 minutes
            this.intervals.push(browserInterval);
            // Collect browser history once on startup (delayed)
            setTimeout(async () => {
                try {
                    const entries = await this.browserHistoryCollector.collect();
                    if (entries.length > 0) {
                        const visits = entries.map(e => ({
                            agentId: this.wsClient.getAgentId() || config_1.config.agent.hostname,
                            timestamp: e.visitTime,
                            url: e.url,
                            title: e.title,
                            domain: e.domain,
                            duration: e.visitDuration,
                            browser: e.browser,
                            tabId: 0,
                            isActive: false,
                            isBackgroundActivity: true,
                            backgroundRequests: Math.max(0, Number(e.visitCount || 0)),
                        }));
                        this.wsClient.send({
                            type: 'browser:batch',
                            payload: { visits },
                            agentId: this.wsClient.getAgentId() || undefined,
                        });
                    }
                }
                catch (e) {
                    logger_1.logger.debug('Initial browser history collection failed');
                }
            }, 25000);
        }
        // WiFi metrics and IP address reporting interval
        const wifiInterval = setInterval(async () => {
            try {
                const wifiInfo = await this.systemCollector.collectWifi();
                const ipInfo = await this.systemCollector.collectIPAddresses();
                this.onNetworkStateChanged(ipInfo.all || []);
                await this.refreshPublicIpTelemetry();
                if (wifiInfo) {
                    const wifiPayload = {
                        ...wifiInfo,
                        ip_addresses: ipInfo.all,
                        timestamp: new Date().toISOString(),
                    };
                    this.attachPublicIpTelemetry(wifiPayload);
                    if (this.tenantFeatures.networkMonitoring) {
                        this.wsClient.sendMetrics('network:wifi', wifiPayload);
                    }
                }
                // Always send IP update
                if (ipInfo.all.length > 0) {
                    const ipUpdatePayload = {
                        ip_addresses: ipInfo.all,
                        primary_ip: ipInfo.primary,
                    };
                    this.attachPublicIpTelemetry(ipUpdatePayload);
                    this.wsClient.sendMetrics('agent:ip-update', ipUpdatePayload);
                }
            }
            catch (e) {
                logger_1.logger.debug('WiFi/IP collection error:', e);
            }
        }, config_1.config.intervals.wifi);
        this.intervals.push(wifiInterval);
        // Collect wifi/IP once on startup (delayed)
        setTimeout(async () => {
            try {
                await this.refreshPublicIpTelemetry(true);
                const ipInfo = await this.systemCollector.collectIPAddresses();
                this.onNetworkStateChanged(ipInfo.all || []);
                if (ipInfo.all.length > 0) {
                    const ipUpdatePayload = {
                        ip_addresses: ipInfo.all,
                        primary_ip: ipInfo.primary,
                    };
                    this.attachPublicIpTelemetry(ipUpdatePayload);
                    this.wsClient.sendMetrics('agent:ip-update', ipUpdatePayload);
                }
                const wifiInfo = await this.systemCollector.collectWifi();
                if (wifiInfo) {
                    const wifiPayload = {
                        ...wifiInfo,
                        ip_addresses: ipInfo.all,
                        timestamp: new Date().toISOString(),
                    };
                    this.attachPublicIpTelemetry(wifiPayload);
                    if (this.tenantFeatures.networkMonitoring) {
                        this.wsClient.sendMetrics('network:wifi', wifiPayload);
                    }
                }
            }
            catch (e) {
                logger_1.logger.debug('Initial WiFi/IP collection failed');
            }
        }, 15000);
        // Network interface tracking (every 60s)
        const netIfaceInterval = setInterval(async () => {
            if (!this.wsClient.isConnected())
                return;
            try {
                const snapshot = await this.netIfaceCollector.collect();
                if (snapshot.interfaces.length > 0) {
                    this.wsClient.sendMetrics('net:interfaces:update', snapshot);
                }
            }
            catch (e) {
                logger_1.logger.debug('Net interface collection error:', e);
            }
        }, 60000);
        this.intervals.push(netIfaceInterval);
        // Collect once on startup (delayed slightly)
        setTimeout(async () => {
            if (!this.wsClient.isConnected())
                return;
            try {
                const snapshot = await this.netIfaceCollector.collect();
                if (snapshot.interfaces.length > 0) {
                    this.wsClient.sendMetrics('net:interfaces:update', snapshot);
                }
            }
            catch (e) {
                logger_1.logger.debug('Initial net interface collection failed:', e);
            }
        }, 20000);
        logger_1.logger.info('All collectors started');
    }
    async sendSoftwareSnapshot(context) {
        if (!this.tenantFeatures.softwareManagement) {
            logger_1.logger.debug(`Skipping software collection (${context}) because software management is disabled`);
            return;
        }
        if (!this.wsClient.isConnected()) {
            logger_1.logger.debug(`Skipping software collection (${context}) while disconnected`);
            return;
        }
        try {
            const software = await this.softwareCollector.collect();
            if (software.length > 0) {
                this.wsClient.send({
                    type: 'software:list',
                    payload: { software },
                    agentId: this.wsClient.getAgentId() || undefined,
                });
            }
            else {
                logger_1.logger.debug(`Software collection returned no entries (${context})`);
            }
        }
        catch (e) {
            logger_1.logger.error('Software collection error:', e);
        }
    }
    async handleSoftwareUninstall(payload) {
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const requestedBy = String(payload?.requestedBy || 'unknown').trim() || 'unknown';
        const softwareName = String(payload?.name || '').trim();
        const sendResult = (success, message, extras) => {
            this.wsClient.sendMetrics('software:uninstall:result', {
                agentId,
                requestedBy,
                name: softwareName,
                success,
                message,
                ...(extras || {}),
                timestamp: new Date().toISOString(),
            });
        };
        if (!softwareName) {
            sendResult(false, 'Software name is required');
            return;
        }
        if (process.platform !== 'win32') {
            sendResult(false, `Software uninstall is not supported on ${process.platform} agents yet`);
            return;
        }
        const escapedSoftwareName = softwareName.replace(/'/g, "''");
        const script = `
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$target = '${escapedSoftwareName}'
$paths = @(
  'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
  'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
  'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'
)
function Get-UninstallCandidates([string]$command) {
  $cmd = [string]$command
  if ([string]::IsNullOrWhiteSpace($cmd)) { return @() }
  $candidates = New-Object System.Collections.Generic.List[string]
  if ($cmd -match '(?i)msiexec(\\.exe)?') {
    $base = $cmd
    $candidates.Add($base)
    $baseX = [regex]::Replace($base, '(?i)/I\\s*\\{', '/X{')
    $baseX = [regex]::Replace($baseX, '(?i)/I(\\s+)', '/X$1')
    if (-not [string]::Equals($baseX, $base, [System.StringComparison]::OrdinalIgnoreCase)) {
      $candidates.Add($baseX)
    }
    $normalized = New-Object System.Collections.Generic.List[string]
    foreach ($candidate in $candidates) {
      $invoke = $candidate
      if ($invoke -notmatch '(?i)\\s/q[nrb]') { $invoke += ' /qn /norestart' }
      $normalized.Add($invoke)
    }
    return @($normalized | Select-Object -Unique)
  }
  if ($cmd -match '(?i)\\s/(quiet|silent|s|verysilent)\\b') {
    $candidates.Add($cmd)
  } else {
    $candidates.Add("$cmd /S")
    $candidates.Add("$cmd /s /v\`\"/qn /norestart\`\"")
    $candidates.Add("$cmd /quiet")
    $candidates.Add("$cmd /silent")
    $candidates.Add("$cmd /uninstall /quiet")
    $candidates.Add("$cmd /VERYSILENT /SUPPRESSMSGBOXES /NORESTART")
    $candidates.Add($cmd)
  }
  return @($candidates | Select-Object -Unique)
}
function Stop-ProcessesForInstallLocation([string]$path) {
  $root = [Environment]::ExpandEnvironmentVariables([string]$path).Trim()
  if ([string]::IsNullOrWhiteSpace($root) -or -not (Test-Path $root)) { return }
  $rootNorm = $root.ToLowerInvariant()
  try {
    $procs = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
      $_.ExecutablePath -and $_.ExecutablePath.ToLowerInvariant().StartsWith($rootNorm)
    }
    foreach ($p in $procs) {
      try { Stop-Process -Id [int]$p.ProcessId -Force -ErrorAction SilentlyContinue } catch {}
    }
  } catch {}
}
function Invoke-UninstallCommand([string]$command, [int]$timeoutSeconds = 90) {
  $trimmed = [string]$command
  if ([string]::IsNullOrWhiteSpace($trimmed)) { throw 'Empty uninstall command' }
  $exe = ''
  $args = ''
  if ($trimmed.StartsWith('"')) {
    $m = [regex]::Match($trimmed, '^"([^"]+)"\\s*(.*)$')
    if ($m.Success) {
      $exe = $m.Groups[1].Value
      $args = $m.Groups[2].Value
    }
  }
  if (-not $exe) {
    $parts = $trimmed.Split(' ', 2)
    $exe = $parts[0]
    if ($parts.Length -gt 1) { $args = $parts[1] }
  }
  if (-not [string]::IsNullOrWhiteSpace($exe)) {
    $exe = [Environment]::ExpandEnvironmentVariables($exe)
  }
  $workingDir = ''
  try { $workingDir = [string](Split-Path -Path $exe -Parent) } catch {}
  if ([string]::IsNullOrWhiteSpace($workingDir) -or -not (Test-Path $workingDir)) {
    $workingDir = [Environment]::GetFolderPath('Windows')
  }
  $proc = $null
  try {
    $proc = Start-Process -FilePath $exe -ArgumentList $args -PassThru -WindowStyle Hidden -WorkingDirectory $workingDir
  } catch {
    $proc = Start-Process -FilePath 'cmd.exe' -ArgumentList '/c', $trimmed -PassThru -WindowStyle Hidden -WorkingDirectory $workingDir
  }
  if (-not $proc) { throw 'Failed to start uninstall command' }
  $timeoutMs = [Math]::Max(1000, [int]$timeoutSeconds * 1000)
  if (-not $proc.WaitForExit($timeoutMs)) {
    try { $proc.Kill() } catch {}
    throw ('timeout after {0}s' -f [int]$timeoutSeconds)
  }
  return $proc
}
try {
  $entries = @()
  foreach ($regPath in $paths) {
    try {
      $entries += Get-ItemProperty $regPath -ErrorAction SilentlyContinue | Where-Object {
        $_.DisplayName -and (
          $_.DisplayName.Trim().ToLowerInvariant() -eq $target.Trim().ToLowerInvariant() -or
          $_.DisplayName.Trim().ToLowerInvariant().Contains($target.Trim().ToLowerInvariant())
        )
      }
    } catch {}
  }
  $entries = @($entries | Sort-Object PSPath -Unique)
  if ($entries.Count -eq 0) {
    @{
      success = $true
      removed = 0
      total = 0
      message = "No installed software matched '$target'"
    } | ConvertTo-Json -Compress
    exit 0
  }
  $messages = @()
  $successCount = 0
  foreach ($entry in $entries) {
    $displayName = [string]$entry.DisplayName
    $uninstallCmd = [string]$entry.QuietUninstallString
    if (-not $uninstallCmd) { $uninstallCmd = [string]$entry.UninstallString }
    if (-not $uninstallCmd) {
      $messages += ('{0}: uninstall command not found' -f $displayName)
      continue
    }
    $entrySuccess = $false
    $lastFailure = ''
    $invokeCandidates = New-Object System.Collections.Generic.List[string]
    foreach ($candidate in Get-UninstallCandidates $uninstallCmd) {
      if (-not [string]::IsNullOrWhiteSpace([string]$candidate)) { $invokeCandidates.Add([string]$candidate) }
    }
    $expandedInstallPath = ''
    $installPathMissing = $true
    $installLocation = [string]$entry.InstallLocation
    if (-not [string]::IsNullOrWhiteSpace($installLocation)) {
      $expandedInstallPath = [Environment]::ExpandEnvironmentVariables($installLocation).Trim()
      if (-not [string]::IsNullOrWhiteSpace($expandedInstallPath)) {
        $installLocation = $expandedInstallPath
        $installPathMissing = -not (Test-Path $installLocation)
      }
      Stop-ProcessesForInstallLocation $installLocation
      foreach ($name in @('uninstall.exe','Uninstall.exe','unins000.exe','Unins000.exe')) {
        try {
          $candidatePath = Join-Path $installLocation $name
          if (Test-Path $candidatePath) {
            $invokeCandidates.Add(('"{0}" /S' -f $candidatePath))
            $invokeCandidates.Add(('"{0}" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART' -f $candidatePath))
          }
        } catch {}
      }
      try {
        $dynamicUninstallers = Get-ChildItem -Path $installLocation -File -ErrorAction SilentlyContinue |
          Where-Object { $_.Name -match '(?i)(uninstall|unins).*\\.exe$' } |
          Select-Object -First 5
        foreach ($item in $dynamicUninstallers) {
          if ($item -and $item.FullName) {
            $invokeCandidates.Add(('"{0}" /S' -f [string]$item.FullName))
            $invokeCandidates.Add(('"{0}" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART' -f [string]$item.FullName))
          }
        }
      } catch {}
    }
    if ([string]::IsNullOrWhiteSpace($expandedInstallPath)) {
      try {
        $cmdPath = ''
        if ($uninstallCmd.StartsWith('"')) {
          $pathMatch = [regex]::Match($uninstallCmd, '^"([^"]+)"')
          if ($pathMatch.Success) { $cmdPath = [string]$pathMatch.Groups[1].Value }
        }
        if ([string]::IsNullOrWhiteSpace($cmdPath)) {
          $parts = $uninstallCmd.Split(' ', 2)
          if ($parts.Length -gt 0) { $cmdPath = [string]$parts[0] }
        }
        if (-not [string]::IsNullOrWhiteSpace($cmdPath)) {
          $cmdPath = [Environment]::ExpandEnvironmentVariables($cmdPath.Trim())
          if (Test-Path $cmdPath) {
            $derivedInstallPath = [string](Split-Path -Path $cmdPath -Parent)
            if (-not [string]::IsNullOrWhiteSpace($derivedInstallPath)) {
              $expandedInstallPath = $derivedInstallPath
              $installPathMissing = -not (Test-Path $expandedInstallPath)
            }
          }
        }
      } catch {}
    }
    if ($displayName -match '(?i)blender') {
      foreach ($base in @($env:ProgramFiles, [Environment]::GetEnvironmentVariable('ProgramFiles(x86)'))) {
        if ([string]::IsNullOrWhiteSpace([string]$base)) { continue }
        try {
          $match = Get-ChildItem -Path (Join-Path $base 'Blender Foundation') -Filter 'uninstall.exe' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
          if ($match -and $match.FullName) {
            $invokeCandidates.Add(('"{0}" /S' -f [string]$match.FullName))
            break
          }
        } catch {}
      }
    }
    $resolvedCandidates = @($invokeCandidates | Select-Object -Unique)
    if ($resolvedCandidates.Count -lt 1) {
      $messages += ('{0}: no uninstall candidates found' -f $displayName)
      continue
    }
    $messages += ('{0}: trying {1} candidate(s)' -f $displayName, $resolvedCandidates.Count)
    foreach ($invokeCmd in $resolvedCandidates) {
      $cmdLabel = [string]$invokeCmd
      if ($cmdLabel.Length -gt 140) { $cmdLabel = $cmdLabel.Substring(0, 140) + '...' }
      try {
        $proc = Invoke-UninstallCommand $invokeCmd
        $exitCode = [int]($proc.ExitCode)
        if ($exitCode -eq 0 -or $exitCode -eq 1641 -or $exitCode -eq 3010) {
          $successCount++
          $entrySuccess = $true
          $messages += ('{0}: success ({1}) [{2}]' -f $displayName, $exitCode, $cmdLabel)
          break
        }
        $lastFailure = "exit code $exitCode [$cmdLabel]"
      } catch {
        $lastFailure = ([string]$_.Exception.Message + " [$cmdLabel]")
      }
    }
    $regPath = [string]$entry.PSPath
    if (-not $entrySuccess -and -not [string]::IsNullOrWhiteSpace($regPath)) {
      try {
        if (-not (Test-Path -LiteralPath $regPath)) {
          $successCount++
          $entrySuccess = $true
          $messages += ('{0}: registry entry already absent after uninstall attempt' -f $displayName)
        }
      } catch {}
    }
    $allowForcedCleanup = ($lastFailure -match 'exit code 1605') -or (($lastFailure -match 'exit code 1') -and ($installPathMissing -or ($displayName -match '(?i)configtool') -or ($displayName -match '(?i)gameloop'))) -or (($lastFailure -match 'timeout after') -and -not [string]::IsNullOrWhiteSpace($expandedInstallPath))
    if (-not $entrySuccess -and $allowForcedCleanup) {
      if (-not [string]::IsNullOrWhiteSpace($regPath) -and ($installPathMissing -or [string]::IsNullOrWhiteSpace($expandedInstallPath))) {
        try {
          Remove-Item -LiteralPath $regPath -Recurse -Force -ErrorAction Stop
          $successCount++
          $entrySuccess = $true
          $messages += ('{0}: removed uninstall registry entry via fallback cleanup' -f $displayName)
        } catch {
          $messages += ('{0}: stale registry cleanup failed: {1}' -f $displayName, [string]$_.Exception.Message)
        }
      }
      if (-not $entrySuccess) {
        $installPath = [string]$entry.InstallLocation
        if (-not [string]::IsNullOrWhiteSpace($installPath)) {
          $expandedInstallPath = [Environment]::ExpandEnvironmentVariables($installPath).Trim()
          $programFiles = [Environment]::GetFolderPath('ProgramFiles')
          $programFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')
          $isSafeRoot = $false
          if (-not [string]::IsNullOrWhiteSpace($programFiles) -and $expandedInstallPath.StartsWith($programFiles, [System.StringComparison]::OrdinalIgnoreCase)) { $isSafeRoot = $true }
          if (-not $isSafeRoot -and -not [string]::IsNullOrWhiteSpace($programFilesX86) -and $expandedInstallPath.StartsWith($programFilesX86, [System.StringComparison]::OrdinalIgnoreCase)) { $isSafeRoot = $true }
          if ($isSafeRoot -and (Test-Path $expandedInstallPath)) {
            try {
              Stop-ProcessesForInstallLocation $expandedInstallPath
              Remove-Item -LiteralPath $expandedInstallPath -Recurse -Force -ErrorAction Stop
              if (-not [string]::IsNullOrWhiteSpace($regPath)) {
                try { Remove-Item -LiteralPath $regPath -Recurse -Force -ErrorAction SilentlyContinue } catch {}
              }
              $successCount++
              $entrySuccess = $true
              $messages += ('{0}: removed install directory via fallback cleanup' -f $displayName)
            } catch {
              $messages += ('{0}: install directory cleanup failed: {1}' -f $displayName, [string]$_.Exception.Message)
            }
          }
        }
      }
    }
    if (-not $entrySuccess) {
      if (-not $lastFailure) { $lastFailure = 'uninstall command failed' }
      $messages += ('{0}: {1}' -f $displayName, $lastFailure)
      if (($lastFailure -match 'timeout after') -and [string]::IsNullOrWhiteSpace($expandedInstallPath)) {
        $messages += ('{0}: timeout cleanup skipped (install path unavailable)' -f $displayName)
      }
    }
  }
  if ($successCount -lt 1) {
    try {
      $winget = Get-Command winget.exe -ErrorAction SilentlyContinue
      if ($winget) {
        $wingetAttempts = @(
          @('uninstall', '--name', $target, '--silent', '--accept-source-agreements', '--accept-package-agreements'),
          @('uninstall', '--id', $target, '--exact', '--silent', '--accept-source-agreements', '--accept-package-agreements')
        )
        foreach ($wgArgs in $wingetAttempts) {
          $wgProc = Start-Process -FilePath $winget.Source -ArgumentList $wgArgs -Wait -PassThru -WindowStyle Hidden
          if ([int]$wgProc.ExitCode -eq 0) {
            $successCount++
            $messages += ('winget uninstall success for {0}' -f ($wgArgs -join ' '))
            break
          }
          $messages += ('winget uninstall exit code {0} ({1})' -f [int]$wgProc.ExitCode, ($wgArgs -join ' '))
        }
      } else {
        $messages += 'winget not available'
      }
    } catch {
      $messages += ('winget uninstall failed: {0}' -f [string]$_.Exception.Message)
    }
  }
  if ($successCount -lt 1) {
    try {
      $choco = Get-Command choco.exe -ErrorAction SilentlyContinue
      if ($choco) {
        $chocoArgs = @('uninstall', $target, '-y', '--no-progress')
        $chocoProc = Start-Process -FilePath $choco.Source -ArgumentList $chocoArgs -Wait -PassThru -WindowStyle Hidden
        if ([int]$chocoProc.ExitCode -eq 0) {
          $successCount++
          $messages += ('choco uninstall success for {0}' -f $target)
        } else {
          $messages += ('choco uninstall exit code {0}' -f [int]$chocoProc.ExitCode)
        }
      } else {
        $messages += 'choco not available'
      }
    } catch {
      $messages += ('choco uninstall failed: {0}' -f [string]$_.Exception.Message)
    }
  }
  if ($successCount -lt 1) {
    throw ($messages -join '; ')
  }
  @{
    success = $true
    removed = $successCount
    total = $entries.Count
    message = ($messages -join '; ')
  } | ConvertTo-Json -Compress
} catch {
  @{
    success = $false
    removed = 0
    total = 0
    message = [string]$_.Exception.Message
  } | ConvertTo-Json -Compress
}
exit 0
`;
        try {
            const output = await this.runPowerShellScript(script, 10 * 60 * 1000);
            const parsed = this.parseJsonOutput(output);
            if (!parsed || typeof parsed.success !== 'boolean') {
                const snippet = String(output || '').replace(/\s+/g, ' ').trim().slice(0, 320);
                sendResult(false, snippet ? `Uninstall response parse failure: ${snippet}` : 'Uninstall response parse failure');
                await this.sendSoftwareSnapshot('post-uninstall-parse-failure');
                return;
            }
            if (parsed.success === false) {
                sendResult(false, String(parsed.message || `Failed to uninstall ${softwareName}`), {
                    removed: 0,
                    total: Number(parsed.total || 0),
                });
                await this.sendSoftwareSnapshot('post-uninstall-failed');
                return;
            }
            const removed = Number(parsed.removed || 0);
            const total = Number(parsed.total || 0);
            sendResult(true, String(parsed.message || `Uninstall command executed for ${softwareName}`), {
                removed: Number.isFinite(removed) ? removed : 0,
                total: Number.isFinite(total) ? total : 0,
            });
            await this.sendSoftwareSnapshot('post-uninstall');
        }
        catch (error) {
            sendResult(false, String(error?.message || `Failed to uninstall ${softwareName}`));
            await this.sendSoftwareSnapshot('post-uninstall-exception');
        }
    }
    normalizeSoftwareManager(value) {
        const normalized = String(value || '').trim().toLowerCase();
        if (normalized === 'winget')
            return 'winget';
        if (normalized === 'choco' || normalized === 'chocolatey')
            return 'choco';
        return 'auto';
    }
    normalizeInstallerType(value) {
        const normalized = String(value || '').trim().toLowerCase();
        if (normalized === 'exe' || normalized === 'msi' || normalized === 'zip' || normalized === 'powershell' || normalized === 'script') {
            return normalized;
        }
        return 'auto';
    }
    escapePowerShellSingleQuoted(value) {
        return String(value ?? '').replace(/'/g, "''");
    }
    async getWingetStatus() {
        if (process.platform !== 'win32') {
            return { available: false, message: 'WinGet is only supported on Windows' };
        }
        const script = `
$ErrorActionPreference = 'SilentlyContinue'
function Resolve-WinGetPath {
  $cmd = Get-Command winget.exe -ErrorAction SilentlyContinue
  if ($cmd -and -not [string]::IsNullOrWhiteSpace($cmd.Source) -and (Test-Path $cmd.Source)) {
    return [string]$cmd.Source
  }
  $candidates = @(
    (Join-Path $env:LOCALAPPDATA 'Microsoft\\WindowsApps\\winget.exe'),
    (Join-Path $env:USERPROFILE 'AppData\\Local\\Microsoft\\WindowsApps\\winget.exe')
  ) | Where-Object { $_ -and (Test-Path $_) }
  if ($candidates.Count -gt 0) {
    return [string]$candidates[0]
  }
  $desktopAppInstaller = Get-AppxPackage Microsoft.DesktopAppInstaller -ErrorAction SilentlyContinue |
    Sort-Object Version -Descending |
    Select-Object -First 1
  if ($desktopAppInstaller) {
    $cli = Join-Path $desktopAppInstaller.InstallLocation 'AppInstallerCLI.exe'
    if (Test-Path $cli) {
      return [string]$cli
    }
  }
  return ''
}

$desktopAppInstaller = Get-AppxPackage Microsoft.DesktopAppInstaller -ErrorAction SilentlyContinue |
  Sort-Object Version -Descending |
  Select-Object -First 1
$path = Resolve-WinGetPath
$version = ''
if (-not [string]::IsNullOrWhiteSpace($path)) {
  try {
    $version = [string]((& $path --version 2>$null | Select-Object -First 1))
  } catch {}
}
[pscustomobject]@{
  available = -not [string]::IsNullOrWhiteSpace($path)
  path = [string]$path
  version = [string]($version.Trim())
  appInstallerVersion = [string]($desktopAppInstaller.Version)
} | ConvertTo-Json -Compress
`;
        try {
            const output = await this.runPowerShellScript(script, 30000);
            const parsed = this.parseJsonOutput(output);
            if (parsed && typeof parsed.available === 'boolean') {
                return {
                    available: parsed.available === true,
                    path: String(parsed.path || '').trim() || undefined,
                    version: String(parsed.version || '').trim() || undefined,
                    appInstallerVersion: String(parsed.appInstallerVersion || '').trim() || undefined,
                };
            }
            const snippet = String(output || '').replace(/\s+/g, ' ').trim().slice(0, 240);
            return { available: false, message: snippet || 'WinGet status lookup returned no data' };
        }
        catch (error) {
            return { available: false, message: String(error?.message || error || 'WinGet status lookup failed') };
        }
    }
    async ensureWingetInstalled(forceInstall = false) {
        const current = await this.getWingetStatus();
        if (current.available) {
            return {
                available: true,
                path: current.path,
                version: current.version,
                method: 'existing',
                message: current.version
                    ? `WinGet package manager ready (${current.version})`
                    : 'WinGet package manager ready',
            };
        }
        if (process.platform !== 'win32') {
            return { available: false, message: current.message || 'WinGet is only supported on Windows' };
        }
        const now = Date.now();
        const retryIntervalMs = 10 * 60 * 1000;
        if (!forceInstall && this.wingetInstallAttempted && (now - this.wingetLastInstallAttemptMs) < retryIntervalMs) {
            return {
                available: false,
                message: current.message || 'WinGet bootstrap recently attempted; waiting before retry',
            };
        }
        this.wingetInstallAttempted = true;
        this.wingetLastInstallAttemptMs = now;
        logger_1.logger.info('WinGet not found, attempting automatic installation');
        const wingetBundleUrl = 'https://github.com/microsoft/winget-cli/releases/download/v1.28.190/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle';
        const wingetDependenciesUrl = 'https://github.com/microsoft/winget-cli/releases/download/v1.28.190/DesktopAppInstaller_Dependencies.zip';
        const script = `
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Get-WinGetStatus {
  $cmd = Get-Command winget.exe -ErrorAction SilentlyContinue
  $path = ''
  if ($cmd -and -not [string]::IsNullOrWhiteSpace($cmd.Source) -and (Test-Path $cmd.Source)) {
    $path = [string]$cmd.Source
  }
  if ([string]::IsNullOrWhiteSpace($path)) {
    $candidates = @(
      (Join-Path $env:LOCALAPPDATA 'Microsoft\\WindowsApps\\winget.exe'),
      (Join-Path $env:USERPROFILE 'AppData\\Local\\Microsoft\\WindowsApps\\winget.exe')
    ) | Where-Object { $_ -and (Test-Path $_) }
    if ($candidates.Count -gt 0) {
      $path = [string]$candidates[0]
    }
  }
  $desktopAppInstaller = Get-AppxPackage Microsoft.DesktopAppInstaller -ErrorAction SilentlyContinue |
    Sort-Object Version -Descending |
    Select-Object -First 1
  if ([string]::IsNullOrWhiteSpace($path) -and $desktopAppInstaller) {
    $cli = Join-Path $desktopAppInstaller.InstallLocation 'AppInstallerCLI.exe'
    if (Test-Path $cli) {
      $path = [string]$cli
    }
  }
  if ([string]::IsNullOrWhiteSpace($path)) {
    foreach ($root in @((Join-Path \${env:ProgramFiles} 'WindowsApps'), (Join-Path \${env:ProgramW6432} 'WindowsApps'))) {
      if ([string]::IsNullOrWhiteSpace($root) -or -not (Test-Path $root)) { continue }
      try {
        $dirs = Get-ChildItem -Path $root -Directory -ErrorAction SilentlyContinue |
          Where-Object { $_.Name -like 'Microsoft.DesktopAppInstaller_*__8wekyb3d8bbwe' } |
          Sort-Object LastWriteTime -Descending
        foreach ($dir in $dirs) {
          foreach ($candidateName in @('winget.exe', 'AppInstallerCLI.exe')) {
            $candidate = Join-Path $dir.FullName $candidateName
            if (Test-Path $candidate) {
              $path = [string]$candidate
              break
            }
          }
          if (-not [string]::IsNullOrWhiteSpace($path)) { break }
        }
      } catch {}
      if (-not [string]::IsNullOrWhiteSpace($path)) { break }
    }
  }
  $version = ''
  if (-not [string]::IsNullOrWhiteSpace($path)) {
    try {
      $version = [string]((& $path --version 2>$null | Select-Object -First 1))
    } catch {}
  }
  return [pscustomobject]@{
    available = -not [string]::IsNullOrWhiteSpace($path)
    path = [string]$path
    version = [string]($version.Trim())
    appInstallerVersion = [string]($desktopAppInstaller.Version)
  }
}

function Register-WinGetIfProvisioned {
  try {
    Add-AppxPackage -RegisterByFamilyName -MainPackage 'Microsoft.DesktopAppInstaller_8wekyb3d8bbwe' -ErrorAction Stop | Out-Null
  } catch {}
}

function Install-WinGetViaBundle {
  $tempRoot = Join-Path $env:TEMP ('appstats-winget-' + [guid]::NewGuid().ToString('N'))
  New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
  try {
    $bundlePath = Join-Path $tempRoot 'Microsoft.DesktopAppInstaller.msixbundle'
    $depsZip = Join-Path $tempRoot 'DesktopAppInstaller_Dependencies.zip'
    $depsDir = Join-Path $tempRoot 'deps'
    Invoke-WebRequest -Uri '${wingetBundleUrl}' -OutFile $bundlePath -UseBasicParsing -MaximumRedirection 8 -TimeoutSec 300
    if (-not (Test-Path $bundlePath)) { throw 'Downloaded WinGet bundle is missing' }
    Invoke-WebRequest -Uri '${wingetDependenciesUrl}' -OutFile $depsZip -UseBasicParsing -MaximumRedirection 8 -TimeoutSec 300
    if (Test-Path $depsZip) { Expand-Archive -Path $depsZip -DestinationPath $depsDir -Force }
    $depFiles = @()
    if (Test-Path $depsDir) {
      $depFiles = @(Get-ChildItem -Path $depsDir -File -Recurse -Include *.appx,*.msix,*.msixbundle -ErrorAction SilentlyContinue | Sort-Object FullName | Select-Object -ExpandProperty FullName)
    }
    try {
      if ($depFiles.Count -gt 0) {
        Add-AppxPackage -Path $bundlePath -DependencyPath $depFiles -ForceApplicationShutdown -ForceUpdateFromAnyVersion -ErrorAction Stop | Out-Null
      } else {
        Add-AppxPackage -Path $bundlePath -ForceApplicationShutdown -ForceUpdateFromAnyVersion -ErrorAction Stop | Out-Null
      }
    } catch {
      if ($depFiles.Count -gt 0) {
        Add-AppxProvisionedPackage -Online -PackagePath $bundlePath -DependencyPackagePath $depFiles -SkipLicense -ErrorAction Stop | Out-Null
      } else {
        Add-AppxProvisionedPackage -Online -PackagePath $bundlePath -SkipLicense -ErrorAction Stop | Out-Null
      }
    }
    Register-WinGetIfProvisioned
  } finally {
    try {
      if (Test-Path -LiteralPath $tempRoot) {
        Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
      }
    } catch {}
  }
}

function Install-WinGetViaModule {
  try {
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
  } catch {}
  if (-not (Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop | Out-Null
  }
  try { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue } catch {}
  Install-Module -Name Microsoft.WinGet.Client -Force -AllowClobber -Scope AllUsers -ErrorAction Stop | Out-Null
  Import-Module Microsoft.WinGet.Client -Force -ErrorAction Stop
  $repair = Get-Command Repair-WinGetPackageManager -ErrorAction Stop
  $params = @{}
  if ($repair.Parameters.ContainsKey('AllUsers')) { $params['AllUsers'] = $true }
  if ($repair.Parameters.ContainsKey('Latest')) { $params['Latest'] = $true }
  if ($repair.Parameters.ContainsKey('Force')) { $params['Force'] = $true }
  & $repair @params | Out-Null
}

$errors = New-Object System.Collections.Generic.List[string]
$method = ''
$status = Get-WinGetStatus
if (-not $status.available) {
  try {
    Register-WinGetIfProvisioned
  } catch {
    $errors.Add('RegisterByFamilyName failed: ' + [string]$_.Exception.Message)
  }
}
$status = Get-WinGetStatus
if (-not $status.available) {
  try {
    Install-WinGetViaBundle
    $method = 'appinstaller-bundle'
  } catch {
    $errors.Add('App Installer bundle failed: ' + [string]$_.Exception.Message)
  }
}
$status = Get-WinGetStatus
if (-not $status.available) {
  try {
    Install-WinGetViaModule
    $method = if ([string]::IsNullOrWhiteSpace($method)) { 'winget-client-module' } else { $method + ',winget-client-module' }
  } catch {
    $errors.Add('Repair-WinGetPackageManager failed: ' + [string]$_.Exception.Message)
  }
}
$status = Get-WinGetStatus
[pscustomobject]@{
  available = [bool]$status.available
  path = [string]$status.path
  version = [string]$status.version
  appInstallerVersion = [string]$status.appInstallerVersion
  method = [string]$method
  message = if ($status.available) {
    if ([string]::IsNullOrWhiteSpace($status.version)) { 'WinGet package manager ready' } else { 'WinGet package manager ready (' + [string]$status.version + ')' }
  } elseif ($errors.Count -gt 0) {
    [string]::Join(' | ', $errors.ToArray())
  } else {
    'WinGet package manager not detected after bootstrap attempt'
  }
} | ConvertTo-Json -Compress
`;
        try {
            const output = await this.runPowerShellScript(script, 15 * 60 * 1000);
            const parsed = this.parseJsonOutput(output);
            if (parsed && typeof parsed.available === 'boolean') {
                return {
                    available: parsed.available === true,
                    path: String(parsed.path || '').trim() || undefined,
                    version: String(parsed.version || '').trim() || undefined,
                    method: String(parsed.method || '').trim() || undefined,
                    message: String(parsed.message || (parsed.available ? 'WinGet package manager ready' : 'WinGet bootstrap failed')).trim(),
                };
            }
            const snippet = String(output || '').replace(/\s+/g, ' ').trim().slice(0, 320);
            return {
                available: false,
                message: snippet ? `WinGet bootstrap parse failure: ${snippet}` : 'WinGet bootstrap parse failure',
            };
        }
        catch (error) {
            return { available: false, message: String(error?.message || error || 'WinGet bootstrap failed') };
        }
    }
    async runInstallerPayloadInstall(softwareName, installer) {
        const sourceUrl = String(installer?.url || '').trim();
        if (!sourceUrl) {
            return { success: false, message: 'Installer URL is required' };
        }
        const installerType = this.normalizeInstallerType(installer?.fileType || installer?.installerType);
        const fileName = String(installer?.fileName || '').trim();
        const installArgs = String(installer?.installArgs || '').trim();
        const installCommand = String(installer?.installCommand || '').trim();
        const token = String(config_1.config.server?.token || '').trim();
        const script = `
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$url = '${this.escapePowerShellSingleQuoted(sourceUrl)}'
$name = '${this.escapePowerShellSingleQuoted(softwareName || 'Software Package')}'
$fileNameHint = '${this.escapePowerShellSingleQuoted(fileName)}'
$preferredType = '${this.escapePowerShellSingleQuoted(installerType)}'
$installArgs = '${this.escapePowerShellSingleQuoted(installArgs)}'
$installCommand = '${this.escapePowerShellSingleQuoted(installCommand)}'
$agentToken = '${this.escapePowerShellSingleQuoted(token)}'
$tempRoot = Join-Path $env:TEMP ("appstats-install-" + [guid]::NewGuid().ToString('N'))
$downloadPath = ''
$extractDir = ''
$effectiveType = 'auto'
$executedCommand = ''

function Resolve-EffectiveType([string]$preferred, [string]$downloadFile) {
  $p = [string]$preferred
  if (-not [string]::IsNullOrWhiteSpace($p) -and $p -ne 'auto') { return $p.ToLowerInvariant() }
  $ext = [IO.Path]::GetExtension([string]$downloadFile).ToLowerInvariant()
  switch ($ext) {
    '.msi' { return 'msi' }
    '.zip' { return 'zip' }
    '.ps1' { return 'powershell' }
    '.exe' { return 'exe' }
    default { return 'exe' }
  }
}

function Resolve-FileName([string]$hint, [string]$downloadUrl, [string]$fallbackName) {
  $name = ''
  if (-not [string]::IsNullOrWhiteSpace($hint)) {
    $name = [IO.Path]::GetFileName($hint)
  }
  if ([string]::IsNullOrWhiteSpace($name)) {
    try {
      $uri = [Uri]$downloadUrl
      $name = [IO.Path]::GetFileName($uri.LocalPath)
    } catch {}
  }
  if ([string]::IsNullOrWhiteSpace($name)) {
    $safe = ($fallbackName -replace '[^a-zA-Z0-9._-]', '_')
    if ([string]::IsNullOrWhiteSpace($safe)) { $safe = 'installer' }
    $name = "$safe.bin"
  }
  return [IO.Path]::GetFileName($name)
}

function Resolve-Command([string]$template, [string]$downloadFile, [string]$zipDir, [string]$packageName) {
  if ([string]::IsNullOrWhiteSpace($template)) { return '' }
  $cmd = [string]$template
  $cmd = $cmd.Replace('{download_file}', $downloadFile)
  $cmd = $cmd.Replace('{zip_dir}', $zipDir)
  $cmd = $cmd.Replace('{package_name}', $packageName)
  return $cmd
}

function Invoke-Cmd([string]$commandLine, [int]$timeoutSeconds = 1800) {
  if ([string]::IsNullOrWhiteSpace($commandLine)) { throw 'Empty command line' }
  $proc = Start-Process -FilePath 'cmd.exe' -ArgumentList '/d', '/s', '/c', $commandLine -PassThru -WindowStyle Hidden
  if (-not $proc) { throw 'Failed to start command' }
  $waitMs = [Math]::Max(1, [int]$timeoutSeconds) * 1000
  if (-not $proc.WaitForExit($waitMs)) {
    try { $proc.Kill() } catch {}
    throw ("Command timeout after " + [int]$timeoutSeconds + "s")
  }
  if ([int]$proc.ExitCode -ne 0) {
    throw ("Command exit code " + [int]$proc.ExitCode)
  }
}

try {
  New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
  $resolvedName = Resolve-FileName $fileNameHint $url $name
  $downloadPath = Join-Path $tempRoot $resolvedName
  $headers = @{}
  if (-not [string]::IsNullOrWhiteSpace($agentToken)) { $headers['x-agent-token'] = $agentToken }
  if ($headers.Count -gt 0) {
    Invoke-WebRequest -Uri $url -Headers $headers -OutFile $downloadPath -UseBasicParsing -MaximumRedirection 8 -TimeoutSec 300
  } else {
    Invoke-WebRequest -Uri $url -OutFile $downloadPath -UseBasicParsing -MaximumRedirection 8 -TimeoutSec 300
  }
  if (-not (Test-Path -LiteralPath $downloadPath)) { throw 'Installer download failed: file missing' }
  $fi = Get-Item -LiteralPath $downloadPath -ErrorAction Stop
  if ([int64]$fi.Length -le 0) { throw 'Installer download failed: file empty' }

  $effectiveType = Resolve-EffectiveType $preferredType $downloadPath
  if ($effectiveType -eq 'zip') {
    $extractDir = Join-Path $tempRoot 'extracted'
    Expand-Archive -Path $downloadPath -DestinationPath $extractDir -Force
    $custom = Resolve-Command $installCommand $downloadPath $extractDir $name
    if (-not [string]::IsNullOrWhiteSpace($custom)) {
      $executedCommand = $custom
      Invoke-Cmd $executedCommand 3600
    } else {
      $msi = Get-ChildItem -Path $extractDir -File -Recurse -Include *.msi -ErrorAction SilentlyContinue | Select-Object -First 1
      if ($msi) {
        $msiArgs = [string]$installArgs
        if ([string]::IsNullOrWhiteSpace($msiArgs)) { $msiArgs = '/qn /norestart' }
        $executedCommand = ('msiexec.exe /i "' + $msi.FullName + '" ' + $msiArgs)
        Invoke-Cmd $executedCommand 3600
      } else {
        $exe = Get-ChildItem -Path $extractDir -File -Recurse -Include *.exe -ErrorAction SilentlyContinue |
          Where-Object { $_.Name -notmatch '(?i)(uninstall|remove|unins)' } |
          Sort-Object @{ Expression = { if ($_.Name -match '(?i)(setup|install)') { 0 } else { 1 } } }, FullName |
          Select-Object -First 1
        if (-not $exe) { throw 'No installable executable found inside ZIP package' }
        $exeArgs = [string]$installArgs
        if ([string]::IsNullOrWhiteSpace($exeArgs)) { $exeArgs = '/S /silent /verysilent /norestart' }
        $proc = Start-Process -FilePath $exe.FullName -ArgumentList $exeArgs -Wait -PassThru -WindowStyle Hidden
        if ([int]$proc.ExitCode -ne 0) { throw ("Installer exit code " + [int]$proc.ExitCode) }
        $executedCommand = ('"' + $exe.FullName + '" ' + $exeArgs)
      }
    }
  } elseif ($effectiveType -eq 'msi') {
    $msiArgs = [string]$installArgs
    if ([string]::IsNullOrWhiteSpace($msiArgs)) { $msiArgs = '/qn /norestart' }
    $executedCommand = ('msiexec.exe /i "' + $downloadPath + '" ' + $msiArgs)
    Invoke-Cmd $executedCommand 3600
  } elseif ($effectiveType -eq 'powershell' -or $effectiveType -eq 'script') {
    $custom = Resolve-Command $installCommand $downloadPath $extractDir $name
    if (-not [string]::IsNullOrWhiteSpace($custom)) {
      $executedCommand = $custom
      Invoke-Cmd $executedCommand 3600
    } else {
      $ext = [IO.Path]::GetExtension($downloadPath).ToLowerInvariant()
      if ($ext -ne '.ps1') { throw 'Script installer requires installCommand or a .ps1 download' }
      $scriptArgs = [string]$installArgs
      $executedCommand = ('powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "' + $downloadPath + '" ' + $scriptArgs)
      Invoke-Cmd $executedCommand 3600
    }
  } else {
    $exeArgs = [string]$installArgs
    if ([string]::IsNullOrWhiteSpace($exeArgs)) { $exeArgs = '/S /silent /verysilent /norestart' }
    $proc = Start-Process -FilePath $downloadPath -ArgumentList $exeArgs -Wait -PassThru -WindowStyle Hidden
    if ([int]$proc.ExitCode -ne 0) { throw ("Installer exit code " + [int]$proc.ExitCode) }
    $executedCommand = ('"' + $downloadPath + '" ' + $exeArgs)
  }

  @{
    success = $true
    message = ('Software install completed for {0}' -f $name)
    command = [string]$executedCommand
    installerType = [string]$effectiveType
    downloadPath = [string]$downloadPath
  } | ConvertTo-Json -Compress
} catch {
  @{
    success = $false
    message = [string]$_.Exception.Message
    command = [string]$executedCommand
    installerType = [string]$effectiveType
    downloadPath = [string]$downloadPath
  } | ConvertTo-Json -Compress
} finally {
  try {
    if (Test-Path -LiteralPath $tempRoot) {
      Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
    }
  } catch {}
}
exit 0
`;
        const output = await this.runPowerShellScript(script, 45 * 60 * 1000);
        const parsed = this.parseJsonOutput(output);
        if (!parsed || typeof parsed.success !== 'boolean') {
            const snippet = String(output || '').replace(/\s+/g, ' ').trim().slice(0, 320);
            return { success: false, message: snippet ? `Install response parse failure: ${snippet}` : 'Install response parse failure' };
        }
        return {
            success: parsed.success === true,
            message: String(parsed.message || (parsed.success ? `Software install completed for ${softwareName}` : 'Software install failed')),
            command: String(parsed.command || '').trim() || undefined,
            installerType: String(parsed.installerType || installerType || '').trim() || undefined,
            downloadPath: String(parsed.downloadPath || '').trim() || undefined,
        };
    }
    async handleSoftwareInstall(payload) {
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const requestedBy = String(payload?.requestedBy || 'unknown').trim() || 'unknown';
        const softwareName = String(payload?.name || payload?.package || '').trim();
        const manager = this.normalizeSoftwareManager(payload?.manager);
        const installer = payload?.installer && typeof payload.installer === 'object' ? payload.installer : null;
        const installerUrl = String(installer?.url || '').trim();
        const wingetBootstrap = payload?.wingetBootstrap === true;
        const softwareStorePackageId = String(payload?.softwareStorePackageId || payload?.storePackageId || '').trim();
        const softwareStoreTemplateId = String(payload?.softwareStoreTemplateId || '').trim();
        const softwareDeploymentId = String(payload?.softwareDeploymentId || '').trim();
        const sendResult = (success, message, extras) => {
            this.wsClient.sendMetrics('software:install:result', {
                agentId,
                requestedBy,
                name: softwareName || String(installer?.fileName || '').trim() || null,
                manager,
                wingetBootstrap,
                ...(softwareStorePackageId ? { softwareStorePackageId } : {}),
                ...(softwareStoreTemplateId ? { softwareStoreTemplateId } : {}),
                ...(softwareDeploymentId ? { softwareDeploymentId } : {}),
                success,
                message,
                ...(extras || {}),
                timestamp: new Date().toISOString(),
            });
        };
        if (!softwareName && !installerUrl && !wingetBootstrap) {
            sendResult(false, 'Software package name or installer URL is required');
            return;
        }
        if (process.platform !== 'win32') {
            sendResult(false, `Software install is not supported on ${process.platform} agents yet`);
            return;
        }
        if (wingetBootstrap) {
            const wingetState = await this.ensureWingetInstalled(true);
            sendResult(wingetState.available, wingetState.message, {
                wingetPath: wingetState.path || null,
                wingetVersion: wingetState.version || null,
                wingetMethod: wingetState.method || null,
            });
            await this.sendSoftwareSnapshot(wingetState.available ? 'post-install' : 'post-install-failed');
            return;
        }
        if (installerUrl) {
            try {
                const installResult = await this.runInstallerPayloadInstall(softwareName || String(installer?.fileName || 'Software Package'), installer);
                sendResult(installResult.success, installResult.message, {
                    sourceUrl: installerUrl,
                    installerType: installResult.installerType || this.normalizeInstallerType(installer?.fileType || installer?.installerType),
                    command: installResult.command || null,
                    downloadPath: installResult.downloadPath || null,
                });
                await this.sendSoftwareSnapshot(installResult.success ? 'post-install' : 'post-install-failed');
                return;
            }
            catch (error) {
                sendResult(false, String(error?.message || error || 'Software install command failed'), {
                    sourceUrl: installerUrl,
                    installerType: this.normalizeInstallerType(installer?.fileType || installer?.installerType),
                });
                await this.sendSoftwareSnapshot('post-install-exception');
                return;
            }
        }
        if (!softwareName) {
            sendResult(false, 'Software package name is required for package manager installs');
            return;
        }
        const safeName = softwareName.replace(/"/g, '');
        const attempts = [];
        const errors = [];
        if (manager === 'auto' || manager === 'winget') {
            const wingetState = await this.ensureWingetInstalled();
            if (!wingetState.available) {
                errors.push(`winget bootstrap: ${wingetState.message}`);
            }
            else {
                const wingetExe = String(wingetState.path || 'winget').trim() || 'winget';
                const wingetAttempts = [
                    {
                        args: ['install', '--id', safeName, '-e', '--accept-source-agreements', '--accept-package-agreements', '--silent'],
                        label: `winget install --id "${safeName}"`,
                    },
                    {
                        args: ['install', '--name', safeName, '--accept-source-agreements', '--accept-package-agreements', '--silent'],
                        label: `winget install --name "${safeName}"`,
                    },
                ];
                for (const attempt of wingetAttempts) {
                    try {
                        await this.runCommandFile(wingetExe, attempt.args, 15 * 60 * 1000);
                        sendResult(true, `Software install completed for ${softwareName}`, {
                            command: attempt.label,
                            wingetPath: wingetState.path || null,
                            wingetVersion: wingetState.version || null,
                        });
                        await this.sendSoftwareSnapshot('post-install');
                        return;
                    }
                    catch (error) {
                        errors.push(`${attempt.label}: ${String(error?.message || error || 'Software install command failed')}`);
                    }
                }
            }
        }
        if (manager === 'auto' || manager === 'choco') {
            attempts.push(`cmd /c choco install "${safeName}" -y`);
            attempts.push(`cmd /c if exist "%ProgramData%\\chocolatey\\bin\\choco.exe" ("%ProgramData%\\chocolatey\\bin\\choco.exe" install "${safeName}" -y) else exit /b 1`);
        }
        let lastError = 'Software install command failed';
        let attemptedCommand = '';
        for (const command of attempts) {
            attemptedCommand = command;
            try {
                await this.runShellCommand(command, 15 * 60 * 1000);
                sendResult(true, `Software install completed for ${softwareName}`, { command });
                await this.sendSoftwareSnapshot('post-install');
                return;
            }
            catch (error) {
                lastError = String(error?.message || error || 'Software install command failed');
                errors.push(`${command}: ${lastError}`);
            }
        }
        if (manager === 'auto' || manager === 'choco') {
            const bootstrapChocoCommand = "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference='Stop'; Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))\"";
            try {
                await this.runShellCommand(bootstrapChocoCommand, 10 * 60 * 1000);
                const retryCommands = [
                    `cmd /c choco install "${safeName}" -y`,
                    `cmd /c if exist "%ProgramData%\\chocolatey\\bin\\choco.exe" ("%ProgramData%\\chocolatey\\bin\\choco.exe" install "${safeName}" -y) else exit /b 1`,
                ];
                for (const command of retryCommands) {
                    attemptedCommand = command;
                    try {
                        await this.runShellCommand(command, 15 * 60 * 1000);
                        sendResult(true, `Software install completed for ${softwareName}`, { command, managerFallback: 'choco-bootstrap' });
                        await this.sendSoftwareSnapshot('post-install');
                        return;
                    }
                    catch (error) {
                        lastError = String(error?.message || error || 'Software install command failed');
                        errors.push(`${command}: ${lastError}`);
                    }
                }
            }
            catch (error) {
                const bootstrapError = String(error?.message || error || 'Chocolatey bootstrap failed');
                errors.push(`choco-bootstrap: ${bootstrapError}`);
            }
        }
        const failureSummary = errors.length ? errors.slice(-3).join(' | ') : lastError;
        sendResult(false, `${lastError}. Package manager fallback exhausted. ${failureSummary}`, attemptedCommand ? { command: attemptedCommand } : undefined);
    }
    async handleSoftwareUpdate(payload) {
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const requestedBy = String(payload?.requestedBy || 'unknown').trim() || 'unknown';
        const softwareName = String(payload?.name || payload?.package || '').trim();
        const manager = this.normalizeSoftwareManager(payload?.manager);
        const sendResult = (success, message, extras) => {
            this.wsClient.sendMetrics('software:update:result', {
                agentId,
                requestedBy,
                name: softwareName || null,
                manager,
                success,
                message,
                ...(extras || {}),
                timestamp: new Date().toISOString(),
            });
        };
        if (process.platform !== 'win32') {
            sendResult(false, `Software update is not supported on ${process.platform} agents yet`);
            return;
        }
        const safeName = softwareName.replace(/"/g, '');
        const errors = [];
        const attempts = [];
        if (manager === 'auto' || manager === 'winget') {
            const wingetState = await this.ensureWingetInstalled();
            if (!wingetState.available) {
                errors.push(`winget bootstrap: ${wingetState.message}`);
            }
            else {
                const wingetExe = String(wingetState.path || 'winget').trim() || 'winget';
                const wingetAttempts = softwareName
                    ? [
                        {
                            args: ['upgrade', '--id', safeName, '-e', '--accept-source-agreements', '--accept-package-agreements', '--silent'],
                            label: `winget upgrade --id "${safeName}"`,
                        },
                        {
                            args: ['upgrade', '--name', safeName, '--accept-source-agreements', '--accept-package-agreements', '--silent'],
                            label: `winget upgrade --name "${safeName}"`,
                        },
                    ]
                    : [
                        {
                            args: ['upgrade', '--all', '--accept-source-agreements', '--accept-package-agreements', '--silent'],
                            label: 'winget upgrade --all',
                        },
                    ];
                for (const attempt of wingetAttempts) {
                    try {
                        await this.runCommandFile(wingetExe, attempt.args, 25 * 60 * 1000);
                        sendResult(true, softwareName ? `Software updated: ${softwareName}` : 'Software update run completed', {
                            command: attempt.label,
                            wingetPath: wingetState.path || null,
                            wingetVersion: wingetState.version || null,
                        });
                        await this.sendSoftwareSnapshot('post-update');
                        return;
                    }
                    catch (error) {
                        errors.push(`${attempt.label}: ${String(error?.message || error || 'Software update command failed')}`);
                    }
                }
            }
        }
        if (softwareName) {
            if (manager === 'auto' || manager === 'choco') {
                attempts.push(`cmd /c choco upgrade "${safeName}" -y`);
            }
        }
        else {
            if (manager === 'auto' || manager === 'choco') {
                attempts.push('cmd /c choco upgrade all -y');
            }
        }
        let lastError = 'Software update command failed';
        let attemptedCommand = '';
        for (const command of attempts) {
            attemptedCommand = command;
            try {
                await this.runShellCommand(command, 25 * 60 * 1000);
                sendResult(true, softwareName ? `Software updated: ${softwareName}` : 'Software update run completed', { command });
                await this.sendSoftwareSnapshot('post-update');
                return;
            }
            catch (error) {
                lastError = String(error?.message || error || 'Software update command failed');
            }
        }
        const failureSummary = errors.length ? errors.slice(-3).join(' | ') : lastError;
        sendResult(false, failureSummary || lastError, {
            ...(attemptedCommand ? { command: attemptedCommand } : {}),
        });
    }
    getSupportedMaintenanceTaskKeys() {
        return ['clear_temp', 'disk_cleanup', 'defrag_disk', 'trim_ssd', 'flush_dns', 'clear_prefetch'];
    }
    getMaintenanceTaskScript(task) {
        const taskKey = String(task || '').trim().toLowerCase();
        if (taskKey === 'clear_temp') {
            return [
                "$ErrorActionPreference = 'SilentlyContinue'",
                '$paths = @($env:TEMP, "C:\\Windows\\Temp")',
                '$removed = 0',
                'foreach ($p in $paths) {',
                '  if (-not [string]::IsNullOrWhiteSpace($p) -and (Test-Path $p)) {',
                '    Get-ChildItem -LiteralPath $p -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue',
                '    $removed++',
                '  }',
                '}',
                "'Temp cleanup completed'",
            ].join('\n');
        }
        if (taskKey === 'disk_cleanup') {
            return [
                "$ErrorActionPreference = 'SilentlyContinue'",
                '$paths = @($env:TEMP, "C:\\Windows\\Temp", "C:\\Windows\\SoftwareDistribution\\Download")',
                '$removed = 0',
                'foreach ($p in $paths) {',
                '  if (-not [string]::IsNullOrWhiteSpace($p) -and (Test-Path $p)) {',
                '    Get-ChildItem -LiteralPath $p -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue',
                '    $removed++',
                '  }',
                '}',
                'try { Clear-RecycleBin -Force -ErrorAction SilentlyContinue } catch {}',
                "'Disk cleanup completed'",
            ].join('\n');
        }
        if (taskKey === 'defrag_disk') {
            return [
                "$ErrorActionPreference = 'Stop'",
                "$proc = Start-Process -FilePath 'defrag.exe' -ArgumentList 'C:', '/O' -Wait -PassThru -WindowStyle Hidden",
                'if ([int]$proc.ExitCode -ne 0) { throw ("defrag exit code " + [int]$proc.ExitCode) }',
                "'Disk defragmentation completed'",
            ].join('\n');
        }
        if (taskKey === 'trim_ssd') {
            return [
                "$ErrorActionPreference = 'Stop'",
                "$proc = Start-Process -FilePath 'defrag.exe' -ArgumentList 'C:', '/L' -Wait -PassThru -WindowStyle Hidden",
                'if ([int]$proc.ExitCode -ne 0) { throw ("defrag retrim exit code " + [int]$proc.ExitCode) }',
                "'SSD retrim completed'",
            ].join('\n');
        }
        if (taskKey === 'flush_dns') {
            return [
                "$ErrorActionPreference = 'Stop'",
                "$proc = Start-Process -FilePath 'ipconfig.exe' -ArgumentList '/flushdns' -Wait -PassThru -WindowStyle Hidden",
                'if ([int]$proc.ExitCode -ne 0) { throw ("ipconfig /flushdns exit code " + [int]$proc.ExitCode) }',
                "'DNS cache flushed'",
            ].join('\n');
        }
        if (taskKey === 'clear_prefetch') {
            return [
                "$ErrorActionPreference = 'SilentlyContinue'",
                '$prefetch = "C:\\Windows\\Prefetch"',
                'if (Test-Path $prefetch) {',
                '  Get-ChildItem -LiteralPath $prefetch -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue',
                '}',
                "'Prefetch cache cleared'",
            ].join('\n');
        }
        return null;
    }
    async handleMaintenanceRun(payload) {
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const requestedBy = String(payload?.requestedBy || 'unknown').trim() || 'unknown';
        const customScript = String(payload?.customScript || payload?.custom_script || '').trim();
        const customScriptName = String(payload?.customScriptName || payload?.custom_script_name || 'Custom Script').trim() || 'Custom Script';
        const supportedTasks = new Set(this.getSupportedMaintenanceTaskKeys());
        const rawTasks = Array.isArray(payload?.tasks)
            ? payload.tasks
            : [payload?.task];
        const tasks = Array.from(new Set(rawTasks
            .map((task) => String(task || '').trim().toLowerCase())
            .filter((task) => supportedTasks.has(task))));
        const effectiveTasks = customScript ? Array.from(new Set([...tasks, 'custom_script'])) : tasks;
        const sendResult = (success, message, results) => {
            this.wsClient.sendMetrics('maintenance:result', {
                agentId,
                requestedBy,
                success,
                message,
                tasks: effectiveTasks,
                results: Array.isArray(results) ? results : [],
                timestamp: new Date().toISOString(),
            });
        };
        if (!tasks.length && !customScript) {
            sendResult(false, 'No valid maintenance tasks or custom script provided');
            return;
        }
        if (process.platform !== 'win32') {
            sendResult(false, `Maintenance actions are not supported on ${process.platform} agents yet`);
            return;
        }
        const results = [];
        for (const task of tasks) {
            const script = this.getMaintenanceTaskScript(task);
            if (!script) {
                results.push({ task, success: false, message: 'Unsupported maintenance task' });
                continue;
            }
            try {
                const timeoutMs = (task === 'defrag_disk' || task === 'trim_ssd') ? 45 * 60 * 1000 : 10 * 60 * 1000;
                const output = await this.runPowerShellScript(script, timeoutMs);
                results.push({ task, success: true, message: String(output || '').trim() || 'Completed' });
            }
            catch (error) {
                results.push({ task, success: false, message: String(error?.message || error || 'Task failed') });
            }
        }
        if (customScript) {
            try {
                const output = await this.runPowerShellScript(customScript, 30 * 60 * 1000);
                results.push({ task: 'custom_script', name: customScriptName, success: true, message: String(output || '').trim() || 'Completed' });
            }
            catch (error) {
                results.push({ task: 'custom_script', name: customScriptName, success: false, message: String(error?.message || error || 'Task failed') });
            }
        }
        const successCount = results.filter((r) => r.success).length;
        const failedCount = results.length - successCount;
        if (failedCount === 0) {
            sendResult(true, `Maintenance completed for ${successCount} task(s)`, results);
        }
        else {
            sendResult(false, `Maintenance finished with ${failedCount} failed task(s)`, results);
        }
    }
    async handleMaintenanceSchedule(payload) {
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const requestedBy = String(payload?.requestedBy || 'unknown').trim() || 'unknown';
        const task = String(payload?.task || '').trim().toLowerCase();
        const frequencyRaw = String(payload?.frequency || 'daily').trim().toLowerCase();
        const frequency = frequencyRaw === 'weekly' ? 'weekly' : 'daily';
        const day = String(payload?.day || 'MON').trim().toUpperCase();
        const time = String(payload?.time || '02:00').trim();
        const customScript = String(payload?.customScript || payload?.custom_script || '').trim();
        const customScriptName = String(payload?.customScriptName || payload?.custom_script_name || 'custom-script').trim() || 'custom-script';
        const supportedTasks = new Set(this.getSupportedMaintenanceTaskKeys());
        const sendResult = (success, message, extras) => {
            this.wsClient.sendMetrics('maintenance:schedule:result', {
                agentId,
                requestedBy,
                success,
                message,
                task,
                frequency,
                day,
                time,
                ...(extras || {}),
                timestamp: new Date().toISOString(),
            });
        };
        if (!task || (!supportedTasks.has(task) && task !== 'custom_script')) {
            sendResult(false, 'Unsupported maintenance task for scheduling');
            return;
        }
        if (task === 'custom_script' && !customScript) {
            sendResult(false, 'Custom script content is required for custom_script scheduling');
            return;
        }
        if (!/^([01]\\d|2[0-3]):[0-5]\\d$/.test(time)) {
            sendResult(false, 'Invalid schedule time format. Use HH:MM (24-hour).');
            return;
        }
        if (process.platform !== 'win32') {
            sendResult(false, `Maintenance scheduling is not supported on ${process.platform} agents yet`);
            return;
        }
        const taskScript = task === 'custom_script' ? customScript : (this.getMaintenanceTaskScript(task) || '');
        if (!taskScript) {
            sendResult(false, 'Failed to resolve maintenance script');
            return;
        }
        const encodedTask = Buffer.from(taskScript, 'utf16le').toString('base64');
        const taskCommand = `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedTask}`;
        const scheduleClause = frequency === 'weekly'
            ? `/SC WEEKLY /D ${day}`
            : '/SC DAILY';
        const taskNameSuffix = (task === 'custom_script' ? customScriptName : task)
            .toLowerCase()
            .replace(/[^a-z0-9_-]/g, '-')
            .replace(/-+/g, '-')
            .replace(/^-|-$/g, '')
            .slice(0, 40) || 'custom-script';
        const taskName = `AppStats-Maintenance-${taskNameSuffix}`;
        const command = `cmd /c schtasks /Create /F /TN "${taskName}" /TR "${taskCommand}" ${scheduleClause} /ST ${time} /RU SYSTEM`;
        try {
            const output = await this.runShellCommand(command, 60000);
            sendResult(true, 'Maintenance schedule created/updated', { output: String(output || '').trim(), taskName });
        }
        catch (error) {
            sendResult(false, String(error?.message || error || 'Failed to schedule maintenance task'));
        }
    }
    startLiveMonitoring() {
        // When live mode is enabled, send system metrics more frequently (1 second)
        const liveInterval = setInterval(async () => {
            if (!this.liveMonitoring) {
                clearInterval(liveInterval);
                return;
            }
            try {
                const quickMetrics = await this.systemCollector.collectQuick();
                this.wsClient.sendMetrics('system:live', quickMetrics);
            }
            catch { }
        }, 1000);
        this.intervals.push(liveInterval);
    }
    applyServerConfig(serverConfig) {
        if (!serverConfig)
            return;
        let collectorIntervalsChanged = false;
        let runtimeStateChanged = false;
        if (serverConfig.customConfig && typeof serverConfig.customConfig === 'object') {
            this.applyCustomReportingConfig(serverConfig.customConfig);
            collectorIntervalsChanged = this.applyCustomCollectorIntervals(serverConfig.customConfig);
        }
        if (serverConfig.tenantFeatures && typeof serverConfig.tenantFeatures === 'object') {
            runtimeStateChanged = this.applyTenantFeatureConfig(serverConfig.tenantFeatures) || runtimeStateChanged;
        }
        // Apply probe configurations
        if (serverConfig.stopAllProbes === true || !this.tenantFeatures.probes) {
            this.stopAllProbes();
        }
        if (serverConfig.probes && this.tenantFeatures.probes) {
            for (const probe of serverConfig.probes) {
                if (probe.enabled) {
                    this.startProbe(probe);
                }
            }
        }
        // Apply traffic rules
        if (serverConfig.trafficRules) {
            this.networkCollector.updateTrafficRules(serverConfig.trafficRules);
        }
        // Apply URL filters
        if (serverConfig.urlFilters) {
            this.networkCollector.updateURLFilters(serverConfig.urlFilters);
        }
        if (Array.isArray(serverConfig.softwarePolicies)) {
            const previousPolicyCount = this.softwarePolicies.length;
            this.softwarePolicies = this.normalizeSoftwarePolicies(serverConfig.softwarePolicies);
            logger_1.logger.info(`Updated ${this.softwarePolicies.length} software policies`);
            if (previousPolicyCount !== this.softwarePolicies.length && !this.tenantFeatures.processMonitoring) {
                runtimeStateChanged = true;
            }
        }
        // Apply productivity rules
        if (serverConfig.productivityRules) {
            const rulesMap = {};
            const normalizeScore = (value) => {
                const raw = String(value ?? '').trim().toLowerCase();
                if (raw === 'productive' || raw === 'neutral' || raw === 'unproductive')
                    return raw;
                const n = Number(raw);
                if (Number.isFinite(n)) {
                    if (n >= 7)
                        return 'productive';
                    if (n <= 3)
                        return 'unproductive';
                }
                return 'neutral';
            };
            for (const rule of serverConfig.productivityRules) {
                const pattern = String(rule.pattern || '').trim().toLowerCase();
                if (!pattern)
                    continue;
                rulesMap[pattern] = normalizeScore(rule.score);
            }
            this.productivityCollector.updateRules(rulesMap);
        }
        // Apply granular tracking config from tenant settings
        if (serverConfig.trackingConfig || serverConfig.tracking_mode !== undefined) {
            const tc = serverConfig.trackingConfig || serverConfig;
            this.productivityCollector.applyTrackingConfig({
                tracking_mode: tc.tracking_mode,
                track_active_window: tc.track_active_window,
                track_keystrokes_activity: tc.track_keystrokes_activity,
                track_keystrokes_log: tc.track_keystrokes_log,
                track_mouse_activity: tc.track_mouse_activity,
                track_browser_urls: tc.track_browser_urls,
                track_browser_titles: tc.track_browser_titles,
                track_network_traffic: tc.track_network_traffic,
                track_screenshots: tc.track_screenshots,
                screenshot_interval_seconds: tc.screenshot_interval_seconds,
                privacy_anon_users: tc.privacy_anon_users,
                privacy_blur_titles: tc.privacy_blur_titles,
                privacy_exclude_urls: tc.privacy_exclude_urls,
            });
        }
        if (collectorIntervalsChanged || runtimeStateChanged) {
            logger_1.logger.info('Collector intervals updated from server config', {
                system: config_1.config.intervals.system,
                process: config_1.config.intervals.process,
                network: config_1.config.intervals.network,
                wifi: config_1.config.intervals.wifi,
            });
            this.startCollectors();
        }
        logger_1.logger.info('Server configuration applied');
    }
    startProbe(probeConfig) {
        if (!this.tenantFeatures.probes) {
            logger_1.logger.debug('Ignoring probe configuration because probes are disabled by tenant licensing');
            return;
        }
        const { id, type, target } = probeConfig;
        const interval = probeConfig?.interval ?? probeConfig?.interval_seconds;
        const count = probeConfig?.count ?? probeConfig?.packet_count;
        const timeout = probeConfig?.timeout ?? probeConfig?.timeout_ms;
        const probeId = String(id || '');
        if (!probeId || !type || !target) {
            logger_1.logger.warn('Invalid probe configuration received', { probeConfig });
            return;
        }
        // Stop existing probe timer
        this.stopProbe(probeId);
        const intervalSeconds = Math.max(1, Number(interval) || 300);
        const timeoutFor = (fallbackMs) => Math.max(100, Number(timeout) || fallbackMs);
        const probeCount = Math.max(1, Number(count) || 10);
        const isCriticalProbe = probeConfig?.isCritical === true
            || Number(probeConfig?.is_critical || 0) === 1
            || Number(probeConfig?.critical_ws_push || 0) === 1
            || String(probeConfig?.priority || '').trim().toLowerCase() === 'critical';
        logger_1.logger.info(`Starting probe: ${type} -> ${target} every ${intervalSeconds}s`);
        const runProbe = async () => {
            if (this.probeInFlight.has(probeId)) {
                logger_1.logger.debug(`Probe ${probeId} still running, skipping overlap`);
                return;
            }
            this.probeInFlight.add(probeId);
            try {
                let resultType = '';
                let result = null;
                if (type === 'ping') {
                    // Use one packet per scheduled run so 1s probes can produce 1 record per second.
                    result = await this.probeRunner.runPing(target, 1, timeoutFor(5000));
                    resultType = 'probe:ping-result';
                }
                else if (type === 'mtr' || type === 'traceroute') {
                    result = await this.probeRunner.runMTR(target, probeCount, timeoutFor(30000));
                    resultType = 'probe:mtr-result';
                }
                else if (type === 'http') {
                    result = await this.runHTTPProbe(target, timeoutFor(10000));
                    resultType = 'probe:http-result';
                }
                else if (type === 'dns') {
                    result = await this.runDNSProbe(target, timeoutFor(5000));
                    resultType = 'probe:dns-result';
                }
                else if (type === 'tcp') {
                    result = await this.runTCPProbe(target, timeoutFor(5000));
                    resultType = 'probe:tcp-result';
                }
                else {
                    logger_1.logger.warn(`Unknown probe type: ${type}`);
                }
                if (result && resultType) {
                    const payload = {
                        ...result,
                        timestamp: new Date().toISOString(),
                    };
                    if (isCriticalProbe) {
                        this.wsClient.send({
                            type: resultType,
                            payload: { probeId, ...payload },
                            agentId: this.wsClient.getAgentId() || undefined,
                        });
                    }
                    else {
                        // Buffer standard probe results for batched sending.
                        const bufKey = `${probeId}:${resultType}`;
                        if (!this.probeResultBuffers.has(bufKey)) {
                            this.probeResultBuffers.set(bufKey, { type: resultType, probeId: probeId, results: [] });
                        }
                        this.probeResultBuffers.get(bufKey).results.push(payload);
                        if (!this.probeFlushTimer) {
                            this.probeFlushTimer = setInterval(() => this.flushProbeResults(), 30000);
                        }
                    }
                }
            }
            catch (e) {
                logger_1.logger.error(`Probe ${probeId} error:`, e);
                this.wsClient.sendMetrics('probe:error', { probeId: probeId, error: e.message });
            }
            finally {
                this.probeInFlight.delete(probeId);
            }
        };
        // Run immediately
        runProbe();
        // Schedule recurring
        const timer = setInterval(runProbe, intervalSeconds * 1000);
        this.probeTimers.set(probeId, timer);
    }
    async runHTTPProbe(target, timeout) {
        return new Promise((resolve) => {
            const probeStartTime = Date.now();
            const rawTarget = String(target || '').trim();
            const hasExplicitScheme = /^https?:\/\//i.test(rawTarget);
            const normalizedTarget = hasExplicitScheme ? rawTarget : `https://${rawTarget}`;
            const certErrorCodes = new Set([
                'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
                'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
                'SELF_SIGNED_CERT_IN_CHAIN',
                'DEPTH_ZERO_SELF_SIGNED_CERT',
                'CERT_HAS_EXPIRED',
            ]);
            const networkFallbackCodes = new Set([
                'ETIMEDOUT',
                'ECONNREFUSED',
                'ECONNRESET',
                'EHOSTUNREACH',
                'ENETUNREACH',
                'EAI_AGAIN',
                'ENOTFOUND',
            ]);
            let resolved = false;
            let fallbackUsed = false;
            let fallbackReason = '';
            let httpFallbackUsed = false;
            let httpFallbackReason = '';
            let hasTriedHttpFallback = false;
            const resolveOnce = (payload) => {
                if (resolved)
                    return;
                resolved = true;
                resolve(payload);
            };
            let url;
            try {
                url = new url_1.URL(normalizedTarget);
            }
            catch (error) {
                resolveOnce({
                    url: normalizedTarget,
                    error: String(error?.message || error || 'Invalid target URL'),
                    totalTimeMs: Date.now() - probeStartTime,
                    timestamp: new Date().toISOString(),
                });
                return;
            }
            const executeRequest = (requestUrl, verifyTls, allowFallback) => {
                const requestStart = Date.now();
                let ttfbMs = 0;
                let finished = false;
                const finish = (handler) => {
                    if (finished)
                        return;
                    finished = true;
                    handler();
                };
                const isHttps = requestUrl.protocol === 'https:';
                const client = isHttps ? https_1.default : http_1.default;
                const requestOptions = { method: 'GET', timeout };
                if (isHttps && !verifyTls) {
                    requestOptions.rejectUnauthorized = false;
                }
                const req = client.request(requestUrl, requestOptions, (res) => {
                    ttfbMs = Date.now() - requestStart;
                    let dataSize = 0;
                    res.on('data', (chunk) => {
                        dataSize += chunk.length;
                    });
                    res.on('end', () => {
                        finish(() => {
                            const socket = res.socket;
                            const cert = typeof socket?.getPeerCertificate === 'function'
                                ? (socket.getPeerCertificate() || {})
                                : null;
                            const authorizationError = String(socket?.authorizationError || fallbackReason || '').trim() || null;
                            const tlsInfo = cert ? {
                                valid: !authorizationError,
                                authorizationError,
                                verificationBypassed: fallbackUsed,
                                issuer: cert?.issuer?.O || cert?.issuer?.CN || 'unknown',
                                subject: cert?.subject?.CN || null,
                                expiresAt: cert?.valid_to || null,
                            } : null;
                            const warnings = [];
                            if (fallbackUsed) {
                                warnings.push(`TLS verification bypassed after certificate validation failure (${fallbackReason || 'unknown certificate error'})`);
                            }
                            if (httpFallbackUsed) {
                                warnings.push(`HTTPS attempt failed and probe retried over HTTP (${httpFallbackReason || 'network error'})`);
                            }
                            resolveOnce({
                                url: requestUrl.toString(),
                                statusCode: res.statusCode,
                                statusMessage: res.statusMessage,
                                headers: res.headers,
                                ttfbMs,
                                totalTimeMs: Date.now() - probeStartTime,
                                responseSizeBytes: dataSize,
                                contentType: res.headers['content-type'] || null,
                                tls: tlsInfo,
                                warning: warnings.length > 0 ? warnings.join('; ') : undefined,
                                timestamp: new Date().toISOString(),
                            });
                        });
                    });
                });
                req.on('error', (err) => {
                    finish(() => {
                        const errMessage = String(err?.message || err || 'Request failed');
                        const errCode = String(err?.code || '').trim().toUpperCase();
                        const certLikeError = certErrorCodes.has(errCode)
                            || /unable to get local issuer certificate|unable to verify the first certificate|self[-\s]?signed certificate|certificate has expired/i.test(errMessage);
                        if (allowFallback && isHttps && verifyTls && certLikeError) {
                            fallbackUsed = true;
                            fallbackReason = errCode || errMessage;
                            logger_1.logger.warn('HTTP probe TLS validation failed, retrying without certificate verification', {
                                target: normalizedTarget,
                                error: errMessage,
                                code: errCode || null,
                            });
                            executeRequest(requestUrl, false, false);
                            return;
                        }
                        const networkLikeError = networkFallbackCodes.has(errCode)
                            || /timed out|socket hang up|connect|network is unreachable|getaddrinfo|lookup/i.test(errMessage);
                        if (!hasExplicitScheme && isHttps && !hasTriedHttpFallback && networkLikeError) {
                            hasTriedHttpFallback = true;
                            httpFallbackUsed = true;
                            httpFallbackReason = errCode || errMessage;
                            const httpUrl = new url_1.URL(requestUrl.toString());
                            httpUrl.protocol = 'http:';
                            logger_1.logger.warn('HTTP probe HTTPS attempt failed, retrying over HTTP for scheme-less target', {
                                target: normalizedTarget,
                                retryTarget: httpUrl.toString(),
                                error: errMessage,
                                code: errCode || null,
                            });
                            executeRequest(httpUrl, true, false);
                            return;
                        }
                        resolveOnce({
                            url: requestUrl.toString(),
                            error: errMessage,
                            errorCode: errCode || null,
                            totalTimeMs: Date.now() - probeStartTime,
                            ttfbMs: ttfbMs || null,
                            timestamp: new Date().toISOString(),
                        });
                    });
                });
                req.on('timeout', () => {
                    finish(() => {
                        req.destroy();
                        if (!hasExplicitScheme && isHttps && !hasTriedHttpFallback) {
                            hasTriedHttpFallback = true;
                            httpFallbackUsed = true;
                            httpFallbackReason = 'ETIMEDOUT';
                            const httpUrl = new url_1.URL(requestUrl.toString());
                            httpUrl.protocol = 'http:';
                            logger_1.logger.warn('HTTP probe HTTPS attempt timed out, retrying over HTTP for scheme-less target', {
                                target: normalizedTarget,
                                retryTarget: httpUrl.toString(),
                            });
                            executeRequest(httpUrl, true, false);
                            return;
                        }
                        resolveOnce({
                            url: requestUrl.toString(),
                            error: 'Request timed out',
                            errorCode: 'ETIMEDOUT',
                            totalTimeMs: Date.now() - probeStartTime,
                            ttfbMs: ttfbMs || null,
                            timestamp: new Date().toISOString(),
                        });
                    });
                });
                req.end();
            };
            executeRequest(url, true, true);
        });
    }
    async runDNSProbe(target, timeout) {
        return new Promise((resolve) => {
            const startTime = Date.now();
            const hostname = target.replace(/^https?:\/\//, '').split('/')[0].split(':')[0];
            const timer = setTimeout(() => {
                resolve({
                    hostname,
                    error: 'DNS lookup timed out',
                    totalTimeMs: Date.now() - startTime,
                    timestamp: new Date().toISOString()
                });
            }, timeout);
            const results = { hostname, timestamp: new Date().toISOString(), records: {} };
            // Resolve A records
            dns_1.default.resolve4(hostname, (err, addresses) => {
                if (!err)
                    results.records.A = addresses;
                else
                    results.records.A_error = err.message;
                // Resolve AAAA records
                dns_1.default.resolve6(hostname, (err6, addresses6) => {
                    if (!err6)
                        results.records.AAAA = addresses6;
                    else
                        results.records.AAAA_error = err6?.message;
                    // Resolve MX records
                    dns_1.default.resolveMx(hostname, (errMx, mx) => {
                        if (!errMx)
                            results.records.MX = mx;
                        else
                            results.records.MX_error = errMx?.message;
                        // Resolve NS records
                        dns_1.default.resolveNs(hostname, (errNs, ns) => {
                            if (!errNs)
                                results.records.NS = ns;
                            else
                                results.records.NS_error = errNs?.message;
                            clearTimeout(timer);
                            results.totalTimeMs = Date.now() - startTime;
                            resolve(results);
                        });
                    });
                });
            });
        });
    }
    async runTCPProbe(target, timeout) {
        return new Promise((resolve) => {
            const startTime = Date.now();
            // Parse host:port, default port 80
            const parts = target.replace(/^https?:\/\//, '').split('/')[0].split(':');
            const host = parts[0];
            const port = parseInt(parts[1]) || 80;
            const socket = new net_1.default.Socket();
            let banner = '';
            let connected = false;
            let connectTime = 0;
            socket.setTimeout(timeout);
            socket.on('connect', () => {
                connectTime = Date.now() - startTime;
                connected = true;
            });
            socket.on('data', (data) => {
                banner += data.toString().substring(0, 512);
                // Got a banner, close
                socket.end();
            });
            socket.on('timeout', () => {
                socket.destroy();
                resolve({
                    host, port, connected,
                    error: connected ? 'Read timed out after connect' : 'Connection timed out',
                    connectTimeMs: connectTime || null,
                    totalTimeMs: Date.now() - startTime,
                    timestamp: new Date().toISOString()
                });
            });
            socket.on('error', (err) => {
                resolve({
                    host, port, connected,
                    error: err.message,
                    connectTimeMs: connectTime || null,
                    totalTimeMs: Date.now() - startTime,
                    timestamp: new Date().toISOString()
                });
            });
            socket.on('end', () => {
                resolve({
                    host, port, connected: true,
                    connectTimeMs: connectTime,
                    totalTimeMs: Date.now() - startTime,
                    banner: banner || null,
                    timestamp: new Date().toISOString()
                });
            });
            // If no data after 2s post-connect, resolve anyway
            socket.on('connect', () => {
                setTimeout(() => {
                    if (socket.writable) {
                        socket.end();
                    }
                }, 2000);
            });
            socket.connect(port, host);
        });
    }
    stopProbe(probeId) {
        const timer = this.probeTimers.get(probeId);
        if (timer) {
            clearInterval(timer);
            this.probeTimers.delete(probeId);
            logger_1.logger.info(`Stopped probe: ${probeId}`);
        }
        this.probeInFlight.delete(probeId);
        for (const [bufKey, buf] of this.probeResultBuffers.entries()) {
            if (buf.probeId === probeId || bufKey.startsWith(`${probeId}:`)) {
                this.probeResultBuffers.delete(bufKey);
            }
        }
        if (this.probeTimers.size === 0 && this.probeFlushTimer) {
            clearInterval(this.probeFlushTimer);
            this.probeFlushTimer = null;
        }
    }
    stopAllProbes() {
        for (const probeId of Array.from(this.probeTimers.keys())) {
            this.stopProbe(probeId);
        }
    }
    runShellCommand(command, timeoutMs = 180000) {
        return new Promise((resolve, reject) => {
            const { exec } = require('child_process');
            exec(command, {
                timeout: timeoutMs,
                maxBuffer: 8 * 1024 * 1024,
                windowsHide: true,
            }, (error, stdout, stderr) => {
                if (error) {
                    reject(new Error((stderr || error.message || '').toString().trim() || 'Command failed'));
                    return;
                }
                resolve(String(stdout || stderr || '').trim());
            });
        });
    }
    runCommandFile(command, args, timeoutMs = 180000, options = {}) {
        return new Promise((resolve, reject) => {
            const { execFile } = require('child_process');
            execFile(command, Array.isArray(args) ? args : [], {
                timeout: timeoutMs,
                maxBuffer: 8 * 1024 * 1024,
                windowsHide: true,
                env: options.env || process.env,
            }, (error, stdout, stderr) => {
                if (error) {
                    reject(new Error((stderr || stdout || error.message || '').toString().trim() || 'Command failed'));
                    return;
                }
                resolve(String(stdout || stderr || '').trim());
            });
        });
    }
    downloadFileWithRedirects(url, destinationPath, options = {}) {
        const timeoutMs = Math.max(5000, Number(options.timeoutMs || 30000) || 30000);
        const maxRedirects = Math.max(0, Number(options.maxRedirects || 5) || 5);
        const minBytes = Math.max(1, Number(options.minBytes || 1) || 1);
        const headers = options.headers || {};
        const download = (currentUrl, redirectsRemaining) => new Promise((resolve, reject) => {
            let settled = false;
            const finish = (error) => {
                if (settled)
                    return;
                settled = true;
                if (error) {
                    try {
                        fs_1.default.unlinkSync(destinationPath);
                    }
                    catch { }
                    reject(error);
                    return;
                }
                resolve();
            };
            const handleError = (error) => {
                finish(new Error(String(error?.message || error || 'Download failed').trim() || 'Download failed'));
            };
            const parsedUrl = new url_1.URL(currentUrl);
            const client = parsedUrl.protocol === 'https:' ? https_1.default : http_1.default;
            const request = client.get(currentUrl, { headers }, (response) => {
                const status = Number(response.statusCode || 0);
                const location = String(response.headers.location || '').trim();
                if ((status === 301 || status === 302 || status === 307 || status === 308) && location) {
                    response.resume();
                    if (redirectsRemaining <= 0) {
                        handleError(new Error('Too many redirects'));
                        return;
                    }
                    const nextUrl = new url_1.URL(location, currentUrl).toString();
                    download(nextUrl, redirectsRemaining - 1).then(resolve).catch(reject);
                    return;
                }
                if (status !== 200) {
                    response.resume();
                    handleError(new Error(`HTTP ${status || 'unknown'}`));
                    return;
                }
                let totalBytes = 0;
                const file = fs_1.default.createWriteStream(destinationPath);
                response.on('data', (chunk) => {
                    totalBytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
                });
                response.on('error', handleError);
                file.on('error', handleError);
                file.on('finish', () => {
                    file.close(() => {
                        if (totalBytes < minBytes) {
                            handleError(new Error(`Downloaded file too small (${totalBytes} bytes)`));
                            return;
                        }
                        finish();
                    });
                });
                response.pipe(file);
            });
            request.setTimeout(timeoutMs, () => {
                request.destroy(new Error('Download timed out'));
            });
            request.on('error', handleError);
        });
        return download(url, maxRedirects);
    }
    runPowerShellScript(script, timeoutMs = 180000) {
        const encoded = Buffer.from(String(script || ''), 'utf16le').toString('base64');
        const command = `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
        if (process.platform === 'win32' && command.length > 7800) {
            return this.runPowerShellScriptViaFile(script, timeoutMs);
        }
        return this.runShellCommand(command, timeoutMs);
    }
    runPowerShellPromptScript(script, timeoutMs = 60000) {
        const encoded = Buffer.from(String(script || ''), 'utf16le').toString('base64');
        return this.runShellCommand(`powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encoded}`, timeoutMs);
    }
    runPowerShellInteractiveTask(script, timeoutMs = 20000) {
        return new Promise((resolve, reject) => {
            if (process.platform !== 'win32') {
                reject(new Error('interactive powershell task is only available on Windows'));
                return;
            }
            const { execFile } = require('child_process');
            const fs = require('fs');
            const os = require('os');
            const path = require('path');
            const tempFile = path.join(os.tmpdir(), `appstats-emremote-${Date.now()}-${Math.random().toString(16).slice(2)}.ps1`);
            const taskName = `AppStatsEmRemote-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
            const startAt = new Date(Date.now() + 60 * 1000);
            const startTime = `${String(startAt.getHours()).padStart(2, '0')}:${String(startAt.getMinutes()).padStart(2, '0')}`;
            try {
                fs.writeFileSync(tempFile, String(script || ''), 'utf8');
            }
            catch (error) {
                reject(new Error(String(error?.message || error || 'failed to create interactive script')));
                return;
            }
            const runTask = (args, taskTimeoutMs = timeoutMs) => new Promise((done) => {
                execFile('schtasks.exe', args, { timeout: taskTimeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
                    done({
                        ok: !error,
                        stdout: String(stdout || '').trim(),
                        stderr: String(stderr || '').trim(),
                    });
                });
            });
            void (async () => {
                try {
                    const create = await runTask([
                        '/Create', '/F',
                        '/TN', taskName,
                        '/TR', `powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "${tempFile}"`,
                        '/SC', 'ONCE',
                        '/ST', startTime,
                        '/RU', 'INTERACTIVE',
                        '/IT',
                        '/RL', 'HIGHEST',
                    ], timeoutMs);
                    if (!create.ok) {
                        throw new Error(`interactive task create failed: ${(create.stderr || create.stdout || 'unknown').slice(0, 220)}`);
                    }
                    const started = await runTask(['/Run', '/TN', taskName], timeoutMs);
                    if (!started.ok) {
                        throw new Error(`interactive task run failed: ${(started.stderr || started.stdout || 'unknown').slice(0, 220)}`);
                    }
                    await runTask(['/Delete', '/F', '/TN', taskName], timeoutMs);
                    resolve();
                }
                catch (error) {
                    await runTask(['/Delete', '/F', '/TN', taskName], timeoutMs);
                    reject(error);
                }
                finally {
                    try {
                        if (fs.existsSync(tempFile))
                            fs.unlinkSync(tempFile);
                    }
                    catch { }
                }
            })();
        });
    }
    async runPowerShellWithInteractiveFallback(script, timeoutMs = 15000) {
        if (process.platform !== 'win32')
            return;
        try {
            await this.runPowerShellScript(script, timeoutMs);
        }
        catch (error) {
            logger_1.logger.warn('Primary PowerShell execution failed; using interactive task fallback', {
                error: error?.message || String(error),
            });
            await this.runPowerShellInteractiveTask(script, Math.max(timeoutMs, 18000));
        }
    }
    runPowerShellScriptViaFile(script, timeoutMs = 180000) {
        return new Promise((resolve, reject) => {
            const { execFile } = require('child_process');
            const fs = require('fs');
            const os = require('os');
            const path = require('path');
            const tempFile = path.join(os.tmpdir(), `appstats-${Date.now()}-${Math.random().toString(16).slice(2)}.ps1`);
            try {
                fs.writeFileSync(tempFile, String(script || ''), { encoding: 'utf8' });
            }
            catch (error) {
                reject(new Error(String(error?.message || error || 'Failed to prepare PowerShell script')));
                return;
            }
            execFile('powershell', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', tempFile], {
                timeout: timeoutMs,
                maxBuffer: 8 * 1024 * 1024,
                windowsHide: true,
            }, (error, stdout, stderr) => {
                try {
                    fs.unlinkSync(tempFile);
                }
                catch { }
                if (error) {
                    reject(new Error(String(stderr || error.message || stdout || '').trim() || 'Command failed'));
                    return;
                }
                resolve(String(stdout || stderr || '').trim());
            });
        });
    }
    getRemoteApprovalTimeoutSeconds() {
        const raw = Number(process.env.AGENT_REMOTE_APPROVAL_TIMEOUT_SECONDS
            || process.env.REMOTE_SESSION_APPROVAL_TIMEOUT_SECONDS
            || 60);
        if (!Number.isFinite(raw))
            return 60;
        return Math.max(10, Math.min(120, Math.floor(raw)));
    }
    isRemoteApprovalRequired() {
        const raw = String(process.env.AGENT_REMOTE_APPROVAL_REQUIRED
            || process.env.REMOTE_SESSION_APPROVAL_REQUIRED
            || '1').trim().toLowerCase();
        return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no' || raw === 'disabled');
    }
    isRemoteRdpAutoAcceptEnabled() {
        const raw = String(process.env.AGENT_REMOTE_RDP_AUTO_ACCEPT
            || process.env.REMOTE_SESSION_RDP_AUTO_ACCEPT
            || '1').trim().toLowerCase();
        return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no' || raw === 'disabled');
    }
    isRemoteSshAutoAcceptEnabled() {
        const raw = String(process.env.AGENT_REMOTE_SSH_AUTO_ACCEPT
            || process.env.REMOTE_SESSION_SSH_AUTO_ACCEPT
            || '1').trim().toLowerCase();
        return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no' || raw === 'disabled');
    }
    isRemoteRustDeskAutoAcceptEnabled() {
        const raw = String(process.env.AGENT_REMOTE_RUSTDESK_AUTO_ACCEPT
            || process.env.REMOTE_SESSION_RUSTDESK_AUTO_ACCEPT
            || '1').trim().toLowerCase();
        return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no' || raw === 'disabled');
    }
    isRemoteEmRemoteAutoAcceptEnabled() {
        const raw = String(process.env.AGENT_REMOTE_EMREMOTE_AUTO_ACCEPT
            || process.env.REMOTE_SESSION_EMREMOTE_AUTO_ACCEPT
            || '1').trim().toLowerCase();
        return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no' || raw === 'disabled');
    }
    isRemoteApprovalStrictMode() {
        const raw = String(process.env.AGENT_REMOTE_APPROVAL_STRICT
            || process.env.REMOTE_SESSION_APPROVAL_STRICT
            || '').trim().toLowerCase();
        return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
    }
    escapePowerShellLiteral(value) {
        return String(value || '').replace(/'/g, "''");
    }
    getExistingAccountLoginHint() {
        const username = String(process.env.USERNAME || process.env.USER || '').trim();
        const domain = String(process.env.USERDOMAIN || '').trim();
        if (username)
            return domain ? `${domain}\\${username}` : username;
        try {
            const info = os_1.default.userInfo();
            const fallbackUser = String(info?.username || '').trim();
            if (!fallbackUser)
                return '';
            return domain ? `${domain}\\${fallbackUser}` : fallbackUser;
        }
        catch {
            return '';
        }
    }
    async requestWindowsRemoteApproval(params) {
        const requester = this.escapePowerShellLiteral(params.requestedBy);
        const typeLabel = this.escapePowerShellLiteral(String(params.sessionType || 'ssh').toUpperCase());
        const targetLabel = this.escapePowerShellLiteral(`${params.targetHost}:${params.targetPort}`);
        const requestId = this.escapePowerShellLiteral(params.sessionId);
        const script = [
            "$ErrorActionPreference='Stop'",
            `$requester='${requester}'`,
            `$sessionType='${typeLabel}'`,
            `$target='${targetLabel}'`,
            `$requestId='${requestId}'`,
            `$title='AppStats Remote Access Request'`,
            '$message = "An administrator is requesting remote access to this device.`r`n`r`nRequester: $requester`r`nSession type: $sessionType`r`nTarget: $target`r`nRequest ID: $requestId`r`n`r`nSelect Yes to allow or No to deny."',
            '$ws = New-Object -ComObject WScript.Shell',
            `$result = $ws.Popup($message, ${params.timeoutSeconds}, $title, 4388)`,
            "if ($result -eq 6) { 'approved' } elseif ($result -eq 7) { 'denied' } else { 'timeout' }",
        ].join('\n');
        const output = String(await this.runPowerShellPromptScript(script, (params.timeoutSeconds + 20) * 1000)).trim().toLowerCase();
        if (output.includes('approved')) {
            return { approved: true, reason: 'approved' };
        }
        if (output.includes('denied')) {
            return { approved: false, reason: 'Remote session request was denied by the endpoint user.' };
        }
        return { approved: false, reason: 'Remote session request timed out waiting for endpoint approval.' };
    }
    async requestRemoteSessionApproval(params) {
        if (!this.isRemoteApprovalRequired()) {
            return { approved: true, reason: 'approval-disabled', bypassed: true };
        }
        const sessionType = String(params.sessionType || '').trim().toLowerCase();
        if (sessionType === 'rdp' && this.isRemoteRdpAutoAcceptEnabled()) {
            return { approved: true, reason: 'rdp-auto-accepted-by-agent', bypassed: true };
        }
        if (sessionType === 'ssh' && this.isRemoteSshAutoAcceptEnabled()) {
            return { approved: true, reason: 'ssh-auto-accepted-by-agent', bypassed: true };
        }
        if (sessionType === 'rustdesk') {
            if (!this.isRemoteRustDeskAutoAcceptEnabled()) {
                logger_1.logger.warn('RustDesk unattended mode forcing auto-accept despite local approval override');
            }
            return { approved: true, reason: 'rustdesk-unattended-auto-accept', bypassed: true };
        }
        if (sessionType === 'emremote' && this.isRemoteEmRemoteAutoAcceptEnabled()) {
            return { approved: true, reason: 'emremote-auto-accepted-by-agent', bypassed: true };
        }
        const timeoutSeconds = this.getRemoteApprovalTimeoutSeconds();
        if (process.platform !== 'win32') {
            return {
                approved: true,
                reason: `approval-not-supported-${process.platform}`,
                bypassed: true,
            };
        }
        try {
            const approval = await this.requestWindowsRemoteApproval({
                ...params,
                timeoutSeconds,
            });
            if (!approval.approved && !this.isRemoteApprovalStrictMode()) {
                const timeoutReason = String(approval.reason || '').toLowerCase();
                if (timeoutReason.includes('timed out') || timeoutReason.includes('timeout')) {
                    logger_1.logger.warn('Remote session approval timed out; allowing in non-strict mode', {
                        sessionId: params.sessionId,
                        requestedBy: params.requestedBy,
                    });
                    return {
                        approved: true,
                        reason: 'approval-timeout-auto-allow',
                        bypassed: true,
                    };
                }
            }
            return approval;
        }
        catch (error) {
            const message = String(error?.message || error || 'unknown prompt failure');
            if (this.isRemoteApprovalStrictMode()) {
                return {
                    approved: false,
                    reason: `Remote session approval prompt failed on endpoint: ${message}`,
                };
            }
            return {
                approved: true,
                reason: `approval-prompt-unavailable: ${message}`,
                bypassed: true,
            };
        }
    }
    parseJsonOutput(raw) {
        const text = String(raw || '').trim();
        if (!text)
            return null;
        try {
            return JSON.parse(text);
        }
        catch { }
        for (let i = text.indexOf('{'); i >= 0; i = text.indexOf('{', i + 1)) {
            for (let j = text.lastIndexOf('}'); j > i; j = text.lastIndexOf('}', j - 1)) {
                try {
                    return JSON.parse(text.slice(i, j + 1));
                }
                catch {
                    // continue scanning candidates
                }
            }
        }
        return null;
    }
    parseSpeedtestTextOutput(raw) {
        const text = String(raw || '').trim();
        if (!text)
            return null;
        const pick = (re) => {
            const m = text.match(re);
            return m?.[1] ? String(m[1]).trim() : '';
        };
        const toNum = (value) => {
            const n = Number(String(value || '').replace(/,/g, '.'));
            return Number.isFinite(n) ? n : 0;
        };
        const download = toNum(pick(/download(?:\s*speed)?\s*[:=]\s*([\d.,]+)/i));
        const upload = toNum(pick(/upload(?:\s*speed)?\s*[:=]\s*([\d.,]+)/i));
        const ping = toNum(pick(/(?:^|\s)(?:ping|latency)\s*[:=]\s*([\d.,]+)/im));
        const jitterRaw = pick(/jitter\s*[:=]\s*([\d.,]+)/i);
        const jitter = jitterRaw ? toNum(jitterRaw) : null;
        const serverFromHosted = pick(/hosted by\s+(.+?)(?:\s+\[[^\]]+\])?(?::\s*[\d.,]+\s*ms)?$/im);
        const serverFromLine = pick(/server\s*[:=]\s*(.+)$/im);
        const serverName = serverFromHosted || serverFromLine;
        const locationBits = [];
        const hostedLoc = pick(/hosted by\s+.*?\(([^)]+)\)/im);
        if (hostedLoc)
            locationBits.push(hostedLoc);
        const serverLoc = pick(/server\s*[:=]\s*.*?\(([^)]+)\)/im);
        if (serverLoc && !locationBits.includes(serverLoc))
            locationBits.push(serverLoc);
        const sponsor = serverFromHosted || '';
        const resultUrl = pick(/result\s*url\s*[:=]\s*(https?:\/\/\S+)/im);
        const shareUrl = pick(/(https?:\/\/\S+\.(?:png|jpg|jpeg))/im);
        const imageUrl = resultUrl || shareUrl || null;
        if (!(download > 0 || upload > 0 || ping > 0))
            return null;
        return {
            downloadMbps: download,
            uploadMbps: upload,
            pingMs: ping,
            jitterMs: jitter,
            serverName,
            serverLocation: locationBits.join(', '),
            sponsor,
            imageUrl,
        };
    }
    async collectSpeedtestInterfaceSamples() {
        const [networkStatsRaw, networkDetailsRaw, defaultGatewayRaw] = await Promise.all([
            si.networkStats().catch(() => []),
            si.networkInterfaces().catch(() => []),
            si.networkGatewayDefault().catch(() => ''),
        ]);
        const capturedAt = new Date().toISOString();
        const networkStats = Array.isArray(networkStatsRaw) ? networkStatsRaw : [];
        const networkDetails = Array.isArray(networkDetailsRaw) ? networkDetailsRaw : [];
        const defaultGateway = String(defaultGatewayRaw || '').trim();
        const detailByName = new Map();
        const statsByName = new Map();
        const normalizeInterfaceName = (value) => String(value || '').trim();
        const detailMatchesDefaultRoute = (detail) => (detail?.default === true || detail?.defaultRoute === true || detail?.default_gateway === true);
        const isLikelyPrivateIp = (value) => {
            const ip = String(value || '').trim();
            return !!ip && ip !== '127.0.0.1' && ip !== '::1';
        };
        for (const detail of networkDetails) {
            const key = normalizeInterfaceName(detail?.iface || detail?.ifaceName || detail?.name);
            if (key)
                detailByName.set(key, detail);
        }
        for (const stats of networkStats) {
            const key = normalizeInterfaceName(stats?.iface || stats?.ifaceName || stats?.name);
            if (key)
                statsByName.set(key, stats);
        }
        const interfaceNames = Array.from(new Set([
            ...Array.from(detailByName.keys()),
            ...Array.from(statsByName.keys()),
        ]));
        const vpnHintRegex = /\b(vpn|wireguard|wg\d*|wintun|openvpn|ovpn|tun\d*|tap\d*|utun\d*|ppp\d*|ipsec|ikev2|nordlynx|protonvpn|tailscale|zerotier|zscaler|z[-\s]?tunnel|zpa|zia|globalprotect|pan[-\s]?gp|anyconnect|forticlient|fortinet|pulse\s*secure|checkpoint|hamachi|meshnet|warp|cloudflare\s*warp|netskope|prisma)\b/i;
        const defaultDetail = networkDetails.find((detail) => detailMatchesDefaultRoute(detail))
            || networkDetails.find((detail) => !detail?.internal && isLikelyPrivateIp(detail?.ip4))
            || null;
        const defaultIfaceName = normalizeInterfaceName(defaultDetail?.iface || defaultDetail?.ifaceName || defaultDetail?.name);
        const interfaces = interfaceNames.map((ifaceName) => {
            const detail = detailByName.get(ifaceName) || {};
            const stats = statsByName.get(ifaceName) || {};
            const descriptor = [
                ifaceName,
                detail?.ifaceName,
                detail?.iface,
                detail?.type,
                detail?.vendor,
                detail?.model,
                detail?.driver,
                detail?.dnsSuffix,
            ].filter(Boolean).join(' ').toLowerCase();
            const isLoopback = /(^|[\s_-])(lo\d*|loopback)([\s_-]|$)/i.test(descriptor) || detail?.internal === true;
            const isWifi = /(wi-?fi|wlan\d*|wireless|airport|802\.11)/i.test(descriptor) || String(detail?.type || '').toLowerCase() === 'wireless';
            const isVpn = vpnHintRegex.test(descriptor);
            let type = 'ethernet';
            if (isLoopback)
                type = 'loopback';
            else if (isVpn)
                type = 'vpn';
            else if (isWifi)
                type = 'wifi';
            const speedRaw = Number(stats?.speed || detail?.speed || 0);
            return {
                interface: ifaceName,
                type,
                description: String(detail?.ifaceName || '').trim() || undefined,
                vendor: String(detail?.vendor || '').trim() || undefined,
                model: String(detail?.model || '').trim() || undefined,
                driver: String(detail?.driver || '').trim() || undefined,
                mac: String(detail?.mac || '').trim() || undefined,
                ip4: String(detail?.ip4 || '').trim() || undefined,
                ip6: String(detail?.ip6 || '').trim() || undefined,
                gateway: String(detail?.gateway || '').trim() || ((defaultGateway && ifaceName === defaultIfaceName) ? defaultGateway : undefined),
                operstate: String(stats?.operstate || detail?.operstate || '').trim() || undefined,
                speedMbps: Number.isFinite(speedRaw) ? speedRaw : 0,
                rxBytes: Number(stats?.rx_bytes || 0) || 0,
                txBytes: Number(stats?.tx_bytes || 0) || 0,
                rxPerSec: Number(stats?.rx_sec || 0) || 0,
                txPerSec: Number(stats?.tx_sec || 0) || 0,
                isDefaultRoute: detailMatchesDefaultRoute(detail) || (!!defaultGateway && ifaceName === defaultIfaceName),
                isInternal: detail?.internal === true || isLoopback,
                isVirtual: detail?.virtual === true,
            };
        });
        return { capturedAt, interfaces };
    }
    chooseSpeedtestInterfaceSnapshot(beforeSample, afterSample) {
        const beforeByName = new Map();
        for (const entry of beforeSample?.interfaces || []) {
            const key = String(entry?.interface || '').trim();
            if (key)
                beforeByName.set(key, entry);
        }
        const ranked = (afterSample.interfaces || []).map((entry) => {
            const previous = beforeByName.get(String(entry.interface || '').trim());
            const deltaRxBytes = Math.max(0, entry.rxBytes - Number(previous?.rxBytes || 0));
            const deltaTxBytes = Math.max(0, entry.txBytes - Number(previous?.txBytes || 0));
            const totalDeltaBytes = deltaRxBytes + deltaTxBytes;
            return {
                ...entry,
                capturedAt: afterSample.capturedAt,
                deltaRxBytes,
                deltaTxBytes,
                totalDeltaBytes,
            };
        });
        const sortCandidates = (items) => items.sort((a, b) => {
            const aTraffic = Number(a.totalDeltaBytes || 0);
            const bTraffic = Number(b.totalDeltaBytes || 0);
            if (bTraffic !== aTraffic)
                return bTraffic - aTraffic;
            const aLive = Number(a.rxPerSec || 0) + Number(a.txPerSec || 0);
            const bLive = Number(b.rxPerSec || 0) + Number(b.txPerSec || 0);
            if (bLive !== aLive)
                return bLive - aLive;
            if ((b.isDefaultRoute ? 1 : 0) !== (a.isDefaultRoute ? 1 : 0))
                return (b.isDefaultRoute ? 1 : 0) - (a.isDefaultRoute ? 1 : 0);
            if ((!!b.ip4 ? 1 : 0) !== (!!a.ip4 ? 1 : 0))
                return (!!b.ip4 ? 1 : 0) - (!!a.ip4 ? 1 : 0);
            return String(a.interface || '').localeCompare(String(b.interface || ''));
        });
        const preferred = sortCandidates(ranked.filter((entry) => !entry.isInternal && entry.type !== 'loopback' && entry.type !== 'vpn'));
        const fallback = sortCandidates(ranked.filter((entry) => !entry.isInternal && entry.type !== 'loopback'));
        const anyCandidate = sortCandidates(ranked.filter((entry) => !entry.isInternal));
        return preferred[0] || fallback[0] || anyCandidate[0] || null;
    }
    async captureSpeedtestInterfaceSnapshot(beforeSample) {
        try {
            const afterSample = await this.collectSpeedtestInterfaceSamples();
            return this.chooseSpeedtestInterfaceSnapshot(beforeSample, afterSample);
        }
        catch (error) {
            logger_1.logger.debug('Speedtest interface snapshot capture failed', { error: error?.message || String(error) });
            return null;
        }
    }
    getSpeedtestChecks() {
        if (process.platform !== 'win32') {
            const appStatsSpeedtest = path_1.default.join((0, app_paths_1.getAppStatsBinDir)(), 'speedtest');
            const homeDir = String(process.env.HOME || os_1.default.homedir() || '').trim();
            const userSpeedtestCli = homeDir ? path_1.default.join(homeDir, '.local', 'bin', 'speedtest-cli') : '';
            const userSpeedtest = homeDir ? path_1.default.join(homeDir, '.local', 'bin', 'speedtest') : '';
            return [
                appStatsSpeedtest ? `${(0, linux_desktop_1.quoteShellArg)(appStatsSpeedtest)} --version` : '',
                userSpeedtestCli ? `${(0, linux_desktop_1.quoteShellArg)(userSpeedtestCli)} --version` : '',
                userSpeedtest ? `${(0, linux_desktop_1.quoteShellArg)(userSpeedtest)} --version` : '',
                'speedtest-cli --version',
                'speedtest --version',
                'python3 -m speedtest --version',
                'python -m speedtest --version',
                'npx --yes speedtest-cli --version',
            ];
        }
        const runtimeNode = String(process.execPath || '').replace(/"/g, '\\"');
        const localSpeedtestCmd = path_1.default.resolve(__dirname, '../node_modules/.bin/speedtest.cmd').replace(/\//g, '\\');
        const localSpeedtestCliJs = path_1.default.resolve(__dirname, '../node_modules/speedtest-cli/lib/speedtest.js').replace(/\//g, '\\');
        const programDataSpeedtestCmd = '%ProgramData%\\AppStats\\npm\\speedtest.cmd';
        const programDataSpeedtestCliJs = '%ProgramData%\\AppStats\\npm\\node_modules\\speedtest-cli\\lib\\speedtest.js';
        return [
            'cmd /c speedtest-cli --version',
            'cmd /c speedtest --version',
            `cmd /c if exist "${localSpeedtestCmd}" ("${localSpeedtestCmd}" --help) else exit /b 1`,
            runtimeNode
                ? `cmd /c if exist "${localSpeedtestCliJs}" ("${runtimeNode}" "${localSpeedtestCliJs}" --help) else exit /b 1`
                : '',
            `cmd /c if exist "${programDataSpeedtestCmd}" ("${programDataSpeedtestCmd}" --help) else exit /b 1`,
            'cmd /c if exist "%ProgramFiles%\\Ookla\\Speedtest\\speedtest.exe" ("%ProgramFiles%\\Ookla\\Speedtest\\speedtest.exe" --version) else exit /b 1',
            'cmd /c if exist "%ProgramFiles(x86)%\\Ookla\\Speedtest\\speedtest.exe" ("%ProgramFiles(x86)%\\Ookla\\Speedtest\\speedtest.exe" --version) else exit /b 1',
            'cmd /c if exist "%ProgramData%\\AppStats\\speedtest\\speedtest.exe" ("%ProgramData%\\AppStats\\speedtest\\speedtest.exe" --version) else exit /b 1',
            runtimeNode
                ? `cmd /c if exist "${programDataSpeedtestCliJs}" ("${runtimeNode}" "${programDataSpeedtestCliJs}" --help) else exit /b 1`
                : '',
            'cmd /c npx --yes speedtest-cli --version',
            'cmd /c py -m speedtest --version',
            'cmd /c py -3 -m speedtest --version',
            'cmd /c python -m speedtest --version',
            'cmd /c python3 -m speedtest --version',
            `powershell -NoProfile -Command "$exe=(Get-ChildItem -Path $env:APPDATA\\Python -Recurse -Filter speedtest-cli.exe -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName); if($exe){ & $exe --version } else { exit 1 }"`,
        ].filter((cmd) => !!String(cmd).trim());
    }
    getSpeedtestInstallAttempts(includeWinget = false) {
        if (process.platform !== 'win32') {
            const attempts = [
                'if command -v apt-get >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get update -y && (apt-get install -y speedtest-cli || apt-get install -y speedtest); else exit 1; fi',
                'if command -v dnf >/dev/null 2>&1; then dnf install -y speedtest-cli || dnf install -y speedtest; else exit 1; fi',
                'if command -v yum >/dev/null 2>&1; then yum install -y speedtest-cli || yum install -y speedtest; else exit 1; fi',
                'if command -v zypper >/dev/null 2>&1; then zypper --non-interactive install speedtest-cli || zypper --non-interactive install speedtest; else exit 1; fi',
                'if command -v pacman >/dev/null 2>&1; then pacman -Sy --noconfirm speedtest-cli || pacman -Sy --noconfirm speedtest; else exit 1; fi',
                'if command -v apk >/dev/null 2>&1; then apk add --no-cache speedtest-cli || apk add --no-cache speedtest; else exit 1; fi',
            ];
            const pipAttempts = [
                'python3 -m pip install --user --upgrade speedtest-cli',
                'python3 -m pip install --user --upgrade --break-system-packages speedtest-cli',
                'python -m pip install --user --upgrade speedtest-cli',
                'python -m pip install --user --upgrade --break-system-packages speedtest-cli',
                'pip3 install --user --upgrade speedtest-cli',
                'pip3 install --user --upgrade --break-system-packages speedtest-cli',
                'pip install --user --upgrade speedtest-cli',
                'pip install --user --upgrade --break-system-packages speedtest-cli',
                'npm install -g speedtest-cli --silent --no-progress',
            ];
            return [
                ...attempts,
                ...pipAttempts,
            ];
        }
        // Windows: detect which package managers are available and prioritize them
        const attempts = [];
        // Try to detect available package managers and use them first
        try {
            const { execSync } = require('child_process');
            if (includeWinget) {
                attempts.push('cmd /c winget install --id Ookla.Speedtest.CLI -e --accept-source-agreements --accept-package-agreements --silent');
            }
            // Check for choco
            try {
                execSync('cmd /c where choco >nul 2>&1', { timeout: 2000 });
                attempts.push('cmd /c choco install speedtest -y');
            }
            catch (e) {
                // choco not available
            }
            // Check for python/pip
            try {
                execSync('cmd /c python -m pip --version >nul 2>&1', { timeout: 2000 });
                attempts.push('cmd /c python -m pip install --upgrade speedtest-cli');
                attempts.push('cmd /c python -m pip install --user --upgrade speedtest-cli');
            }
            catch (e) {
                // python not available
            }
            try {
                execSync('cmd /c py -m pip --version >nul 2>&1', { timeout: 2000 });
                attempts.push('cmd /c py -m pip install --upgrade speedtest-cli');
                attempts.push('cmd /c py -m pip install --user --upgrade speedtest-cli');
            }
            catch (e) {
                // py not available
            }
            // Check for npm
            try {
                execSync('cmd /c npm --version >nul 2>&1', { timeout: 2000 });
                attempts.push('cmd /c npm install -g speedtest-cli --silent --no-progress');
            }
            catch (e) {
                // npm not available
            }
        }
        catch (e) {
            // fall back to default attempts
        }
        // If no managers detected, fall back to comprehensive attempt list
        if (attempts.length === 0) {
            attempts.push('cmd /c py -m ensurepip --default-pip', 'cmd /c py -3 -m ensurepip --default-pip', 'cmd /c python -m ensurepip --default-pip', 'cmd /c python3 -m ensurepip --default-pip', 'cmd /c npm config set fund false', 'cmd /c npm config set audit false', 'cmd /c if exist "%ProgramFiles%\\nodejs\\npm.cmd" ("%ProgramFiles%\\nodejs\\npm.cmd" config set fund false) else exit /b 1', 'cmd /c if exist "%ProgramFiles%\\nodejs\\npm.cmd" ("%ProgramFiles%\\nodejs\\npm.cmd" config set audit false) else exit /b 1', 'cmd /c if exist "%ProgramFiles(x86)%\\nodejs\\npm.cmd" ("%ProgramFiles(x86)%\\nodejs\\npm.cmd" config set fund false) else exit /b 1', 'cmd /c if exist "%ProgramFiles(x86)%\\nodejs\\npm.cmd" ("%ProgramFiles(x86)%\\nodejs\\npm.cmd" config set audit false) else exit /b 1', 'cmd /c pip install --upgrade speedtest-cli', 'cmd /c pip install --user --upgrade speedtest-cli', 'cmd /c py -m pip install --upgrade speedtest-cli', 'cmd /c py -m pip install --user --upgrade speedtest-cli', 'cmd /c py -3 -m pip install --upgrade speedtest-cli', 'cmd /c py -3 -m pip install --user --upgrade speedtest-cli', 'cmd /c python -m pip install --upgrade speedtest-cli', 'cmd /c python -m pip install --user --upgrade speedtest-cli', 'cmd /c python3 -m pip install --upgrade speedtest-cli', 'cmd /c python3 -m pip install --user --upgrade speedtest-cli', 'cmd /c npm install -g speedtest-cli --silent --no-progress', 'cmd /c npm install --location=global speedtest-cli --silent --no-progress', 'cmd /c if exist "%ProgramFiles%\\nodejs\\npm.cmd" ("%ProgramFiles%\\nodejs\\npm.cmd" install -g speedtest-cli --silent --no-progress) else exit /b 1', 'cmd /c if exist "%ProgramFiles(x86)%\\nodejs\\npm.cmd" ("%ProgramFiles(x86)%\\nodejs\\npm.cmd" install -g speedtest-cli --silent --no-progress) else exit /b 1', 'cmd /c if not exist "%ProgramData%\\AppStats\\npm" mkdir "%ProgramData%\\AppStats\\npm"', 'cmd /c npm install speedtest-cli --prefix "%ProgramData%\\AppStats\\npm" --silent --no-progress', 'cmd /c if exist "%ProgramFiles%\\nodejs\\npm.cmd" ("%ProgramFiles%\\nodejs\\npm.cmd" install speedtest-cli --prefix "%ProgramData%\\AppStats\\npm" --silent --no-progress) else exit /b 1', 'cmd /c if exist "%ProgramFiles(x86)%\\nodejs\\npm.cmd" ("%ProgramFiles(x86)%\\nodejs\\npm.cmd" install speedtest-cli --prefix "%ProgramData%\\AppStats\\npm" --silent --no-progress) else exit /b 1', 'cmd /c npm install -g speedtest-cli --prefix "%ProgramData%\\AppStats\\npm" --silent --no-progress', 'cmd /c if exist "%ProgramFiles%\\nodejs\\npm.cmd" ("%ProgramFiles%\\nodejs\\npm.cmd" install -g speedtest-cli --prefix "%ProgramData%\\AppStats\\npm" --silent --no-progress) else exit /b 1', 'cmd /c if exist "%ProgramFiles(x86)%\\nodejs\\npm.cmd" ("%ProgramFiles(x86)%\\nodejs\\npm.cmd" install -g speedtest-cli --prefix "%ProgramData%\\AppStats\\npm" --silent --no-progress) else exit /b 1', 'cmd /c winget install --id Ookla.Speedtest.CLI -e --accept-source-agreements --accept-package-agreements --silent', 'cmd /c choco install speedtest -y');
        }
        return attempts;
    }
    async isSpeedtestCliAvailable(checks, timeoutPerCheckMs = 5000, maxDurationMs = 45000) {
        const deadline = Date.now() + Math.max(5000, maxDurationMs);
        for (const check of checks) {
            const remaining = deadline - Date.now();
            if (remaining <= 500)
                break;
            try {
                await this.runShellCommand(check, Math.min(timeoutPerCheckMs, remaining));
                return true;
            }
            catch {
                // continue checks
            }
        }
        return false;
    }
    async downloadSpeedtestFromServer() {
        const serverUrl = String(config_1.config.server.url || '').replace(/\/+$/, '');
        if (!serverUrl)
            return false;
        const platform = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux';
        const arch = process.arch === 'x64' ? 'x86_64' : process.arch;
        const downloadUrl = `${serverUrl}/api/software/download/speedtest-cli?platform=${platform}&arch=${arch}`;
        logger_1.logger.info('[Speedtest] Attempting download from server software store', { url: downloadUrl });
        try {
            const proto = downloadUrl.startsWith('https') ? https_1.default : http_1.default;
            const tmpDir = os_1.default.tmpdir();
            const ext = platform === 'windows' ? '.zip' : '.tgz';
            const tmpFile = path_1.default.join(tmpDir, `speedtest-cli${ext}`);
            const downloaded = await new Promise((resolve) => {
                const req = proto.get(downloadUrl, { timeout: 30000, headers: { Authorization: `Bearer ${config_1.config.server.token}` } }, (res) => {
                    if (res.statusCode !== 200) {
                        logger_1.logger.debug('[Speedtest] Server returned non-200 for speedtest download', { status: res.statusCode });
                        res.resume();
                        resolve(false);
                        return;
                    }
                    const fileStream = fs_1.default.createWriteStream(tmpFile);
                    res.pipe(fileStream);
                    fileStream.on('finish', () => { fileStream.close(); resolve(true); });
                    fileStream.on('error', () => resolve(false));
                });
                req.on('error', () => resolve(false));
                req.on('timeout', () => { req.destroy(); resolve(false); });
            });
            if (!downloaded || !fs_1.default.existsSync(tmpFile))
                return false;
            // Extract and install
            const installDir = process.platform === 'win32'
                ? path_1.default.join(process.env.ProgramData || 'C:\\ProgramData', 'AppStats', 'speedtest')
                : (0, app_paths_1.ensureAppStatsBinDir)();
            if (platform !== 'windows') {
                const quotedInstallDir = (0, linux_desktop_1.quoteShellArg)(installDir);
                const quotedTmpFile = (0, linux_desktop_1.quoteShellArg)(tmpFile);
                const quotedTarget = (0, linux_desktop_1.quoteShellArg)(path_1.default.join(installDir, 'speedtest'));
                const extractCommand = [
                    `mkdir -p ${quotedInstallDir}`,
                    `tar -xzf ${quotedTmpFile} -C ${quotedInstallDir}`,
                    `found=$(find ${quotedInstallDir} -maxdepth 3 -type f -name speedtest | head -n 1)`,
                    '[ -n "$found" ]',
                    `cp "$found" ${quotedTarget}`,
                    `chmod +x ${quotedTarget} 2>/dev/null || true`,
                ].join(' && ');
                await this.runShellCommand(extractCommand, 20000);
            }
            else {
                const mkdirCmd = `cmd /c if not exist "${installDir}" mkdir "${installDir}"`;
                await this.runShellCommand(mkdirCmd, 5000).catch(() => { });
                await this.runShellCommand(`powershell -NoProfile -Command "Expand-Archive -Force -Path '${tmpFile}' -DestinationPath '${installDir}'"`, 15000);
            }
            try {
                fs_1.default.unlinkSync(tmpFile);
            }
            catch { /* best-effort cleanup */ }
            logger_1.logger.info('[Speedtest] Installed speedtest CLI from server software store', { installDir });
            return true;
        }
        catch (e) {
            logger_1.logger.warn('[Speedtest] Server download failed', { error: e?.message || String(e) });
            return false;
        }
    }
    async ensureSpeedtestCliInstalled(forceInstall = false) {
        if (!this.tenantFeatures.speedtest) {
            logger_1.logger.debug('Skipping speedtest CLI installation because speedtest is disabled by tenant licensing');
            return false;
        }
        const checks = this.getSpeedtestChecks();
        if (await this.isSpeedtestCliAvailable(checks))
            return true;
        const now = Date.now();
        const retryIntervalMs = 2 * 60 * 1000;
        if (!forceInstall && this.speedtestInstallAttempted && (now - this.speedtestLastInstallAttemptMs) < retryIntervalMs) {
            return false;
        }
        this.speedtestInstallAttempted = true;
        this.speedtestLastInstallAttemptMs = now;
        logger_1.logger.info('Speedtest CLI not found, attempting automatic installation');
        // Try server software store first
        try {
            if (await this.downloadSpeedtestFromServer()) {
                if (await this.isSpeedtestCliAvailable(checks, 4000, 10000)) {
                    logger_1.logger.info('Speedtest CLI installed from server software store');
                    return true;
                }
            }
        }
        catch (e) {
            logger_1.logger.debug('Speedtest server download skipped', { error: e?.message || String(e) });
        }
        const wingetState = process.platform === 'win32'
            ? await this.ensureWingetInstalled()
            : { available: false };
        if (process.platform === 'win32' && wingetState.available) {
            try {
                await this.runCommandFile(String(wingetState.path || 'winget').trim() || 'winget', ['install', '--id', 'Ookla.Speedtest.CLI', '-e', '--accept-source-agreements', '--accept-package-agreements', '--silent'], 60000);
                if (await this.isSpeedtestCliAvailable(checks, 4000, 30000)) {
                    logger_1.logger.info('Speedtest CLI auto-install succeeded via WinGet');
                    return true;
                }
            }
            catch (error) {
                logger_1.logger.warn('Speedtest WinGet installer attempt failed', {
                    error: error?.message || String(error),
                    wingetPath: wingetState.path || null,
                });
            }
        }
        // Fall back to local package manager install attempts
        const installAttempts = this.getSpeedtestInstallAttempts(false);
        const installWindowMs = process.platform === 'win32' ? 150000 : 180000;
        const installDeadline = Date.now() + installWindowMs;
        for (const installCmd of installAttempts) {
            const remaining = installDeadline - Date.now();
            if (remaining <= 1500)
                break;
            try {
                await this.runShellCommand(installCmd, Math.min(process.platform === 'win32' ? 60000 : 90000, remaining));
                if (await this.isSpeedtestCliAvailable(checks, 4000, Math.min(30000, Math.max(5000, installDeadline - Date.now())))) {
                    logger_1.logger.info('Speedtest CLI auto-install succeeded');
                    return true;
                }
            }
            catch (error) {
                logger_1.logger.warn('Speedtest installer attempt failed', {
                    command: installCmd,
                    error: error?.message || String(error),
                });
            }
        }
        return await this.isSpeedtestCliAvailable(checks, 4000, 20000);
    }
    async handleSpeedtestRun(payload) {
        const requestId = String(payload?.requestId || '').trim();
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const hardTimeoutMs = process.platform === 'win32' ? 300000 : 240000;
        let speedtestInterfaceBefore = null;
        let resultSent = false;
        let hardTimeout = null;
        const sendResult = (resultPayload) => {
            if (resultSent)
                return;
            resultSent = true;
            if (hardTimeout) {
                clearTimeout(hardTimeout);
                hardTimeout = null;
            }
            this.wsClient.send({
                type: 'speedtest:result',
                payload: {
                    requestId,
                    agentId,
                    ...resultPayload,
                    timestamp: new Date().toISOString(),
                },
            });
        };
        if (!this.tenantFeatures.speedtest) {
            sendResult({
                success: false,
                error: 'Speedtest is disabled by tenant licensing.',
            });
            return;
        }
        speedtestInterfaceBefore = await this.collectSpeedtestInterfaceSamples().catch(() => null);
        hardTimeout = setTimeout(() => {
            sendResult({
                success: false,
                error: 'Speedtest execution timed out while waiting for command completion.',
            });
        }, hardTimeoutMs);
        const speedtestChecks = this.getSpeedtestChecks();
        let installAttemptedForRun = false;
        if (!(await this.isSpeedtestCliAvailable(speedtestChecks, 3000, 15000))) {
            installAttemptedForRun = true;
            await this.ensureSpeedtestCliInstalled(true);
        }
        const runtimeNode = String(process.execPath || '').replace(/"/g, '\\"');
        const localSpeedtestCmd = path_1.default.resolve(__dirname, '../node_modules/.bin/speedtest.cmd').replace(/\//g, '\\');
        const localSpeedtestCliJs = path_1.default.resolve(__dirname, '../node_modules/speedtest-cli/lib/speedtest.js').replace(/\//g, '\\');
        const programDataSpeedtestCmd = '%ProgramData%\\AppStats\\npm\\speedtest.cmd';
        const programDataSpeedtestCliJs = '%ProgramData%\\AppStats\\npm\\node_modules\\speedtest-cli\\lib\\speedtest.js';
        const appStatsSpeedtest = path_1.default.join((0, app_paths_1.getAppStatsBinDir)(), 'speedtest');
        const homeDir = String(process.env.HOME || os_1.default.homedir() || '').trim();
        const userSpeedtestCli = homeDir ? path_1.default.join(homeDir, '.local', 'bin', 'speedtest-cli') : '';
        const userSpeedtest = homeDir ? path_1.default.join(homeDir, '.local', 'bin', 'speedtest') : '';
        const attempts = process.platform === 'win32'
            ? [
                `cmd /c if exist "${localSpeedtestCmd}" ("${localSpeedtestCmd}" --json --secure) else exit /b 1`,
                runtimeNode
                    ? `cmd /c if exist "${localSpeedtestCliJs}" ("${runtimeNode}" "${localSpeedtestCliJs}" --json --secure) else exit /b 1`
                    : '',
                'cmd /c speedtest --accept-license --accept-gdpr --format=json',
                'cmd /c speedtest --accept-license --accept-gdpr -f json',
                'cmd /c if exist "%ProgramFiles%\\Ookla\\Speedtest\\speedtest.exe" ("%ProgramFiles%\\Ookla\\Speedtest\\speedtest.exe" --accept-license --accept-gdpr --format=json) else exit /b 1',
                'cmd /c if exist "%ProgramFiles%\\Ookla\\Speedtest\\speedtest.exe" ("%ProgramFiles%\\Ookla\\Speedtest\\speedtest.exe" --accept-license --accept-gdpr -f json) else exit /b 1',
                'cmd /c if exist "%ProgramFiles(x86)%\\Ookla\\Speedtest\\speedtest.exe" ("%ProgramFiles(x86)%\\Ookla\\Speedtest\\speedtest.exe" --accept-license --accept-gdpr --format=json) else exit /b 1',
                'cmd /c if exist "%ProgramFiles(x86)%\\Ookla\\Speedtest\\speedtest.exe" ("%ProgramFiles(x86)%\\Ookla\\Speedtest\\speedtest.exe" --accept-license --accept-gdpr -f json) else exit /b 1',
                'cmd /c if exist "%ProgramData%\\AppStats\\speedtest\\speedtest.exe" ("%ProgramData%\\AppStats\\speedtest\\speedtest.exe" --accept-license --accept-gdpr --format=json) else exit /b 1',
                'cmd /c if exist "%ProgramData%\\AppStats\\speedtest\\speedtest.exe" ("%ProgramData%\\AppStats\\speedtest\\speedtest.exe" --accept-license --accept-gdpr -f json) else exit /b 1',
                'cmd /c npx --yes speedtest-cli --json --secure',
                'cmd /c if exist "%AppData%\\npm\\speedtest-cli.cmd" ("%AppData%\\npm\\speedtest-cli.cmd" --json --secure) else exit /b 1',
                'cmd /c if exist "%AppData%\\npm\\speedtest.cmd" ("%AppData%\\npm\\speedtest.cmd" --json --secure) else exit /b 1',
                'cmd /c if exist "%ProgramData%\\AppStats\\npm\\speedtest-cli.cmd" ("%ProgramData%\\AppStats\\npm\\speedtest-cli.cmd" --json --secure) else exit /b 1',
                `cmd /c if exist "${programDataSpeedtestCmd}" ("${programDataSpeedtestCmd}" --json --secure) else exit /b 1`,
                `cmd /c if exist "%ProgramFiles%\\nodejs\\node.exe" if exist "${programDataSpeedtestCliJs}" ("%ProgramFiles%\\nodejs\\node.exe" "${programDataSpeedtestCliJs}" --json --secure) else exit /b 1`,
                `cmd /c if exist "%ProgramFiles(x86)%\\nodejs\\node.exe" if exist "${programDataSpeedtestCliJs}" ("%ProgramFiles(x86)%\\nodejs\\node.exe" "${programDataSpeedtestCliJs}" --json --secure) else exit /b 1`,
                runtimeNode
                    ? `cmd /c if exist "${programDataSpeedtestCliJs}" ("${runtimeNode}" "${programDataSpeedtestCliJs}" --json --secure) else exit /b 1`
                    : '',
                `cmd /c if exist "${programDataSpeedtestCliJs}" (node "${programDataSpeedtestCliJs}" --json --secure) else exit /b 1`,
                'cmd /c speedtest-cli --json --secure',
                'cmd /c speedtest-cli --secure',
                'cmd /c py -3 -m speedtest --json --secure',
                'cmd /c py -m speedtest --json --secure',
                'cmd /c python -m speedtest --json --secure',
                'cmd /c python3 -m speedtest --json --secure',
                `powershell -NoProfile -Command "$exe=(Get-ChildItem -Path $env:APPDATA\\Python -Recurse -Filter speedtest-cli.exe -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName); if($exe){ & $exe --json --secure }"`,
                'cmd /c speedtest --accept-license --accept-gdpr',
            ].filter((cmd) => !!String(cmd).trim())
            : [
                appStatsSpeedtest ? `${(0, linux_desktop_1.quoteShellArg)(appStatsSpeedtest)} --accept-license --accept-gdpr --format=json` : '',
                appStatsSpeedtest ? `${(0, linux_desktop_1.quoteShellArg)(appStatsSpeedtest)} --accept-license --accept-gdpr -f json` : '',
                userSpeedtestCli ? `${(0, linux_desktop_1.quoteShellArg)(userSpeedtestCli)} --json --secure` : '',
                userSpeedtest ? `${(0, linux_desktop_1.quoteShellArg)(userSpeedtest)} --accept-license --accept-gdpr --format=json` : '',
                userSpeedtest ? `${(0, linux_desktop_1.quoteShellArg)(userSpeedtest)} --accept-license --accept-gdpr -f json` : '',
                'npx --yes speedtest-cli --json --secure',
                'speedtest-cli --json --secure',
                'speedtest --accept-license --accept-gdpr --format=json',
                'speedtest --accept-license --accept-gdpr -f json',
                'python -m speedtest --json --secure',
                'python3 -m speedtest --json --secure',
                'speedtest --accept-license --accept-gdpr',
            ];
        let parsed = null;
        let textSummary = null;
        let usedCommand = '';
        let lastError = '';
        let rawResult = null;
        const commandTimeoutMs = process.platform === 'win32' ? 60000 : 90000;
        const runDeadlineMs = process.platform === 'win32' ? 120000 : 150000;
        const runDeadline = Date.now() + runDeadlineMs;
        const executeAttempts = async (cmds) => {
            for (const cmd of cmds) {
                const remaining = runDeadline - Date.now();
                if (remaining <= 1500) {
                    lastError = `Speedtest timed out after ${Math.round(runDeadlineMs / 1000)} seconds`;
                    break;
                }
                try {
                    const raw = await this.runShellCommand(cmd, Math.min(commandTimeoutMs, remaining));
                    const data = this.parseJsonOutput(raw);
                    if (data && typeof data === 'object') {
                        parsed = data;
                        usedCommand = cmd;
                        rawResult = data;
                        return;
                    }
                    const parsedText = this.parseSpeedtestTextOutput(raw);
                    if (parsedText) {
                        textSummary = parsedText;
                        usedCommand = cmd;
                        rawResult = { output: raw };
                        return;
                    }
                    lastError = 'Speedtest output was not valid JSON';
                }
                catch (error) {
                    lastError = error?.message || String(error);
                }
            }
        };
        await executeAttempts(attempts);
        if (!parsed && !textSummary && !installAttemptedForRun) {
            installAttemptedForRun = true;
            await this.ensureSpeedtestCliInstalled(true);
            await executeAttempts(attempts);
        }
        if (!parsed && !textSummary) {
            const timeoutFailure = /timed out/i.test(String(lastError || ''));
            const missingCli = !timeoutFailure && /(not recognized as an internal or external command|command not found|cannot find|no such file)/i.test(String(lastError || ''));
            const activeInterfaceSnapshot = await this.captureSpeedtestInterfaceSnapshot(speedtestInterfaceBefore);
            sendResult({
                success: false,
                error: timeoutFailure
                    ? 'Speedtest execution timed out while trying available CLI/install options. Please verify endpoint internet/package manager access.'
                    : (missingCli
                        ? 'No speedtest CLI found on agent after auto-install attempt. Ensure npm or Python/pip is available on the endpoint.'
                        : (lastError || 'No speedtest command available on agent')),
                activeInterfaceSnapshot,
            });
            return;
        }
        let downloadMbps = 0;
        let uploadMbps = 0;
        let pingMs = 0;
        let jitterMs = null;
        let serverName = '';
        let serverLocation = '';
        let sponsor = '';
        let imageUrl = null;
        if (parsed) {
            const isOokla = parsed.download && typeof parsed.download === 'object' && parsed.download.bandwidth != null;
            downloadMbps = isOokla
                ? (Number(parsed.download?.bandwidth || 0) * 8) / 1000000
                : Number(parsed.download || 0) / 1000000;
            uploadMbps = isOokla
                ? (Number(parsed.upload?.bandwidth || 0) * 8) / 1000000
                : Number(parsed.upload || 0) / 1000000;
            pingMs = isOokla ? Number(parsed.ping?.latency || 0) : Number(parsed.ping || 0);
            jitterMs = isOokla ? Number(parsed.ping?.jitter || 0) : null;
            serverName = String(parsed.server?.name || parsed.server?.host || '');
            const locationBits = [parsed.server?.location, parsed.server?.country].filter((v) => !!v).map((v) => String(v));
            serverLocation = locationBits.join(', ');
            sponsor = String(parsed.server?.sponsor || '');
            imageUrl = String(parsed.share || parsed.result?.url || '').trim() || null;
        }
        else if (textSummary) {
            downloadMbps = Number(textSummary.downloadMbps || 0);
            uploadMbps = Number(textSummary.uploadMbps || 0);
            pingMs = Number(textSummary.pingMs || 0);
            jitterMs = Number.isFinite(Number(textSummary.jitterMs)) ? Number(textSummary.jitterMs) : null;
            serverName = String(textSummary.serverName || '');
            serverLocation = String(textSummary.serverLocation || '');
            sponsor = String(textSummary.sponsor || '');
            imageUrl = textSummary.imageUrl ? String(textSummary.imageUrl) : null;
        }
        const activeInterfaceSnapshot = await this.captureSpeedtestInterfaceSnapshot(speedtestInterfaceBefore);
        const rawPayload = rawResult && typeof rawResult === 'object' && !Array.isArray(rawResult)
            ? { ...rawResult, activeInterfaceSnapshot }
            : rawResult;
        sendResult({
            success: true,
            command: usedCommand,
            summary: {
                downloadMbps: Number.isFinite(downloadMbps) ? Math.round(downloadMbps * 100) / 100 : 0,
                uploadMbps: Number.isFinite(uploadMbps) ? Math.round(uploadMbps * 100) / 100 : 0,
                pingMs: Number.isFinite(pingMs) ? Math.round(pingMs * 100) / 100 : 0,
                jitterMs: Number.isFinite(Number(jitterMs)) ? Math.round(Number(jitterMs) * 100) / 100 : null,
                serverName,
                serverLocation,
                sponsor,
            },
            activeInterfaceSnapshot,
            imageUrl,
            raw: rawPayload || parsed || textSummary,
        });
    }
    flushProbeResults() {
        if (!this.wsClient.isConnected()) {
            return;
        }
        let hasPending = false;
        for (const [bufKey, buf] of this.probeResultBuffers.entries()) {
            if (buf.results.length === 0)
                continue;
            hasPending = true;
            this.wsClient.send({
                type: buf.type,
                payload: { probeId: buf.probeId, results: [...buf.results] },
                agentId: this.wsClient.getAgentId() || undefined,
            });
            logger_1.logger.info(`Flushed ${buf.results.length} ${buf.type} results for probe ${buf.probeId}`);
            buf.results = [];
        }
        if (!hasPending && this.probeTimers.size === 0 && this.probeFlushTimer) {
            clearInterval(this.probeFlushTimer);
            this.probeFlushTimer = null;
        }
    }
    // ---- Security Scan Handler ----
    async handleSecurityScan(payload) {
        const { scanId, scanType, target, portRange, options } = payload || {};
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const normalizedScanType = String(scanType || 'port').toLowerCase();
        const scanTarget = normalizedScanType === 'vlan' ? (target || 'local') : (target || 'localhost');
        logger_1.logger.info(`Security scan requested: ${normalizedScanType} on ${scanTarget} (scanId: ${scanId})`);
        try {
            const result = await this.securityScanner.scan(scanId, normalizedScanType, scanTarget, portRange || '1-1024', options || {}, (progress) => {
                // Send progress updates
                this.wsClient.sendMetrics('security:scan:progress', {
                    scanId,
                    progress: Math.round(progress),
                });
            });
            result.agentId = agentId;
            // Send results back to server
            this.wsClient.send({
                type: 'security:scan:complete',
                payload: {
                    scanId,
                    agentId,
                    status: result.error ? 'error' : 'completed',
                    results: result,
                    summary: result.summary,
                    completedAt: new Date().toISOString(),
                },
            });
            logger_1.logger.info(`Security scan complete: ${scanId} - ${result.summary.openPorts} open ports, ${result.findings.length} findings`);
        }
        catch (error) {
            logger_1.logger.error(`Security scan failed: ${error.message}`);
            this.wsClient.send({
                type: 'security:scan:complete',
                payload: {
                    scanId,
                    agentId,
                    status: 'error',
                    error: error.message,
                    completedAt: new Date().toISOString(),
                },
            });
        }
    }
    // ---- XDR Handlers ----
    async handleXdrScan(payload) {
        const { scanId } = payload || {};
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        logger_1.logger.info(`XDR scan requested (scanId: ${scanId})`);
        try {
            const result = await this.xdrScanner.scan(scanId || `xdr_${Date.now()}`, (progress) => {
                this.wsClient.sendMetrics('xdr:scan:progress', { scanId, progress: Math.round(progress) });
            });
            result.agentId = agentId;
            this.wsClient.send({
                type: 'xdr:scan:complete',
                payload: {
                    scanId,
                    agentId,
                    status: result.error ? 'error' : 'completed',
                    results: result,
                    summary: result.summary,
                    completedAt: new Date().toISOString(),
                },
            });
            logger_1.logger.info(`XDR scan complete: ${scanId} - ${result.threats.length} threats found`);
        }
        catch (e) {
            logger_1.logger.error(`XDR scan failed: ${e.message}`);
            this.wsClient.send({
                type: 'xdr:scan:complete',
                payload: { scanId, agentId, status: 'error', error: e.message, completedAt: new Date().toISOString() },
            });
        }
    }
    async handleXdrBlock(payload) {
        const { threatId, pid, processName, remoteIp, remotePort } = payload || {};
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        logger_1.logger.info(`XDR block requested: pid=${pid} remoteIp=${remoteIp}:${remotePort}`);
        const results = {};
        try {
            if (pid && processName) {
                results.process = await this.xdrScanner.blockProcess(pid, processName);
            }
            if (remoteIp && remotePort) {
                results.connection = await this.xdrScanner.blockConnection(remoteIp, remotePort);
            }
            this.wsClient.send({
                type: 'xdr:block:result',
                payload: { threatId, agentId, results, success: true, timestamp: new Date().toISOString() },
            });
        }
        catch (e) {
            logger_1.logger.error(`XDR block failed: ${e.message}`);
            this.wsClient.send({
                type: 'xdr:block:result',
                payload: { threatId, agentId, results, success: false, error: e.message, timestamp: new Date().toISOString() },
            });
        }
    }
    handleXdrAllow(payload) {
        const { threatId, remoteIp, remotePort } = payload || {};
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        logger_1.logger.info(`XDR allow acknowledged for threat ${threatId}`);
        if (remoteIp && remotePort) {
            this.xdrScanner.unblockConnection(remoteIp, remotePort).catch(() => { });
        }
        this.wsClient.send({
            type: 'xdr:allow:result',
            payload: { threatId, agentId, success: true, timestamp: new Date().toISOString() },
        });
    }
    handleXdrSettings(payload) {
        const { enabled, scanIntervalMinutes } = payload || {};
        this.xdrEnabled = !!enabled;
        logger_1.logger.info(`XDR settings updated: enabled=${this.xdrEnabled}`);
        if (this.xdrScanInterval) {
            clearInterval(this.xdrScanInterval);
            this.xdrScanInterval = null;
        }
        if (this.xdrEnabled && scanIntervalMinutes && scanIntervalMinutes > 0) {
            const intervalMs = Math.max(5, scanIntervalMinutes) * 60 * 1000;
            this.xdrScanInterval = setInterval(() => {
                if (!this.wsClient.isConnected() || !this.xdrEnabled)
                    return;
                const scanId = `xdr_auto_${Date.now()}`;
                this.handleXdrScan({ scanId });
            }, intervalMs);
            logger_1.logger.info(`XDR auto-scan scheduled every ${scanIntervalMinutes} minutes`);
        }
    }
    /**
     * Check whether a Windows service is installed, its start type, and running state.
     * Returns null on non-Windows or if child_process fails.
     */
    async getWindowsServiceState(serviceName) {
        if (process.platform !== 'win32')
            return null;
        try {
            const { execSync } = require('child_process');
            const raw = execSync(`powershell -NoProfile -NonInteractive -Command "Get-Service -Name '${serviceName}' -ErrorAction SilentlyContinue | Select-Object Status,StartType | ConvertTo-Json"`, { timeout: 8000 }).toString().trim();
            if (!raw)
                return { exists: false, running: false, startType: 'Unknown' };
            const obj = JSON.parse(raw);
            return {
                exists: true,
                running: typeof obj.Status === 'string'
                    ? obj.Status === 'Running'
                    : obj.Status === 4, // ServiceControllerStatus.Running = 4
                startType: typeof obj.StartType === 'string' ? obj.StartType : String(obj.StartType),
            };
        }
        catch {
            return null;
        }
    }
    mapWindowsServiceStartTypeToScToken(startType) {
        const normalized = String(startType || '').trim().toLowerCase();
        if (normalized === 'disabled')
            return 'disabled';
        if (normalized === 'automatic' || normalized === 'automaticdelayedstart')
            return 'auto';
        return 'demand';
    }
    /**
     * TCP probe — returns true if the port is already accepting connections.
     */
    tcpProbe(host, port, timeoutMs = 5000) {
        return new Promise((resolve) => {
            const socket = new net_1.default.Socket();
            socket.setTimeout(timeoutMs);
            socket.on('connect', () => { socket.destroy(); resolve(true); });
            socket.on('timeout', () => { socket.destroy(); resolve(false); });
            socket.on('error', () => { socket.destroy(); resolve(false); });
            socket.connect(port, host);
        });
    }
    /**
     * Ensure SSH (OpenSSH) is available and listening on the local machine.
     * Only called when the TCP probe says the port is NOT yet open.
     *
     * Returns a cleanup descriptor so we know what to undo later.
     */
    async ensureSSHAvailable(wgSourceIp) {
        const { execSync } = require('child_process');
        const cleanup = {
            weStartedService: false,
            weEnabledService: false,
            weAddedFirewallRule: false,
            firewallRuleName: `AppStats-SSH-Temp-${Date.now()}`,
            originalStartType: undefined,
        };
        const svcState = await this.getWindowsServiceState('sshd');
        if (!svcState || !svcState.exists) {
            // Try to install OpenSSH Server (Windows 10/11 Optional Feature)
            logger_1.logger.info('SSH: sshd not installed — attempting to add OpenSSH.Server capability');
            execSync('powershell -NoProfile -NonInteractive -Command "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0"', { timeout: 120000 });
        }
        // Re-check after potential install
        const svcState2 = await this.getWindowsServiceState('sshd');
        if (!svcState2 || !svcState2.exists) {
            throw new Error('OpenSSH Server is not available on this machine and could not be installed automatically.');
        }
        cleanup.originalStartType = svcState2.startType;
        if (svcState2.startType === 'Disabled') {
            logger_1.logger.info('SSH: service is Disabled — temporarily enabling (Manual start)');
            execSync('sc.exe config sshd start= demand', { timeout: 8000 });
            cleanup.weEnabledService = true;
        }
        if (!svcState2.running) {
            logger_1.logger.info('SSH: starting sshd service');
            execSync('net start sshd', { timeout: 15000 });
            cleanup.weStartedService = true;
        }
        // Add a scoped inbound firewall rule: only allow traffic from the WireGuard IP
        const ruleName = cleanup.firewallRuleName;
        logger_1.logger.info(`SSH: adding scoped firewall rule '${ruleName}' (allow from ${wgSourceIp})`);
        execSync(`netsh advfirewall firewall add rule name="${ruleName}" protocol=TCP dir=in localport=22 action=allow remoteip="${wgSourceIp}"`, { timeout: 8000 });
        cleanup.weAddedFirewallRule = true;
        // Wait briefly for the service to start listening
        await new Promise(r => setTimeout(r, 2000));
        return cleanup;
    }
    /**
     * Ensure RDP (Remote Desktop) is available on the local machine.
     * Only called when the TCP probe says port 3389 is NOT yet open.
     */
    async ensureRDPAvailable(wgSourceIp) {
        const { execSync } = require('child_process');
        const sourceIp = String(wgSourceIp || '').trim() || 'any';
        const cleanup = {
            weStartedService: false,
            weEnabledService: false,
            weAddedFirewallRule: false,
            firewallRuleName: `AppStats-RDP-Temp-${Date.now()}`,
            originalStartType: undefined,
        };
        const svcState = await this.getWindowsServiceState('TermService');
        if (!svcState || !svcState.exists) {
            throw new Error('Remote Desktop Services (TermService) is not installed on this machine.');
        }
        cleanup.originalStartType = svcState.startType;
        // Ensure RDP is allowed before service startup so listener binds correctly.
        execSync('reg add "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f', { timeout: 5000 });
        if (svcState.startType === 'Disabled') {
            logger_1.logger.info('RDP: TermService is Disabled — temporarily enabling (Manual start)');
            execSync('sc.exe config TermService start= demand', { timeout: 8000 });
            cleanup.weEnabledService = true;
        }
        if (!svcState.running) {
            logger_1.logger.info('RDP: starting TermService');
            execSync('net start TermService', { timeout: 15000 });
            cleanup.weStartedService = true;
        }
        try {
            execSync('netsh advfirewall firewall set rule group="remote desktop" new enable=Yes', { timeout: 8000 });
        }
        catch { }
        // Add scoped inbound firewall rule: only allow traffic from the WireGuard source IP
        const ruleName = cleanup.firewallRuleName;
        logger_1.logger.info(`RDP: adding scoped firewall rule '${ruleName}' (allow from ${sourceIp})`);
        execSync(`netsh advfirewall firewall add rule name="${ruleName}" protocol=TCP dir=in localport=3389 action=allow remoteip="${sourceIp}"`, { timeout: 8000 });
        cleanup.weAddedFirewallRule = true;
        await new Promise(r => setTimeout(r, 2000));
        const listening = await this.tcpProbe('127.0.0.1', 3389, 3000);
        if (!listening) {
            logger_1.logger.warn('RDP: listener still closed after enable attempt');
        }
        return cleanup;
    }
    /**
     * Tear down everything we set up for a session that was NOT previously running.
     */
    async tearDownRemoteService(sessionId) {
        const state = this.remoteServiceState.get(sessionId);
        if (!state)
            return;
        this.remoteServiceState.delete(sessionId);
        const { execSync } = require('child_process');
        // Remove the scoped firewall rule we added
        if (state.weAddedFirewallRule && state.firewallRuleName) {
            try {
                logger_1.logger.info(`Remote teardown: removing firewall rule '${state.firewallRuleName}'`);
                execSync(`netsh advfirewall firewall delete rule name="${state.firewallRuleName}"`, { timeout: 8000 });
            }
            catch (e) {
                logger_1.logger.warn(`Remote teardown: failed to remove firewall rule — ${e.message}`);
            }
        }
        const svcName = state.type === 'ssh' ? 'sshd' : 'TermService';
        // Stop the service only if WE started it
        if (state.weStartedService) {
            try {
                logger_1.logger.info(`Remote teardown: stopping ${svcName} (we started it)`);
                execSync(`net stop ${svcName} /y`, { timeout: 15000 });
            }
            catch (e) {
                logger_1.logger.warn(`Remote teardown: could not stop ${svcName} — ${e.message}`);
            }
        }
        // Restore the original start type only if WE changed it
        if (state.weEnabledService && state.originalStartType) {
            try {
                const scToken = this.mapWindowsServiceStartTypeToScToken(state.originalStartType);
                logger_1.logger.info(`Remote teardown: restoring ${svcName} start type to '${scToken}'`);
                execSync(`sc.exe config ${svcName} start= ${scToken}`, { timeout: 8000 });
            }
            catch (e) {
                logger_1.logger.warn(`Remote teardown: failed to restore service start type — ${e.message}`);
            }
        }
        logger_1.logger.info(`Remote teardown complete for session ${sessionId} (type: ${state.type})`);
    }
    summarizeRustDeskError(error) {
        return String(error?.message || error || 'unknown error').trim().split('\n')[0].slice(0, 240);
    }
    isRustDeskAutoInstallEnabled() {
        if (!this.tenantFeatures.remoteAccess)
            return false;
        const raw = String(process.env.APPSTATS_RUSTDESK_AUTO_INSTALL
            || process.env.RUSTDESK_AUTO_INSTALL
            || '1').trim().toLowerCase();
        return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no' || raw === 'disabled');
    }
    async isWindowsCommandAvailable(command) {
        if (process.platform !== 'win32')
            return false;
        const cmd = String(command || '').trim();
        if (!cmd)
            return false;
        if (cmd.toLowerCase() === 'winget' || cmd.toLowerCase() === 'winget.exe') {
            const winget = await this.getWingetStatus();
            return winget.available;
        }
        try {
            await this.runShellCommand(`where ${cmd}`, 5000);
            return true;
        }
        catch {
            return false;
        }
    }
    normalizeServerHttpBaseUrl(value) {
        const raw = String(value || '').trim();
        if (!raw)
            return '';
        try {
            const parsed = new url_1.URL(raw);
            if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
                return '';
            const pathname = String(parsed.pathname || '').replace(/\/+$/, '');
            return `${parsed.protocol}//${parsed.host}${pathname}`;
        }
        catch {
            return '';
        }
    }
    normalizeServerWsBaseUrl(value) {
        const raw = String(value || '').trim();
        if (!raw)
            return '';
        try {
            const parsed = new url_1.URL(raw);
            if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:')
                return '';
            const pathname = String(parsed.pathname || '').replace(/\/+$/, '');
            return `${parsed.protocol}//${parsed.host}${pathname}`;
        }
        catch {
            return '';
        }
    }
    deriveServerHttpBaseUrlFromWs(value) {
        const normalizedWs = this.normalizeServerWsBaseUrl(value);
        if (!normalizedWs)
            return '';
        try {
            const parsed = new url_1.URL(normalizedWs);
            const protocol = parsed.protocol === 'wss:' ? 'https:' : 'http:';
            const pathname = String(parsed.pathname || '').replace(/\/+$/, '').replace(/\/ws$/i, '');
            return `${protocol}//${parsed.host}${pathname}`;
        }
        catch {
            return '';
        }
    }
    getPublicIpEndpointCandidates() {
        const serverBases = Array.from(new Set([
            this.normalizeServerHttpBaseUrl(config_1.config.server?.primaryUrl),
            this.normalizeServerHttpBaseUrl(config_1.config.server?.secondaryUrl),
            this.normalizeServerHttpBaseUrl(this.wgDeployUrl),
            this.deriveServerHttpBaseUrlFromWs(config_1.config.server?.primaryWsUrl),
            this.deriveServerHttpBaseUrlFromWs(config_1.config.server?.secondaryWsUrl),
            this.deriveServerHttpBaseUrlFromWs(this.wgDeployWsUrl),
            this.normalizeServerHttpBaseUrl(config_1.config.server?.url),
            this.deriveServerHttpBaseUrlFromWs(config_1.config.server?.wsUrl),
            this.normalizeServerHttpBaseUrl(process.env.SERVER_URL),
            this.normalizeServerHttpBaseUrl(process.env.PRIMARY_SERVER_URL),
            this.normalizeServerHttpBaseUrl(process.env.SECONDARY_SERVER_URL),
            this.deriveServerHttpBaseUrlFromWs(process.env.WS_URL),
            this.deriveServerHttpBaseUrlFromWs(process.env.PRIMARY_WS_URL),
            this.deriveServerHttpBaseUrlFromWs(process.env.SECONDARY_WS_URL),
        ].map((value) => String(value || '').trim()).filter(Boolean)));
        return serverBases.map((base) => `${base.replace(/\/+$/, '')}/api/agent/public-ip`);
    }
    attachPublicIpTelemetry(target) {
        if (!target || !this.publicIpTelemetry)
            return;
        target.publicIp = this.publicIpTelemetry.publicIp;
        target.publicIpObservedAt = this.publicIpTelemetry.observedAt;
        target.publicIpSource = this.publicIpTelemetry.source;
        if (this.publicIpTelemetry.location) {
            target.publicIpLocation = this.publicIpTelemetry.location;
        }
    }
    isTlsValidationError(error) {
        const message = String(error?.message || error || '').toLowerCase();
        return message.includes('self signed certificate')
            || message.includes('unable to verify the first certificate')
            || message.includes('certificate has expired')
            || message.includes('hostname/ip does not match certificate')
            || message.includes('altname')
            || message.includes('certificate')
            || message.includes('tls');
    }
    async requestObservedPublicIp(endpointUrl, options = {}) {
        const url = new url_1.URL(endpointUrl);
        const client = url.protocol === 'https:' ? https_1.default : http_1.default;
        const headers = {
            'user-agent': `EmtelleAnalyticsAgent/${VERSION}`,
            'x-agent-id': this.wsClient.getAgentId() || config_1.config.agent.hostname,
            'x-agent-hostname': config_1.config.agent.hostname,
        };
        const token = String(config_1.config.server?.token || '').trim();
        if (token)
            headers['x-agent-token'] = token;
        await new Promise((resolve, reject) => {
            const req = client.get(url, {
                headers,
                ...(url.protocol === 'https:' && options.relaxTls ? { rejectUnauthorized: false } : {}),
            }, (res) => {
                const status = Number(res.statusCode || 0);
                const chunks = [];
                res.on('data', (chunk) => {
                    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
                });
                res.on('end', () => {
                    if (status !== 200) {
                        reject(new Error(`Public IP endpoint returned HTTP ${status || 'unknown'}`));
                        return;
                    }
                    try {
                        const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
                        const publicIp = String(parsed?.publicIp || '').trim();
                        if (!publicIp) {
                            reject(new Error('Public IP endpoint returned no public IP'));
                            return;
                        }
                        this.publicIpTelemetry = {
                            publicIp,
                            observedAt: String(parsed?.observedAt || new Date().toISOString()),
                            source: String(parsed?.source || 'agent_http_probe').trim() || 'agent_http_probe',
                            location: parsed?.location && typeof parsed.location === 'object'
                                ? {
                                    city: parsed.location.city ?? null,
                                    region: parsed.location.region ?? null,
                                    country: parsed.location.country ?? null,
                                    latitude: Number.isFinite(Number(parsed.location.latitude)) ? Number(parsed.location.latitude) : null,
                                    longitude: Number.isFinite(Number(parsed.location.longitude)) ? Number(parsed.location.longitude) : null,
                                }
                                : null,
                        };
                        this.publicIpLastRefreshAt = Date.now();
                        resolve();
                    }
                    catch (error) {
                        reject(new Error(`Public IP endpoint returned invalid JSON: ${error?.message || String(error)}`));
                    }
                });
            });
            req.setTimeout(15000, () => {
                req.destroy(new Error('Public IP endpoint request timed out'));
            });
            req.on('error', reject);
        });
    }
    async fetchObservedPublicIpFromEndpoint(endpointUrl) {
        try {
            await this.requestObservedPublicIp(endpointUrl);
        }
        catch (error) {
            const allowTlsBypass = config_1.config.agent?.allowTlsDowngrade !== false;
            if (allowTlsBypass && String(endpointUrl || '').startsWith('https:') && this.isTlsValidationError(error)) {
                await this.requestObservedPublicIp(endpointUrl, { relaxTls: true });
                return;
            }
            throw error;
        }
    }
    async refreshPublicIpTelemetry(force = false) {
        const now = Date.now();
        if (!force && this.publicIpTelemetry && (now - this.publicIpLastRefreshAt) < Math.max(60000, Number(config_1.config.intervals.wifi || 120000))) {
            return;
        }
        if (this.publicIpRefreshInFlight) {
            await this.publicIpRefreshInFlight;
            return;
        }
        const run = (async () => {
            const endpoints = this.getPublicIpEndpointCandidates();
            let lastError = '';
            for (const endpoint of endpoints) {
                try {
                    await this.fetchObservedPublicIpFromEndpoint(endpoint);
                    return;
                }
                catch (error) {
                    lastError = error?.message || String(error);
                }
            }
            if (lastError) {
                logger_1.logger.debug(`Agent public IP refresh failed: ${lastError}`);
            }
        })();
        this.publicIpRefreshInFlight = run;
        try {
            await run;
        }
        finally {
            this.publicIpRefreshInFlight = null;
        }
    }
    getRustDeskInstallerUrlCandidates(configuredUrl) {
        const serverBases = Array.from(new Set([
            this.normalizeServerHttpBaseUrl(config_1.config.server?.primaryUrl),
            this.normalizeServerHttpBaseUrl(config_1.config.server?.url),
            this.normalizeServerHttpBaseUrl(process.env.PRIMARY_SERVER_URL),
            this.normalizeServerHttpBaseUrl(process.env.SERVER_URL),
        ].map((v) => String(v || '').trim()).filter(Boolean)));
        const serverInstallerUrls = process.platform === 'linux'
            ? serverBases.flatMap((base) => ([
                `${base.replace(/\/+$/, '')}/api/upgrades/packages/rustdesk.AppImage`,
                `${base.replace(/\/+$/, '')}/api/upgrades/packages/rustdesk-x86_64.AppImage`,
            ]))
            : serverBases.map((base) => `${base.replace(/\/+$/, '')}/api/upgrades/packages/rustdesk-install.exe`);
        const defaultUrls = process.platform === 'linux'
            ? [
                'https://github.com/rustdesk/rustdesk/releases/download/1.4.6/rustdesk-1.4.6-x86_64.AppImage',
                'https://github.com/rustdesk/rustdesk/releases/latest/download/rustdesk-x86_64.AppImage',
            ]
            : [
                'https://github.com/rustdesk/rustdesk/releases/download/1.4.6/rustdesk-1.4.6-x86_64.exe',
                'https://github.com/rustdesk/rustdesk/releases/latest/download/rustdesk-x86_64.exe',
                'https://github.com/rustdesk/rustdesk/releases/latest/download/rustdesk-x86-sciter.exe',
            ];
        return Array.from(new Set([
            configuredUrl,
            ...serverInstallerUrls,
            ...defaultUrls,
        ].map((v) => String(v || '').trim()).filter(Boolean)));
    }
    async tryInstallRustDeskViaDirectDownload() {
        if (process.platform !== 'win32')
            return;
        const configuredUrl = String(process.env.APPSTATS_RUSTDESK_INSTALLER_URL
            || process.env.RUSTDESK_INSTALLER_URL
            || '').trim();
        const agentToken = String(config_1.config.server?.token || '').trim();
        const urlCandidates = this.getRustDeskInstallerUrlCandidates(configuredUrl);
        if (urlCandidates.length === 0) {
            throw new Error('RustDesk direct download install failed: no installer URL candidates');
        }
        const installerPath = path_1.default.join(os_1.default.tmpdir(), `appstats-rustdesk-install-${Date.now()}.exe`);
        let lastDownloadError = '';
        for (const url of urlCandidates) {
            try {
                logger_1.logger.info(`RustDesk direct download attempt: ${url}`);
                const psScript = [
                    "$ErrorActionPreference='Stop'",
                    "$ProgressPreference='SilentlyContinue'",
                    `$url='${this.escapePowerShellLiteral(url)}'`,
                    `$out='${this.escapePowerShellLiteral(installerPath)}'`,
                    `$agentToken='${this.escapePowerShellLiteral(agentToken)}'`,
                    '$headers = @{}',
                    "if (-not [string]::IsNullOrWhiteSpace($agentToken) -and $url -like '*api/upgrades/packages/*') { $headers['x-agent-token'] = $agentToken }",
                    'if ($headers.Count -gt 0) {',
                    '  Invoke-WebRequest -Uri $url -Headers $headers -OutFile $out -UseBasicParsing -MaximumRedirection 8 -TimeoutSec 180',
                    '} else {',
                    '  Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing -MaximumRedirection 8 -TimeoutSec 180',
                    '}',
                    "if (!(Test-Path $out)) { throw 'RustDesk installer download failed' }",
                    '$size = (Get-Item $out).Length',
                    'if ($size -lt 500000) { throw ("RustDesk installer download too small (" + $size + " bytes)") }',
                ].join('\n');
                await this.runPowerShellScript(psScript, 180000);
                const installArgSets = [
                    ['--silent-install'],
                    ['/S'],
                    ['/VERYSILENT'],
                ];
                const installErrors = [];
                for (const args of installArgSets) {
                    try {
                        await this.runCommandFile(installerPath, args, 240000);
                        return;
                    }
                    catch (installError) {
                        const argLabel = args.length > 0 ? args.join(' ') : '(no args)';
                        installErrors.push(`${argLabel}: ${this.summarizeRustDeskError(installError)}`);
                    }
                }
                lastDownloadError = `Installer execution failed: ${installErrors.join(' | ')}`;
            }
            catch (downloadError) {
                lastDownloadError = this.summarizeRustDeskError(downloadError);
            }
            finally {
                try {
                    if (fs_1.default.existsSync(installerPath))
                        fs_1.default.unlinkSync(installerPath);
                }
                catch { }
            }
        }
        throw new Error(`RustDesk direct download install failed: ${lastDownloadError || 'download failed'}`);
    }
    async tryInstallRustDeskOnWindows() {
        if (process.platform !== 'win32')
            return;
        const attempts = [];
        const wingetState = await this.ensureWingetInstalled();
        if (wingetState.available) {
            try {
                logger_1.logger.info('RustDesk auto-install attempt via WinGet');
                await this.runCommandFile(String(wingetState.path || 'winget').trim() || 'winget', ['install', '--id', 'RustDesk.RustDesk', '--exact', '--silent', '--accept-package-agreements', '--accept-source-agreements'], 180000);
                logger_1.logger.info('RustDesk auto-install command completed via WinGet');
                return;
            }
            catch (error) {
                attempts.push(`winget (${this.summarizeRustDeskError(error)})`);
            }
        }
        if (await this.isWindowsCommandAvailable('choco')) {
            attempts.push('cmd /c choco install rustdesk -y --no-progress');
        }
        const errors = [];
        for (const cmd of attempts) {
            try {
                logger_1.logger.info(`RustDesk auto-install attempt: ${cmd}`);
                await this.runShellCommand(cmd, 180000);
                logger_1.logger.info('RustDesk auto-install command completed');
                return;
            }
            catch (error) {
                errors.push(`${cmd} (${this.summarizeRustDeskError(error)})`);
            }
        }
        try {
            await this.tryInstallRustDeskViaDirectDownload();
            logger_1.logger.info('RustDesk auto-install completed via direct download fallback');
            return;
        }
        catch (error) {
            errors.push(`direct-download (${this.summarizeRustDeskError(error)})`);
        }
        throw new Error(`RustDesk auto-install failed: ${errors.join(' | ')}`);
    }
    getRustDeskExecutionEnv(binaryPath) {
        if (process.platform !== 'linux')
            return undefined;
        const candidates = Array.from(new Set([
            String(binaryPath || '').trim(),
            (() => {
                try {
                    return fs_1.default.realpathSync(binaryPath);
                }
                catch {
                    return '';
                }
            })(),
        ].map((value) => String(value || '').trim().toLowerCase()).filter(Boolean)));
        if (candidates.some((candidate) => candidate.endsWith('.appimage') || candidate.startsWith((0, app_paths_1.getAppStatsBinDir)().toLowerCase()))) {
            const env = {
                ...process.env,
                APPIMAGE_EXTRACT_AND_RUN: String(process.env.APPIMAGE_EXTRACT_AND_RUN || '1'),
            };
            const noDesktopIntegration = String(process.env.NO_DESKTOPINTEGRATION || '').trim();
            if (noDesktopIntegration) {
                env.NO_DESKTOPINTEGRATION = noDesktopIntegration;
            }
            return env;
        }
        return undefined;
    }
    quoteDesktopExecArg(value) {
        return `"${String(value || '').replace(/(["\\`$])/g, '\\$1')}"`;
    }
    buildRustDeskDesktopExec(binaryPath) {
        const env = this.getRustDeskExecutionEnv(binaryPath);
        const envAssignments = [];
        if (env?.APPIMAGE_EXTRACT_AND_RUN) {
            envAssignments.push(`APPIMAGE_EXTRACT_AND_RUN=${String(env.APPIMAGE_EXTRACT_AND_RUN).trim()}`);
        }
        if (env?.NO_DESKTOPINTEGRATION) {
            envAssignments.push(`NO_DESKTOPINTEGRATION=${String(env.NO_DESKTOPINTEGRATION).trim()}`);
        }
        if (envAssignments.length > 0) {
            return ['/usr/bin/env', ...envAssignments, this.quoteDesktopExecArg(binaryPath)].join(' ');
        }
        return this.quoteDesktopExecArg(binaryPath);
    }
    buildRustDeskDesktopEntry(binaryPath) {
        const lines = [
            '# Managed by AppStats',
            '[Desktop Entry]',
            'Version=1.0',
            'Type=Application',
            'Name=RustDesk',
            'GenericName=Remote Desktop',
            'Comment=AppStats managed RustDesk desktop client',
            `Exec=${this.buildRustDeskDesktopExec(binaryPath)}`,
            'Terminal=false',
            'Categories=Network;RemoteAccess;',
            'StartupNotify=true',
            'StartupWMClass=RustDesk',
            'Icon=applications-internet',
        ];
        return lines.join('\n') + '\n';
    }
    async removeRustDeskLinuxAutostartEntries() {
        if (process.platform !== 'linux')
            return;
        const userHome = String(process.env.HOME || os_1.default.homedir() || '').trim();
        const autostartTargets = [
            '/etc/xdg/autostart/rustdesk.desktop',
            userHome ? path_1.default.join(userHome, '.config', 'autostart', 'rustdesk.desktop') : '',
        ].filter(Boolean);
        for (const targetPath of autostartTargets) {
            try {
                if (fs_1.default.existsSync(targetPath)) {
                    fs_1.default.unlinkSync(targetPath);
                }
            }
            catch {
                // ignore and fall back to best-effort cleanup below
            }
        }
        const shellCleanup = [
            '[ -f /etc/xdg/autostart/rustdesk.desktop ] && rm -f /etc/xdg/autostart/rustdesk.desktop || true',
            '[ -f "$HOME/.config/autostart/rustdesk.desktop" ] && rm -f "$HOME/.config/autostart/rustdesk.desktop" || true',
        ].join(' && ');
        await (0, linux_desktop_1.runLinuxDesktopCommand)(shellCleanup, 10000).catch(() => { });
    }
    async ensureRustDeskLinuxDesktopIntegration(binaryPath) {
        if (process.platform !== 'linux')
            return;
        const resolvedBinaryPath = String(binaryPath || '').trim();
        if (!resolvedBinaryPath || !fs_1.default.existsSync(resolvedBinaryPath)) {
            throw new Error('RustDesk Linux desktop integration skipped: executable not found');
        }
        let launcherBinaryPath = resolvedBinaryPath;
        const systemLinkPath = '/usr/local/bin/rustdesk';
        try {
            const targetRealPath = fs_1.default.realpathSync(resolvedBinaryPath);
            if (!fs_1.default.existsSync(systemLinkPath)) {
                fs_1.default.mkdirSync(path_1.default.dirname(systemLinkPath), { recursive: true });
                fs_1.default.symlinkSync(resolvedBinaryPath, systemLinkPath);
                launcherBinaryPath = systemLinkPath;
            }
            else {
                try {
                    const existingRealPath = fs_1.default.realpathSync(systemLinkPath);
                    if (existingRealPath === targetRealPath) {
                        launcherBinaryPath = systemLinkPath;
                    }
                }
                catch {
                    // leave the existing system binary untouched
                }
            }
        }
        catch {
            // system link is optional
        }
        const launcherEntry = this.buildRustDeskDesktopEntry(launcherBinaryPath);
        const writeFileIfChanged = (targetPath, content) => {
            const existing = fs_1.default.existsSync(targetPath) ? String(fs_1.default.readFileSync(targetPath, 'utf8') || '') : '';
            if (existing === content)
                return false;
            fs_1.default.mkdirSync(path_1.default.dirname(targetPath), { recursive: true });
            fs_1.default.writeFileSync(targetPath, content, { mode: 0o644 });
            return true;
        };
        let launcherWritten = false;
        for (const launcherPath of ['/usr/local/share/applications/rustdesk.desktop', '/usr/share/applications/rustdesk.desktop']) {
            try {
                writeFileIfChanged(launcherPath, launcherEntry);
                launcherWritten = true;
                break;
            }
            catch {
                // try the next desktop launcher path
            }
        }
        await this.removeRustDeskLinuxAutostartEntries();
        if (!launcherWritten) {
            const localDesktopCommand = [
                'mkdir -p "$HOME/.local/bin" "$HOME/.local/share/applications" "$HOME/.config/autostart"',
                `ln -sf ${(0, linux_desktop_1.quoteShellArg)(launcherBinaryPath)} "$HOME/.local/bin/rustdesk"`,
                `printf %s ${(0, linux_desktop_1.quoteShellArg)(launcherEntry)} > "$HOME/.local/share/applications/rustdesk.desktop"`,
                'chmod 644 "$HOME/.local/share/applications/rustdesk.desktop"',
                'rm -f "$HOME/.config/autostart/rustdesk.desktop"',
            ].join(' && ');
            await (0, linux_desktop_1.runLinuxDesktopCommand)(localDesktopCommand, 15000);
        }
    }
    async launchRustDeskLinuxDesktop(binaryPath) {
        if (process.platform !== 'linux')
            return;
        const rustdeskEnv = this.getRustDeskExecutionEnv(binaryPath);
        const envAssignments = [];
        if (rustdeskEnv?.APPIMAGE_EXTRACT_AND_RUN) {
            envAssignments.push(`APPIMAGE_EXTRACT_AND_RUN=${(0, linux_desktop_1.quoteShellArg)(String(rustdeskEnv.APPIMAGE_EXTRACT_AND_RUN).trim())}`);
        }
        if (rustdeskEnv?.NO_DESKTOPINTEGRATION) {
            envAssignments.push(`NO_DESKTOPINTEGRATION=${(0, linux_desktop_1.quoteShellArg)(String(rustdeskEnv.NO_DESKTOPINTEGRATION).trim())}`);
        }
        const envPrefix = envAssignments.length > 0 ? `env ${envAssignments.join(' ')} ` : '';
        const launchCommand = `[ -x ${(0, linux_desktop_1.quoteShellArg)(binaryPath)} ] && nohup ${envPrefix}${(0, linux_desktop_1.quoteShellArg)(binaryPath)} >/dev/null 2>&1 </dev/null &`;
        await (0, linux_desktop_1.runLinuxDesktopCommand)(launchCommand, 12000);
    }
    getRustDeskExecutableCandidates() {
        const appStatsBinDir = (0, app_paths_1.getAppStatsBinDir)();
        const homeDir = String(process.env.HOME || os_1.default.homedir() || '').trim();
        return process.platform === 'win32'
            ? [
                'C:\\Program Files\\RustDesk\\rustdesk.exe',
                'C:\\Program Files (x86)\\RustDesk\\rustdesk.exe',
            ]
            : process.platform === 'darwin'
                ? [
                    '/Applications/RustDesk.app/Contents/MacOS/RustDesk',
                    '/usr/local/bin/rustdesk',
                    '/opt/homebrew/bin/rustdesk',
                ]
                : [
                    path_1.default.join(appStatsBinDir, 'rustdesk'),
                    path_1.default.join(appStatsBinDir, 'rustdesk.AppImage'),
                    homeDir ? path_1.default.join(homeDir, '.local', 'bin', 'rustdesk') : '',
                    '/usr/bin/rustdesk',
                    '/usr/local/bin/rustdesk',
                    '/snap/bin/rustdesk',
                ].filter(Boolean);
    }
    async findRustDeskExecutablePath() {
        const envCandidates = [
            String(process.env.APPSTATS_RUSTDESK_PATH || '').trim(),
            String(process.env.RUSTDESK_PATH || '').trim(),
        ].filter(Boolean);
        for (const candidate of envCandidates) {
            if (fs_1.default.existsSync(candidate))
                return candidate;
        }
        for (const candidate of this.getRustDeskExecutableCandidates()) {
            if (fs_1.default.existsSync(candidate))
                return candidate;
        }
        try {
            const lookupOutput = await this.runShellCommand(process.platform === 'win32' ? 'where rustdesk' : 'command -v rustdesk', 8000);
            const resolved = String(lookupOutput || '')
                .split(/\r?\n/)
                .map((line) => line.trim())
                .find((line) => line && fs_1.default.existsSync(line));
            if (resolved)
                return resolved;
        }
        catch { }
        return null;
    }
    async bootstrapRustDeskLinuxDesktop() {
        if (process.platform !== 'linux')
            return;
        const binaryPath = await this.findRustDeskExecutablePath();
        if (!binaryPath)
            return;
        await this.ensureRustDeskLinuxDesktopIntegration(binaryPath);
    }
    async tryInstallRustDeskOnLinux() {
        if (process.platform !== 'linux')
            return;
        const configuredUrl = String(process.env.APPSTATS_RUSTDESK_INSTALLER_URL
            || process.env.RUSTDESK_INSTALLER_URL
            || '').trim();
        const agentToken = String(config_1.config.server?.token || '').trim();
        const urlCandidates = this.getRustDeskInstallerUrlCandidates(configuredUrl);
        if (urlCandidates.length === 0) {
            throw new Error('RustDesk Linux auto-install failed: no installer URL candidates');
        }
        const installDir = (0, app_paths_1.ensureAppStatsBinDir)();
        const binaryPath = path_1.default.join(installDir, 'rustdesk');
        const errors = [];
        for (const url of urlCandidates) {
            try {
                logger_1.logger.info(`RustDesk Linux auto-install attempt: ${url}`);
                await this.downloadFileWithRedirects(url, binaryPath, {
                    timeoutMs: 180000,
                    maxRedirects: 8,
                    minBytes: 500000,
                    headers: agentToken && url.includes('/api/upgrades/packages/')
                        ? { 'x-agent-token': agentToken }
                        : undefined,
                });
                fs_1.default.chmodSync(binaryPath, 0o755);
                await this.runCommandFile(binaryPath, ['--version'], 30000, {
                    env: this.getRustDeskExecutionEnv(binaryPath),
                });
                await this.ensureRustDeskLinuxDesktopIntegration(binaryPath).catch((integrationError) => {
                    logger_1.logger.warn('RustDesk Linux desktop integration refresh failed', {
                        error: this.summarizeRustDeskError(integrationError),
                    });
                });
                return;
            }
            catch (error) {
                errors.push(`${url} (${this.summarizeRustDeskError(error)})`);
                try {
                    if (fs_1.default.existsSync(binaryPath))
                        fs_1.default.unlinkSync(binaryPath);
                }
                catch { }
            }
        }
        throw new Error(`RustDesk Linux auto-install failed: ${errors.join(' | ')}`);
    }
    async resolveRustDeskExecutablePath() {
        const existingBinaryPath = await this.findRustDeskExecutablePath();
        if (existingBinaryPath)
            return existingBinaryPath;
        let autoInstallFailure = '';
        if ((process.platform === 'win32' || process.platform === 'linux') && this.isRustDeskAutoInstallEnabled()) {
            const now = Date.now();
            const minRetryMs = 2 * 60 * 1000;
            const canAttemptInstall = !this.rustdeskAutoInstallAttempted || (now - this.rustdeskLastInstallAttemptMs) >= minRetryMs;
            if (canAttemptInstall) {
                this.rustdeskAutoInstallAttempted = true;
                this.rustdeskLastInstallAttemptMs = now;
                try {
                    if (process.platform === 'win32') {
                        await this.tryInstallRustDeskOnWindows();
                    }
                    else {
                        await this.tryInstallRustDeskOnLinux();
                    }
                }
                catch (installError) {
                    autoInstallFailure = this.summarizeRustDeskError(installError);
                    logger_1.logger.warn('RustDesk auto-install attempt failed', {
                        error: autoInstallFailure,
                    });
                }
                const installedBinaryPath = await this.findRustDeskExecutablePath();
                if (installedBinaryPath)
                    return installedBinaryPath;
            }
        }
        const failureHint = autoInstallFailure ? ` Auto-install attempt failed: ${autoInstallFailure}` : '';
        throw new Error(`RustDesk executable not found. Install RustDesk on the endpoint or set APPSTATS_RUSTDESK_PATH.${failureHint}`);
    }
    async startRustDeskBackground(binaryPath) {
        const { spawn } = require('child_process');
        const rustdeskEnv = this.getRustDeskExecutionEnv(binaryPath);
        const launch = (args) => {
            try {
                const child = spawn(binaryPath, args, {
                    detached: true,
                    stdio: 'ignore',
                    windowsHide: true,
                    env: rustdeskEnv || process.env,
                });
                child.unref();
                return true;
            }
            catch {
                return false;
            }
        };
        let launched = launch(['--server']);
        if (!launched && process.platform === 'linux') {
            launched = launch(['--service']);
        }
        if (!launched && process.platform !== 'linux') {
            launched = launch([]);
        }
        if (process.platform === 'linux') {
            try {
                await this.ensureRustDeskLinuxDesktopIntegration(binaryPath);
            }
            catch (integrationError) {
                logger_1.logger.debug('RustDesk Linux desktop integration refresh skipped', {
                    error: this.summarizeRustDeskError(integrationError),
                });
            }
        }
        if (!launched) {
            logger_1.logger.warn('RustDesk background start failed', { binaryPath });
        }
    }
    async prepareRustDeskSession(binaryPath, sessionPassword) {
        const warnings = [];
        const rustdeskConfig = String(process.env.APPSTATS_RUSTDESK_CONFIG || '').trim();
        if (rustdeskConfig) {
            try {
                await this.runCommandFile(binaryPath, ['--config', rustdeskConfig], 20000, {
                    env: this.getRustDeskExecutionEnv(binaryPath),
                });
            }
            catch (error) {
                warnings.push(`Config import failed: ${this.summarizeRustDeskError(error)}`);
            }
        }
        if (process.platform === 'win32' && !this.rustdeskServicePrepared) {
            try {
                await this.runCommandFile(binaryPath, ['--install-service'], 15000);
                this.rustdeskServicePrepared = true;
            }
            catch (error) {
                warnings.push(`Service setup failed: ${this.summarizeRustDeskError(error)}`);
            }
        }
        if (sessionPassword) {
            try {
                await this.runCommandFile(binaryPath, ['--password', sessionPassword], 8000, {
                    env: this.getRustDeskExecutionEnv(binaryPath),
                });
            }
            catch (error) {
                warnings.push(`Password setup failed: ${this.summarizeRustDeskError(error)}`);
            }
        }
        await this.startRustDeskBackground(binaryPath);
        const cachedId = String(this.rustdeskCachedId || '').trim();
        if (/^[a-z0-9._-]{6,}$/i.test(cachedId)
            && (Date.now() - this.rustdeskCachedIdAt) < (24 * 60 * 60 * 1000)) {
            return { rustdeskId: cachedId, warnings };
        }
        let rustdeskId = '';
        let rustdeskIdWarning = '';
        const maxIdAttempts = 8;
        for (let attempt = 1; attempt <= maxIdAttempts; attempt++) {
            try {
                const idOutput = await this.runCommandFile(binaryPath, ['--get-id'], 5000, {
                    env: this.getRustDeskExecutionEnv(binaryPath),
                });
                rustdeskId = String(idOutput || '')
                    .split(/\r?\n/)
                    .map((line) => line.trim())
                    .find((line) => /^[a-z0-9._-]{6,}$/i.test(line)) || '';
                if (rustdeskId)
                    break;
                rustdeskIdWarning = 'RustDesk ID is empty.';
            }
            catch (error) {
                rustdeskIdWarning = `Could not query RustDesk ID: ${this.summarizeRustDeskError(error)}`;
            }
            if (attempt < maxIdAttempts) {
                await new Promise((resolve) => setTimeout(resolve, 700));
            }
        }
        if (rustdeskId) {
            this.rustdeskCachedId = rustdeskId;
            this.rustdeskCachedIdAt = Date.now();
        }
        if (!rustdeskId && rustdeskIdWarning) {
            warnings.push(rustdeskIdWarning);
        }
        return { rustdeskId, warnings };
    }
    async notifyRustDeskSessionStarted(params) {
        const requester = String(params.requestedBy || 'Administrator').replace(/[^\w .\\@-]/g, ' ').trim() || 'Administrator';
        const shortSession = String(params.sessionId || '').trim().slice(0, 8) || 'unknown';
        const notice = `AppStats notice: RustDesk session started by ${requester} (session ${shortSession}).`;
        if (process.platform === 'win32') {
            const { execFile } = require('child_process');
            try {
                await new Promise((resolve, reject) => {
                    execFile('msg', ['*', '/TIME:60', notice], { timeout: 6000, windowsHide: true, maxBuffer: 256 * 1024 }, (error) => {
                        if (error) {
                            reject(error);
                            return;
                        }
                        resolve();
                    });
                });
                return;
            }
            catch {
                // ignore and try popup fallback below
            }
            const literalNotice = this.escapePowerShellLiteral(notice);
            const popupScript = [
                "$ErrorActionPreference='Stop'",
                `$notice='${literalNotice}'`,
                '$ws = New-Object -ComObject WScript.Shell',
                '$null = $ws.Popup($notice, 20, "AppStats Remote Notice", 64)',
            ].join('\n');
            await this.runPowerShellWithInteractiveFallback(popupScript, 15000);
            return;
        }
        if (process.platform === 'linux') {
            try {
                await (0, linux_desktop_1.runLinuxDesktopCommand)(`command -v notify-send >/dev/null 2>&1 && notify-send ${(0, linux_desktop_1.quoteShellArg)('AppStats Remote Notice')} ${(0, linux_desktop_1.quoteShellArg)(notice)} -t 20000`, 8000);
                return;
            }
            catch {
                // fall back to wall for headless sessions
            }
            const shellSafeNotice = notice.replace(/"/g, '\\"');
            await this.runShellCommand(`wall "${shellSafeNotice}"`, 6000);
            return;
        }
    }
    async notifyEmRemoteSessionStarted(params) {
        const requester = String(params.requestedBy || 'Administrator').replace(/[^\w .\\@-]/g, ' ').trim() || 'Administrator';
        const shortSession = String(params.sessionId || '').trim().slice(0, 8) || 'unknown';
        const notice = `AppStats notice: EmRemote session started by ${requester} (session ${shortSession}).`;
        if (process.platform === 'win32') {
            const { execFile } = require('child_process');
            try {
                await new Promise((resolve, reject) => {
                    execFile('msg', ['*', '/TIME:60', notice], { timeout: 6000, windowsHide: true, maxBuffer: 256 * 1024 }, (error) => {
                        if (error) {
                            reject(error);
                            return;
                        }
                        resolve();
                    });
                });
                return;
            }
            catch (msgError) {
                const literalNotice = this.escapePowerShellLiteral(notice);
                const popupScript = [
                    "$ErrorActionPreference='Stop'",
                    `$notice='${literalNotice}'`,
                    '$ws = New-Object -ComObject WScript.Shell',
                    '$null = $ws.Popup($notice, 20, "AppStats Remote Notice", 64)',
                ].join('\n');
                try {
                    await this.runPowerShellWithInteractiveFallback(popupScript, 15000);
                    return;
                }
                catch (popupError) {
                    const msgFailure = String(msgError?.message || msgError || 'msg command failed');
                    const popupFailure = String(popupError?.message || popupError || 'Popup fallback failed');
                    throw new Error(`Unable to display EmRemote endpoint notification (${msgFailure}; ${popupFailure})`);
                }
            }
        }
        if (process.platform === 'linux') {
            try {
                const shellSafeNotice = notice.replace(/"/g, '\\"');
                await this.runShellCommand(`wall "${shellSafeNotice}"`, 6000);
                return;
            }
            catch (error) {
                const message = String(error?.message || error || 'wall command failed');
                throw new Error(`Unable to display EmRemote endpoint notification on Linux (${message})`);
            }
        }
        throw new Error(`EmRemote user notification is unsupported on platform '${process.platform}'`);
    }
    startEmRemoteFrameLoop(sessionId) {
        this.stopEmRemoteFrameLoop(sessionId);
        const tick = () => { void this.sendEmRemoteFrame(sessionId); };
        tick();
        const timer = setInterval(tick, 800);
        this.emRemoteFrameTimers.set(sessionId, timer);
    }
    stopEmRemoteFrameLoop(sessionId) {
        const timer = this.emRemoteFrameTimers.get(sessionId);
        if (timer) {
            clearInterval(timer);
            this.emRemoteFrameTimers.delete(sessionId);
        }
        this.emRemoteFrameInFlight.delete(sessionId);
        this.emRemoteFrameFailureLogAt.delete(sessionId);
    }
    async sendEmRemoteFrame(sessionId) {
        if (!this.wsClient.isConnected())
            return;
        if (this.emRemoteFrameInFlight.has(sessionId))
            return;
        this.emRemoteFrameInFlight.add(sessionId);
        try {
            const shot = await this.productivityCollector.captureScreenshotForRemote(null);
            if (!shot?.imageBase64) {
                const nowTs = Date.now();
                const lastWarnAt = this.emRemoteFrameFailureLogAt.get(sessionId) || 0;
                if ((nowTs - lastWarnAt) >= 15000) {
                    this.emRemoteFrameFailureLogAt.set(sessionId, nowTs);
                    const reason = String(this.productivityCollector.getLastScreenshotFailureReason() || '').trim();
                    logger_1.logger.warn('EmRemote frame capture returned no image', {
                        sessionId,
                        reason: reason || 'unknown',
                    });
                }
                return;
            }
            this.emRemoteFrameFailureLogAt.delete(sessionId);
            this.wsClient.send({
                type: 'remote:emremote:frame',
                payload: {
                    sessionId,
                    timestamp: shot.timestamp || new Date().toISOString(),
                    mimeType: shot.mimeType || 'image/jpeg',
                    imageBase64: shot.imageBase64,
                    width: shot.width || null,
                    height: shot.height || null,
                    application: shot.application || '',
                    windowTitle: shot.windowTitle || '',
                },
                agentId: this.wsClient.getAgentId() || undefined,
            });
        }
        catch (error) {
            logger_1.logger.warn('EmRemote frame capture failed', {
                sessionId,
                error: error?.message || String(error),
            });
        }
        finally {
            this.emRemoteFrameInFlight.delete(sessionId);
        }
    }
    async handleEmRemoteInput(payload) {
        const sessionId = String(payload?.sessionId || payload?.session_id || '').trim();
        if (!sessionId)
            return;
        const sessionInfo = this.activeSessions.get(sessionId);
        if (!sessionInfo || String(sessionInfo?.sessionType || '').trim().toLowerCase() !== 'emremote')
            return;
        if (process.platform !== 'win32' && process.platform !== 'linux')
            return;
        const action = String(payload?.action || '').trim().toLowerCase();
        if (action === 'refresh' || action === 'frame') {
            void this.sendEmRemoteFrame(sessionId);
            return;
        }
        if (action === 'click') {
            const nxRaw = Number(payload?.xNorm ?? payload?.x_norm ?? payload?.x);
            const nyRaw = Number(payload?.yNorm ?? payload?.y_norm ?? payload?.y);
            if (!Number.isFinite(nxRaw) || !Number.isFinite(nyRaw))
                return;
            const nx = Math.max(0, Math.min(1, nxRaw));
            const ny = Math.max(0, Math.min(1, nyRaw));
            const button = String(payload?.button || 'left').trim().toLowerCase() === 'right' ? 'right' : 'left';
            if (process.platform === 'linux') {
                const geometryOutput = await (0, linux_desktop_1.runLinuxDesktopCommand)('xdotool getdisplaygeometry', 8000);
                const [widthRaw, heightRaw] = String(geometryOutput || '').trim().split(/\s+/);
                const width = Math.max(1, Number(widthRaw) || 1);
                const height = Math.max(1, Number(heightRaw) || 1);
                const x = Math.max(0, Math.min(width - 1, Math.round((width - 1) * nx)));
                const y = Math.max(0, Math.min(height - 1, Math.round((height - 1) * ny)));
                const buttonNumber = button === 'right' ? 3 : 1;
                await (0, linux_desktop_1.runLinuxDesktopCommand)(`xdotool mousemove --sync ${x} ${y} click ${buttonNumber}`, 8000);
                void this.sendEmRemoteFrame(sessionId);
                return;
            }
            const downFlag = button === 'right' ? '0x0008' : '0x0002';
            const upFlag = button === 'right' ? '0x0010' : '0x0004';
            const script = [
                "$ErrorActionPreference='Stop'",
                'Add-Type -AssemblyName System.Windows.Forms',
                'Add-Type -AssemblyName System.Drawing',
                'Add-Type -TypeDefinition @"',
                'using System;',
                'using System.Runtime.InteropServices;',
                'public static class EmRemoteNative {',
                '  [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo);',
                '}',
                '"@',
                '$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds',
                `$nx = ${nx}; $ny = ${ny};`,
                '$x = [Math]::Max(0, [Math]::Min($b.Width - 1, [int][Math]::Round(($b.Width - 1) * $nx)))',
                '$y = [Math]::Max(0, [Math]::Min($b.Height - 1, [int][Math]::Round(($b.Height - 1) * $ny)))',
                '[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point($x, $y)',
                `[EmRemoteNative]::mouse_event(${downFlag}, 0, 0, 0, [UIntPtr]::Zero)`,
                `[EmRemoteNative]::mouse_event(${upFlag}, 0, 0, 0, [UIntPtr]::Zero)`,
            ].join('\n');
            await this.runPowerShellInteractiveTask(script, 15000);
            void this.sendEmRemoteFrame(sessionId);
            return;
        }
        if (action === 'text') {
            const text = String(payload?.text || '').slice(0, 300);
            if (!text.trim())
                return;
            if (process.platform === 'linux') {
                await (0, linux_desktop_1.runLinuxDesktopCommand)(`xdotool type --delay 1 -- ${(0, linux_desktop_1.quoteShellArg)(text)}`, 15000);
                void this.sendEmRemoteFrame(sessionId);
                return;
            }
            const literalText = this.escapePowerShellLiteral(text);
            const script = [
                "$ErrorActionPreference='Stop'",
                'Add-Type -AssemblyName System.Windows.Forms',
                `$txt = '${literalText}'`,
                '[System.Windows.Forms.Clipboard]::SetText($txt)',
                '[System.Windows.Forms.SendKeys]::SendWait("^v")',
            ].join('\n');
            await this.runPowerShellInteractiveTask(script, 15000);
            void this.sendEmRemoteFrame(sessionId);
            return;
        }
    }
    getPrivilegedShellLaunchSpec() {
        const commonEnv = {
            ...process.env,
            TERM: 'dumb',
            NO_COLOR: '1',
            CLICOLOR: '0',
            FORCE_COLOR: '0',
            APPSTATS_REMOTE_SHELL: '1',
        };
        if (process.platform === 'win32') {
            return {
                command: 'powershell.exe',
                args: ['-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', '-'],
                shellLabel: 'PowerShell',
                env: commonEnv,
            };
        }
        if (fs_1.default.existsSync('/bin/bash')) {
            return {
                command: '/bin/bash',
                args: ['--noprofile', '--norc'],
                shellLabel: 'bash',
                env: {
                    ...commonEnv,
                    HISTFILE: '',
                },
            };
        }
        return {
            command: '/bin/sh',
            args: [],
            shellLabel: 'sh',
            env: commonEnv,
        };
    }
    getPrivilegedShellIdentity() {
        if (process.platform === 'win32') {
            return String(process.env.USERNAME || process.env.USER || 'SYSTEM').trim() || 'SYSTEM';
        }
        if (typeof process.getuid === 'function' && process.getuid() === 0) {
            return 'root';
        }
        try {
            const info = os_1.default.userInfo();
            if (info?.username)
                return String(info.username).trim();
        }
        catch { }
        return String(process.env.USER || 'root').trim() || 'root';
    }
    sendRemoteShellOutput(sessionId, stream, data) {
        const safeSessionId = String(sessionId || '').trim();
        const text = String(data || '');
        if (!safeSessionId || !text)
            return;
        this.wsClient.send({
            type: 'remote:shell:data',
            payload: {
                sessionId: safeSessionId,
                stream,
                data: text,
                timestamp: new Date().toISOString(),
                agentId: this.wsClient.getAgentId() || config_1.config.agent.hostname,
            },
        });
    }
    async terminateRemoteShellSession(sessionId, force = false) {
        const safeSessionId = String(sessionId || '').trim();
        const shellState = this.remoteShellProcesses.get(safeSessionId);
        if (!shellState || shellState.closed)
            return;
        shellState.closed = true;
        const child = shellState.process;
        try {
            if (child?.stdin && !child.stdin.destroyed) {
                child.stdin.end();
            }
        }
        catch { }
        try {
            if (process.platform === 'win32') {
                child?.kill?.();
                if (force && child?.pid) {
                    const { execSync } = require('child_process');
                    execSync(`taskkill /PID ${Number(child.pid)} /T /F`, { timeout: 10000, stdio: 'ignore' });
                }
            }
            else {
                child?.kill?.(force ? 'SIGKILL' : 'SIGTERM');
            }
        }
        catch { }
        this.remoteShellProcesses.set(safeSessionId, shellState);
    }
    async startPrivilegedShellSession(sessionId, externalHost, host) {
        const safeSessionId = String(sessionId || '').trim();
        if (!safeSessionId) {
            throw new Error('Remote shell session is missing an ID');
        }
        const { spawn } = require('child_process');
        const launchSpec = this.getPrivilegedShellLaunchSpec();
        const shellProcess = spawn(launchSpec.command, launchSpec.args, {
            env: launchSpec.env,
            cwd: process.cwd(),
            windowsHide: true,
            stdio: 'pipe',
        });
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        this.remoteShellProcesses.set(safeSessionId, {
            process: shellProcess,
            closed: false,
            shellLabel: launchSpec.shellLabel,
            launchedAt: new Date().toISOString(),
        });
        this.activeSessions.set(safeSessionId, {
            sessionId: safeSessionId,
            sessionType: 'shell',
            targetHost: externalHost,
            targetPort: null,
            managedByUs: false,
            startedAt: new Date().toISOString(),
        });
        const handleChunk = (stream) => (chunk) => {
            if (!this.activeSessions.has(safeSessionId))
                return;
            const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '');
            if (!text)
                return;
            this.sendRemoteShellOutput(safeSessionId, stream, text);
        };
        shellProcess.stdout?.on('data', handleChunk('stdout'));
        shellProcess.stderr?.on('data', handleChunk('stderr'));
        shellProcess.on('error', (error) => {
            const message = String(error?.message || error || 'Unknown shell process error');
            logger_1.logger.error('Privileged shell process failed', { sessionId: safeSessionId, error: message });
            this.sendRemoteShellOutput(safeSessionId, 'meta', `[appstats] Shell process error: ${message}\n`);
            this.wsClient.send({
                type: 'remote:session:data',
                payload: {
                    sessionId: safeSessionId,
                    agentId,
                    status: 'error',
                    error: message,
                },
            });
        });
        shellProcess.on('exit', (code, signal) => {
            const shellState = this.remoteShellProcesses.get(safeSessionId);
            this.remoteShellProcesses.delete(safeSessionId);
            if (!this.activeSessions.has(safeSessionId))
                return;
            this.activeSessions.delete(safeSessionId);
            this.sendRemoteShellOutput(safeSessionId, 'meta', `[appstats] Shell session ended (${signal ? `signal ${signal}` : `code ${code ?? 0}`}).\n`);
            if (!shellState?.closed) {
                this.wsClient.send({
                    type: 'remote:session:end',
                    payload: {
                        sessionId: safeSessionId,
                        agentId,
                        status: 'ended',
                        endedAt: new Date().toISOString(),
                    },
                });
                void this.handleRemoteRelayEnd({ sessionId: safeSessionId, reason: 'remote-shell-exited' });
            }
        });
        const identity = this.getPrivilegedShellIdentity();
        this.sendRemoteShellOutput(safeSessionId, 'meta', `[appstats] Privileged shell ready on ${os_1.default.hostname()} as ${identity} using ${launchSpec.shellLabel}.\n`);
        this.wsClient.send({
            type: 'remote:session:data',
            payload: {
                sessionId: safeSessionId,
                agentId,
                status: 'active',
                message: `Privileged shell active on ${externalHost || os_1.default.hostname()}.`,
                managedByUs: false,
                connectionDetails: {
                    host: externalHost || os_1.default.hostname(),
                    serviceHost: host || 'localhost',
                    port: null,
                    type: 'shell',
                    channel: 'brokered-shell',
                    shell: launchSpec.shellLabel,
                    identity,
                    privileged: true,
                },
            },
        });
    }
    async handleRemoteShellInput(payload) {
        const sessionId = String(payload?.sessionId || payload?.session_id || '').trim();
        if (!sessionId)
            return;
        const sessionInfo = this.activeSessions.get(sessionId);
        if (!sessionInfo || String(sessionInfo?.sessionType || '').trim().toLowerCase() !== 'shell')
            return;
        const shellState = this.remoteShellProcesses.get(sessionId);
        if (!shellState || shellState.closed)
            return;
        const data = String(payload?.data ?? payload?.text ?? '').slice(0, 16384);
        if (!data)
            return;
        if (!shellState.process?.stdin || shellState.process.stdin.destroyed) {
            logger_1.logger.warn('Privileged shell stdin unavailable', { sessionId });
            return;
        }
        shellState.process.stdin.write(data);
    }
    async handleRemoteShellControl(payload) {
        const sessionId = String(payload?.sessionId || payload?.session_id || '').trim();
        if (!sessionId)
            return;
        const sessionInfo = this.activeSessions.get(sessionId);
        if (!sessionInfo || String(sessionInfo?.sessionType || '').trim().toLowerCase() !== 'shell')
            return;
        const shellState = this.remoteShellProcesses.get(sessionId);
        if (!shellState || shellState.closed)
            return;
        const action = String(payload?.action || '').trim().toLowerCase();
        if (action === 'interrupt') {
            if (shellState.process?.stdin && !shellState.process.stdin.destroyed) {
                shellState.process.stdin.write('\u0003');
            }
            return;
        }
        if (action === 'eof') {
            if (shellState.process?.stdin && !shellState.process.stdin.destroyed) {
                shellState.process.stdin.end();
            }
            return;
        }
        if (action === 'terminate' || action === 'end') {
            await this.handleRemoteSessionEnd({ sessionId });
        }
    }
    async handleRemoteSessionStart(payload) {
        const { sessionId, sessionType, targetHost, serviceHost, targetPort, wgSourceIp } = payload || {};
        const requestedBy = String(payload?.requestedBy || payload?.requested_by || 'Administrator').trim() || 'Administrator';
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        if (!this.tenantFeatures.remoteAccess) {
            this.wsClient.send({
                type: 'remote:session:data',
                payload: {
                    sessionId,
                    agentId,
                    status: 'error',
                    error: 'Remote access is disabled by tenant licensing.',
                    message: 'Remote access is disabled by tenant licensing.',
                },
            });
            return;
        }
        // targetHost is the externally reachable host (prefer WireGuard IP).
        // serviceHost is what the agent should probe/manage locally.
        const externalHost = targetHost || 'localhost';
        const host = serviceHost || targetHost || 'localhost';
        const normalizedSessionType = String(sessionType || '').toLowerCase();
        const isSSH = (sessionType || '').toLowerCase() === 'ssh';
        const isRDP = (sessionType || '').toLowerCase() === 'rdp';
        const isEmRemote = normalizedSessionType === 'emremote';
        const isRustDesk = normalizedSessionType === 'rustdesk';
        const isShell = normalizedSessionType === 'shell';
        const defaultPort = isSSH ? 22 : isRDP ? 3389 : (isEmRemote || isRustDesk || isShell) ? 0 : (targetPort || 22);
        const port = targetPort || defaultPort;
        // The WireGuard peer IP to scope firewall rules to (fall back to any if not supplied)
        const sourceIp = wgSourceIp || '10.0.0.0/8';
        logger_1.logger.info(`Remote session start: ${sessionType} -> service ${host}:${port} (access ${externalHost}:${port}) (session: ${sessionId})`);
        try {
            const approval = await this.requestRemoteSessionApproval({
                sessionId: String(sessionId || ''),
                sessionType: String(sessionType || ''),
                targetHost: String(externalHost || 'localhost'),
                targetPort: Number(port) || defaultPort,
                requestedBy,
            });
            if (!approval.approved) {
                throw new Error(approval.reason || 'Remote session request denied by endpoint user.');
            }
            if (approval.bypassed) {
                logger_1.logger.warn('Remote session approval bypassed', {
                    sessionId,
                    reason: approval.reason,
                    platform: process.platform,
                });
            }
            else {
                logger_1.logger.info(`Remote session ${sessionId}: approved by endpoint user`);
            }
            if (isEmRemote) {
                if (process.platform !== 'win32' && process.platform !== 'linux') {
                    throw new Error(`EmRemote is unsupported on platform '${process.platform}'.`);
                }
                void this.notifyEmRemoteSessionStarted({
                    sessionId: String(sessionId || ''),
                    requestedBy,
                }).catch((notifyError) => {
                    logger_1.logger.warn('EmRemote endpoint notification failed; continuing without popup notice', {
                        sessionId,
                        error: String(notifyError?.message || notifyError || 'unknown error'),
                    });
                });
                this.activeSessions.set(sessionId, {
                    sessionId,
                    sessionType: 'emremote',
                    targetHost: externalHost,
                    targetPort: null,
                    managedByUs: false,
                    startedAt: new Date().toISOString(),
                });
                this.startEmRemoteFrameLoop(String(sessionId || ''));
                this.wsClient.send({
                    type: 'remote:session:data',
                    payload: {
                        sessionId,
                        agentId,
                        status: 'active',
                        message: 'EmRemote session active (screen stream + input control). Endpoint user has been notified.',
                        managedByUs: false,
                        connectionDetails: {
                            host: externalHost,
                            serviceHost: host,
                            port: null,
                            type: 'emremote',
                            channel: 'emremote',
                        },
                    },
                });
                logger_1.logger.info(`Remote session ${sessionId} active (EmRemote)`);
                return;
            }
            if (isRustDesk) {
                const requestedRustDeskPassword = String(payload?.rustdeskPassword || payload?.rustdesk_password || '').trim();
                const rustdeskPassword = requestedRustDeskPassword || crypto_1.default.randomBytes(6).toString('hex').slice(0, 12);
                void this.notifyRustDeskSessionStarted({
                    sessionId: String(sessionId || ''),
                    requestedBy,
                }).catch((notifyError) => {
                    logger_1.logger.warn('RustDesk endpoint notification failed; continuing without popup notice', {
                        sessionId,
                        error: String(notifyError?.message || notifyError || 'unknown error'),
                    });
                });
                const rustdeskBinary = await this.resolveRustDeskExecutablePath();
                const rustdeskPrepared = await this.prepareRustDeskSession(rustdeskBinary, rustdeskPassword);
                const rustdeskId = String(rustdeskPrepared.rustdeskId || '').trim();
                if (!rustdeskId) {
                    const warningHint = rustdeskPrepared.warnings.length
                        ? rustdeskPrepared.warnings[0]
                        : 'RustDesk did not return an endpoint ID.';
                    throw new Error(`RustDesk ID unavailable: ${warningHint}`);
                }
                this.activeSessions.set(sessionId, {
                    sessionId,
                    sessionType: 'rustdesk',
                    targetHost: rustdeskId,
                    targetPort: null,
                    managedByUs: false,
                    startedAt: new Date().toISOString(),
                });
                this.wsClient.send({
                    type: 'remote:session:data',
                    payload: {
                        sessionId,
                        agentId,
                        status: 'active',
                        message: rustdeskPrepared.warnings.length
                            ? `RustDesk session active with warnings: ${rustdeskPrepared.warnings[0]}`
                            : `RustDesk session active. Connect with RustDesk ID ${rustdeskId}.`,
                        managedByUs: false,
                        connectionDetails: {
                            host: rustdeskId,
                            serviceHost: rustdeskBinary,
                            port: null,
                            type: 'rustdesk',
                            channel: 'rustdesk',
                            rustdeskId,
                            rustdeskPassword,
                        },
                    },
                });
                logger_1.logger.info(`Remote session ${sessionId} active (RustDesk)`, {
                    rustdeskId,
                    warnings: rustdeskPrepared.warnings.length,
                });
                return;
            }
            if (isShell) {
                await this.startPrivilegedShellSession(String(sessionId || ''), String(externalHost || os_1.default.hostname()), String(host || 'localhost'));
                logger_1.logger.info(`Remote session ${sessionId} active (privileged shell)`);
                return;
            }
            // ── Step 1: probe whether the service is already listening ──────────────
            const alreadyListening = await this.tcpProbe(host, port, 3000);
            if (alreadyListening) {
                // Service is already up and running — do NOT touch anything.
                logger_1.logger.info(`Remote session ${sessionId}: ${sessionType.toUpperCase()} already listening on ${host}:${port} — no service changes made`);
                this.activeSessions.set(sessionId, {
                    sessionId, sessionType, targetHost: externalHost, targetPort: port,
                    managedByUs: false,
                    startedAt: new Date().toISOString(),
                });
            }
            else {
                // ── Step 2: service not listening — enable it securely ────────────────
                if (process.platform !== 'win32') {
                    // Linux: check and enable via systemctl
                    const { execSync } = require('child_process');
                    const svcName = isRDP ? 'xrdp' : 'ssh';
                    try {
                        const status = execSync(`systemctl is-active ${svcName} 2>/dev/null`, { timeout: 5000 }).toString().trim();
                        if (status !== 'active') {
                            logger_1.logger.info(`Remote session ${sessionId}: starting ${svcName} on Linux`);
                            execSync(`systemctl start ${svcName}`, { timeout: 15000 });
                            this.remoteServiceState.set(sessionId, {
                                type: isRDP ? 'rdp' : 'ssh',
                                weStartedService: true,
                                weEnabledService: false,
                                weAddedFirewallRule: false,
                                firewallRuleName: '',
                            });
                        }
                    }
                    catch {
                        throw new Error(`Could not start ${svcName} on Linux — please ensure it is installed.`);
                    }
                    await new Promise(r => setTimeout(r, 2000));
                }
                else {
                    // Windows: full service + firewall management
                    logger_1.logger.info(`Remote session ${sessionId}: ${sessionType.toUpperCase()} not listening — enabling securely`);
                    let svcCleanup;
                    if (isSSH) {
                        svcCleanup = await this.ensureSSHAvailable(sourceIp);
                    }
                    else if (isRDP) {
                        svcCleanup = await this.ensureRDPAvailable(sourceIp);
                    }
                    else {
                        throw new Error(`Unsupported session type '${sessionType}' — cannot auto-enable service`);
                    }
                    this.remoteServiceState.set(sessionId, { type: isSSH ? 'ssh' : 'rdp', ...svcCleanup });
                }
                // Re-probe to confirm the service is now up
                const nowListening = await this.tcpProbe(host, port, 8000);
                if (!nowListening) {
                    // Clean up what we did, then report failure
                    await this.tearDownRemoteService(sessionId);
                    throw new Error(`${sessionType.toUpperCase()} service was started but ${host}:${port} is still not reachable`);
                }
                logger_1.logger.info(`Remote session ${sessionId}: service enabled and confirmed listening on ${host}:${port}`);
                this.activeSessions.set(sessionId, {
                    sessionId, sessionType, targetHost: externalHost, targetPort: port,
                    managedByUs: true,
                    startedAt: new Date().toISOString(),
                });
            }
            // ── Step 3: Report session active ────────────────────────────────────────
            const sessionState = this.activeSessions.get(sessionId);
            const existingAccountHint = isRDP ? this.getExistingAccountLoginHint() : '';
            const readinessMessage = `${sessionType.toUpperCase()} session ready on ${externalHost}:${port}`
                + (isRDP ? ` (use existing account${existingAccountHint ? `: ${existingAccountHint}` : ''})` : '');
            this.wsClient.send({
                type: 'remote:session:data',
                payload: {
                    sessionId,
                    agentId,
                    status: 'active',
                    message: readinessMessage,
                    managedByUs: sessionState.managedByUs,
                    connectionDetails: {
                        host: externalHost,
                        serviceHost: host,
                        port,
                        type: sessionType,
                        loginHint: existingAccountHint || undefined,
                        existingAccountOnly: isRDP ? true : undefined,
                    },
                },
            });
            logger_1.logger.info(`Remote session ${sessionId} active`);
        }
        catch (error) {
            logger_1.logger.error(`Remote session error: ${error.message}`);
            this.wsClient.send({
                type: 'remote:session:data',
                payload: {
                    sessionId,
                    agentId,
                    status: 'error',
                    error: error.message,
                },
            });
        }
    }
    async handleRemoteSessionEnd(payload) {
        const { sessionId } = payload || {};
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const sessionInfo = this.activeSessions.get(sessionId);
        logger_1.logger.info(`Remote session end: ${sessionId} (managedByUs: ${sessionInfo?.managedByUs ?? false})`);
        if (String(sessionInfo?.sessionType || '').trim().toLowerCase() === 'shell') {
            await this.terminateRemoteShellSession(String(sessionId || ''), true);
        }
        this.activeSessions.delete(sessionId);
        this.stopEmRemoteFrameLoop(String(sessionId || ''));
        // If we enabled the service for this session, tear it down
        if (sessionInfo?.managedByUs) {
            await this.tearDownRemoteService(sessionId);
        }
        this.wsClient.send({
            type: 'remote:session:end',
            payload: {
                sessionId,
                agentId,
                status: 'ended',
                endedAt: new Date().toISOString(),
            },
        });
        await this.handleRemoteRelayEnd({ sessionId, reason: 'remote-session-ended' });
    }
    closeRelaySocket(sessionId) {
        const socket = this.relaySockets.get(sessionId);
        if (!socket)
            return;
        this.relaySockets.delete(sessionId);
        try {
            socket.destroy();
        }
        catch { }
    }
    async handleRemoteRelayStart(payload) {
        const sessionId = String(payload?.sessionId || payload?.session_id || '').trim();
        const relayToken = String(payload?.relayToken || payload?.relay_token || '').trim();
        const targetHost = String(payload?.targetHost || payload?.serviceHost || 'localhost').trim() || 'localhost';
        const targetPortRaw = Number(payload?.targetPort || payload?.port || 0);
        const targetPort = Number.isFinite(targetPortRaw) && targetPortRaw > 0 ? Math.floor(targetPortRaw) : 0;
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        if (!sessionId || !targetPort) {
            logger_1.logger.warn('Remote relay start ignored due to missing session/port', { sessionId, targetPort });
            this.wsClient.send({
                type: 'remote:relay:end',
                payload: {
                    sessionId,
                    relayToken,
                    agentId,
                    reason: 'Invalid relay start payload',
                },
            });
            return;
        }
        this.relayCloseSuppressed.delete(sessionId);
        this.closeRelaySocket(sessionId);
        const socket = net_1.default.createConnection({ host: targetHost, port: targetPort });
        socket.setNoDelay(true);
        socket.setKeepAlive(true, 30000);
        socket.on('connect', () => {
            this.relaySockets.set(sessionId, socket);
            this.wsClient.send({
                type: 'remote:relay:ready',
                payload: {
                    sessionId,
                    relayToken,
                    agentId,
                    status: 'ready',
                    targetHost,
                    targetPort,
                },
            });
        });
        socket.on('data', (chunk) => {
            this.wsClient.send({
                type: 'remote:relay:data',
                payload: {
                    sessionId,
                    relayToken,
                    agentId,
                    direction: 'to-operator',
                    data: Buffer.from(chunk).toString('base64'),
                },
            });
        });
        socket.on('close', () => {
            const suppressCloseNotice = this.relayCloseSuppressed.delete(sessionId);
            this.closeRelaySocket(sessionId);
            if (suppressCloseNotice)
                return;
            this.wsClient.send({
                type: 'remote:relay:end',
                payload: {
                    sessionId,
                    relayToken,
                    agentId,
                    reason: 'target-connection-closed',
                },
            });
        });
        socket.on('error', (error) => {
            logger_1.logger.warn('Remote relay target socket error', {
                sessionId,
                targetHost,
                targetPort,
                error: error?.message || String(error),
            });
            this.wsClient.send({
                type: 'remote:relay:end',
                payload: {
                    sessionId,
                    relayToken,
                    agentId,
                    reason: error?.message || 'target-connection-error',
                },
            });
            this.closeRelaySocket(sessionId);
        });
    }
    async handleRemoteRelayData(payload) {
        const sessionId = String(payload?.sessionId || payload?.session_id || '').trim();
        if (!sessionId)
            return;
        const socket = this.relaySockets.get(sessionId);
        if (!socket || socket.destroyed || !socket.writable)
            return;
        const data = String(payload?.data || '').trim();
        if (!data)
            return;
        try {
            socket.write(Buffer.from(data, 'base64'));
        }
        catch (error) {
            logger_1.logger.warn('Remote relay write failed', { sessionId, error: error?.message || String(error) });
            this.closeRelaySocket(sessionId);
            this.wsClient.send({
                type: 'remote:relay:end',
                payload: {
                    sessionId,
                    relayToken: String(payload?.relayToken || payload?.relay_token || '').trim(),
                    agentId: this.wsClient.getAgentId() || config_1.config.agent.hostname,
                    reason: error?.message || 'relay-write-failed',
                },
            });
        }
    }
    async handleRemoteRelayEnd(payload) {
        const sessionId = String(payload?.sessionId || payload?.session_id || '').trim();
        if (!sessionId)
            return;
        if (!this.relaySockets.has(sessionId)) {
            this.relayCloseSuppressed.delete(sessionId);
            return;
        }
        this.relayCloseSuppressed.add(sessionId);
        this.closeRelaySocket(sessionId);
    }
    async installBrowserExtension(payload) {
        if (!this.tenantFeatures.browserMonitoring) {
            logger_1.logger.warn('Browser extension deployment skipped because browser monitoring is disabled by tenant licensing');
            return;
        }
        const { serverUrl } = payload || {};
        const queueId = Number(payload?.queueId || payload?.queue_id || 0);
        const requestedBrowser = String(payload?.browser || 'edge').toLowerCase();
        const requestedMode = String(payload?.mode || 'install').toLowerCase();
        const operationMode = requestedMode === 'update' ? 'update' : 'install';
        const browserAliases = {
            msedge: 'edge',
            chromium: 'chrome',
            opera_gx: 'operagx',
            'opera-gx': 'operagx',
            'opera gx': 'operagx',
            gx: 'operagx',
        };
        const normalizedBrowser = browserAliases[requestedBrowser] || requestedBrowser;
        const supportedBrowsers = ['edge', 'chrome', 'brave', 'opera', 'operagx', 'firefox'];
        const effectiveRequestedBrowser = normalizedBrowser === 'all'
            ? 'all'
            : (supportedBrowsers.includes(normalizedBrowser) ? normalizedBrowser : 'edge');
        const joinWithBase = (base, ...segments) => {
            const normalizedBase = String(base || '').trim();
            return normalizedBase ? path_1.default.join(normalizedBase, ...segments) : '';
        };
        const requestedTargets = normalizedBrowser === 'all'
            ? supportedBrowsers
            : (supportedBrowsers.includes(normalizedBrowser) ? [normalizedBrowser] : ['edge']);
        const dedupeNonEmpty = (values) => (Array.from(new Set(values.map((value) => String(value || '').trim()).filter(Boolean))));
        const getLinuxHomeDirsForDetection = () => {
            if (process.platform !== 'linux')
                return [];
            const homes = new Set();
            const addHome = (value) => {
                const candidate = String(value || '').trim();
                if (!candidate || !path_1.default.isAbsolute(candidate) || !fs_1.default.existsSync(candidate))
                    return;
                homes.add(candidate);
            };
            addHome(process.env.HOME);
            addHome(os_1.default.homedir());
            try {
                const passwdRaw = String(fs_1.default.readFileSync('/etc/passwd', 'utf8') || '');
                for (const line of passwdRaw.split(/\r?\n/)) {
                    if (!line || line.startsWith('#'))
                        continue;
                    const parts = line.split(':');
                    const homeDir = String(parts[5] || '').trim();
                    const shell = String(parts[6] || '').trim().toLowerCase();
                    if (!homeDir)
                        continue;
                    if (shell.endsWith('/false') || shell.endsWith('/nologin'))
                        continue;
                    addHome(homeDir);
                }
            }
            catch {
                // fall back to discovered directories only
            }
            const homeRoot = '/home';
            if (fs_1.default.existsSync(homeRoot)) {
                try {
                    for (const entry of fs_1.default.readdirSync(homeRoot, { withFileTypes: true })) {
                        if (entry.isDirectory())
                            addHome(path_1.default.join(homeRoot, entry.name));
                    }
                }
                catch {
                    // ignore enumeration errors
                }
            }
            return Array.from(homes);
        };
        const normalizeExecutableCandidate = (value, executableName = '') => {
            const raw = String(value || '').trim().replace(/^"+|"+$/g, '');
            if (!raw)
                return '';
            const commandMatch = raw.match(/([a-zA-Z]:\\[^"]*?\.exe)/);
            let candidate = commandMatch ? commandMatch[1] : raw;
            if (candidate && !/\.exe$/i.test(candidate) && executableName) {
                candidate = path_1.default.join(candidate, executableName);
            }
            candidate = String(candidate || '').trim().replace(/^"+|"+$/g, '');
            return /\.exe$/i.test(candidate) ? candidate : '';
        };
        const collectRegistryAppPathCandidates = (executableNames) => {
            if (process.platform !== 'win32')
                return [];
            const roots = [
                'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths',
                'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths',
                'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths',
            ];
            const collected = [];
            const appPathsCache = new Map();
            for (const rawExecutableName of executableNames) {
                const executableName = String(rawExecutableName || '').trim();
                if (!executableName)
                    continue;
                const cacheKey = executableName.toLowerCase();
                const cached = appPathsCache.get(cacheKey);
                if (cached) {
                    collected.push(...cached);
                    continue;
                }
                const resolvedForExecutable = [];
                for (const root of roots) {
                    const key = `${root}\\${executableName}`;
                    for (const valueArg of ['/ve', '/v "Path"']) {
                        try {
                            const { execSync } = require('child_process');
                            const raw = String(execSync(`reg query "${key}" ${valueArg}`, { timeout: 6000, windowsHide: true }) || '');
                            for (const line of raw.split(/\r?\n/)) {
                                const valueMatch = line.match(/\bREG_\w+\b\s+(.+)$/i);
                                if (!valueMatch)
                                    continue;
                                const normalized = normalizeExecutableCandidate(valueMatch[1], executableName);
                                if (!normalized)
                                    continue;
                                resolvedForExecutable.push(normalized);
                            }
                        }
                        catch {
                            // Ignore missing keys/value names for this root.
                        }
                    }
                }
                const deduped = dedupeNonEmpty(resolvedForExecutable);
                appPathsCache.set(cacheKey, deduped);
                collected.push(...deduped);
            }
            return dedupeNonEmpty(collected);
        };
        const getWindowsUserProfilesForDetection = () => {
            if (process.platform !== 'win32')
                return [];
            const usersRoot = 'C:\\Users';
            if (!fs_1.default.existsSync(usersRoot))
                return [];
            try {
                const ignored = new Set([
                    'all users',
                    'default',
                    'default user',
                    'public',
                    'defaultapppool',
                    'wdagutilityaccount',
                ]);
                return fs_1.default.readdirSync(usersRoot, { withFileTypes: true })
                    .filter((entry) => entry.isDirectory())
                    .map((entry) => entry.name)
                    .filter((name) => !ignored.has(name.toLowerCase()))
                    .map((name) => path_1.default.join(usersRoot, name));
            }
            catch {
                return [];
            }
        };
        const windowsUserProfilesForDetection = getWindowsUserProfilesForDetection();
        const linuxHomeDirsForDetection = getLinuxHomeDirsForDetection();
        const localAppDataRootsForDetection = dedupeNonEmpty([
            String(process.env.LOCALAPPDATA || '').trim(),
            ...windowsUserProfilesForDetection.map((userDir) => path_1.default.join(userDir, 'AppData', 'Local')),
        ]);
        const roamingAppDataRootsForDetection = dedupeNonEmpty([
            String(process.env.APPDATA || '').trim(),
            ...windowsUserProfilesForDetection.map((userDir) => path_1.default.join(userDir, 'AppData', 'Roaming')),
        ]);
        const browserRegistryExecutableCandidates = {
            edge: collectRegistryAppPathCandidates(['msedge.exe']),
            chrome: collectRegistryAppPathCandidates(['chrome.exe']),
            brave: collectRegistryAppPathCandidates(['brave.exe']),
            opera: collectRegistryAppPathCandidates(['opera.exe', 'launcher.exe']),
            operagx: collectRegistryAppPathCandidates(['opera.exe', 'launcher.exe']),
            firefox: collectRegistryAppPathCandidates(['firefox.exe']),
        };
        const firefoxProfileEvidenceRoots = dedupeNonEmpty([
            ...roamingAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Mozilla', 'Firefox', 'Profiles')),
            ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Mozilla', 'Firefox', 'Profiles')),
            ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.mozilla', 'firefox')),
        ]).filter((candidate) => fs_1.default.existsSync(candidate));
        const browserExecutableCandidates = {
            edge: [
                'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
                'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
                joinWithBase(process.env['ProgramFiles(x86)'], 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
                joinWithBase(process.env.PROGRAMFILES, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe')),
                ...(browserRegistryExecutableCandidates.edge || []),
                '/usr/bin/microsoft-edge',
                '/usr/bin/microsoft-edge-stable',
                '/opt/microsoft/msedge/msedge',
                '/snap/bin/microsoft-edge',
            ],
            chrome: [
                'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
                'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
                joinWithBase(process.env.PROGRAMFILES, 'Google', 'Chrome', 'Application', 'chrome.exe'),
                joinWithBase(process.env['ProgramFiles(x86)'], 'Google', 'Chrome', 'Application', 'chrome.exe'),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe')),
                ...(browserRegistryExecutableCandidates.chrome || []),
                '/usr/bin/google-chrome',
                '/usr/bin/google-chrome-stable',
                '/opt/google/chrome/google-chrome',
                '/usr/bin/chromium',
                '/usr/bin/chromium-browser',
                '/snap/bin/chromium',
            ],
            brave: [
                'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
                'C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
                joinWithBase(process.env.PROGRAMFILES, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
                joinWithBase(process.env['ProgramFiles(x86)'], 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
                joinWithBase(process.env.LOCALAPPDATA, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe')),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Programs', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe')),
                ...(browserRegistryExecutableCandidates.brave || []),
                '/usr/bin/brave-browser',
                '/opt/brave.com/brave/brave-browser',
                '/snap/bin/brave',
            ],
            opera: [
                'C:\\Program Files\\Opera\\opera.exe',
                'C:\\Program Files\\Opera\\launcher.exe',
                'C:\\Program Files (x86)\\Opera\\opera.exe',
                'C:\\Program Files (x86)\\Opera\\launcher.exe',
                joinWithBase(process.env.PROGRAMFILES, 'Opera', 'opera.exe'),
                joinWithBase(process.env.PROGRAMFILES, 'Opera', 'launcher.exe'),
                joinWithBase(process.env['ProgramFiles(x86)'], 'Opera', 'opera.exe'),
                joinWithBase(process.env['ProgramFiles(x86)'], 'Opera', 'launcher.exe'),
                joinWithBase(process.env.LOCALAPPDATA, 'Programs', 'Opera', 'opera.exe'),
                joinWithBase(process.env.LOCALAPPDATA, 'Programs', 'Opera', 'launcher.exe'),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Programs', 'Opera', 'opera.exe')),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Programs', 'Opera', 'launcher.exe')),
                ...roamingAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Opera Software', 'Opera Stable', 'opera.exe')),
                ...roamingAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Opera Software', 'Opera Stable', 'launcher.exe')),
                ...(browserRegistryExecutableCandidates.opera || []),
                '/usr/bin/opera',
                '/usr/bin/opera-stable',
            ],
            operagx: [
                'C:\\Program Files\\Opera GX\\opera.exe',
                'C:\\Program Files\\Opera GX\\launcher.exe',
                'C:\\Program Files (x86)\\Opera GX\\opera.exe',
                'C:\\Program Files (x86)\\Opera GX\\launcher.exe',
                joinWithBase(process.env.PROGRAMFILES, 'Opera GX', 'opera.exe'),
                joinWithBase(process.env.PROGRAMFILES, 'Opera GX', 'launcher.exe'),
                joinWithBase(process.env['ProgramFiles(x86)'], 'Opera GX', 'opera.exe'),
                joinWithBase(process.env['ProgramFiles(x86)'], 'Opera GX', 'launcher.exe'),
                joinWithBase(process.env.LOCALAPPDATA, 'Programs', 'Opera GX', 'opera.exe'),
                joinWithBase(process.env.LOCALAPPDATA, 'Programs', 'Opera GX', 'launcher.exe'),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Programs', 'Opera GX', 'opera.exe')),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Programs', 'Opera GX', 'launcher.exe')),
                ...roamingAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Opera Software', 'Opera GX Stable', 'opera.exe')),
                ...roamingAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Opera Software', 'Opera GX Stable', 'launcher.exe')),
                ...(browserRegistryExecutableCandidates.operagx || []),
                '/usr/bin/opera-gx',
            ],
            firefox: [
                'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
                'C:\\Program Files\\Mozilla Firefox ESR\\firefox.exe',
                'C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe',
                'C:\\Program Files (x86)\\Mozilla Firefox ESR\\firefox.exe',
                joinWithBase(process.env.PROGRAMFILES, 'Mozilla Firefox', 'firefox.exe'),
                joinWithBase(process.env.PROGRAMFILES, 'Mozilla Firefox ESR', 'firefox.exe'),
                joinWithBase(process.env['ProgramFiles(x86)'], 'Mozilla Firefox', 'firefox.exe'),
                joinWithBase(process.env['ProgramFiles(x86)'], 'Mozilla Firefox ESR', 'firefox.exe'),
                joinWithBase(process.env.LOCALAPPDATA, 'Mozilla Firefox', 'firefox.exe'),
                joinWithBase(process.env.LOCALAPPDATA, 'Programs', 'Mozilla Firefox', 'firefox.exe'),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Mozilla Firefox', 'firefox.exe')),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Programs', 'Mozilla Firefox', 'firefox.exe')),
                ...localAppDataRootsForDetection.map((root) => path_1.default.join(root, 'Programs', 'Firefox Developer Edition', 'firefox.exe')),
                ...(browserRegistryExecutableCandidates.firefox || []),
                '/usr/bin/firefox',
                '/usr/bin/firefox-esr',
                '/snap/bin/firefox',
            ],
        };
        for (const browser of Object.keys(browserExecutableCandidates)) {
            browserExecutableCandidates[browser] = dedupeNonEmpty(browserExecutableCandidates[browser] || []);
        }
        const browserExecutableDetection = {};
        for (const browser of requestedTargets) {
            const checkedPaths = dedupeNonEmpty(browserExecutableCandidates[browser] || []);
            const foundPaths = checkedPaths.filter((candidate) => fs_1.default.existsSync(candidate));
            const profileEvidencePaths = browser === 'firefox' ? firefoxProfileEvidenceRoots : [];
            browserExecutableDetection[browser] = { checkedPaths, foundPaths, profileEvidencePaths };
        }
        const targetBrowsers = requestedTargets.filter((browser) => {
            const detection = browserExecutableDetection[browser];
            if (!detection)
                return false;
            if (detection.foundPaths.length > 0)
                return true;
            if (browser === 'firefox' && detection.profileEvidencePaths.length > 0)
                return true;
            return false;
        });
        const unavailableBrowsers = requestedTargets.filter((browser) => !targetBrowsers.includes(browser));
        const unmanagedChromiumStoreOnly = payload?.unmanagedChromiumStoreOnly === true
            || String(payload?.unmanagedChromiumStoreOnly || '').toLowerCase() === 'true';
        const normalizeChromiumStoreId = (value) => {
            const raw = String(value || '').trim().toLowerCase();
            return /^[a-p]{32}$/.test(raw) ? raw : '';
        };
        const normalizeUpdateUrl = (value) => {
            const raw = String(value || '').trim();
            if (!raw)
                return '';
            try {
                const parsed = new url_1.URL(raw);
                if (parsed.protocol === 'https:' || parsed.protocol === 'http:')
                    return raw.replace(/\/+$/, '');
                return '';
            }
            catch {
                return '';
            }
        };
        const chromiumStoreMap = {};
        if (payload?.chromiumStoreMap && typeof payload.chromiumStoreMap === 'object' && !Array.isArray(payload.chromiumStoreMap)) {
            for (const [rawBrowserKey, rawEntry] of Object.entries(payload.chromiumStoreMap)) {
                const normalizedKeyInput = String(rawBrowserKey || '').trim().toLowerCase();
                const normalizedKey = browserAliases[normalizedKeyInput] || normalizedKeyInput;
                if (!normalizedKey || !supportedBrowsers.includes(normalizedKey) || normalizedKey === 'firefox')
                    continue;
                const entry = (rawEntry && typeof rawEntry === 'object' && !Array.isArray(rawEntry)) ? rawEntry : {};
                const id = normalizeChromiumStoreId(entry.id);
                const updateUrl = normalizeUpdateUrl(entry.updateUrl);
                if (!id || !updateUrl)
                    continue;
                chromiumStoreMap[normalizedKey] = { id, updateUrl };
            }
        }
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        const browserExecutableDetectionSummary = requestedTargets.reduce((acc, browser) => {
            const detection = browserExecutableDetection[browser] || { checkedPaths: [], foundPaths: [], profileEvidencePaths: [] };
            acc[browser] = {
                checkedPathCount: detection.checkedPaths.length,
                foundPathCount: detection.foundPaths.length,
            };
            return acc;
        }, {});
        logger_1.logger.info(`Browser extension ${operationMode} requested`, {
            requestedBrowser,
            effectiveRequestedBrowser,
            targetBrowsers,
            unavailableBrowsers,
            browserExecutableDetectionSummary,
            unmanagedChromiumStoreOnly,
            chromiumStoreBrowsers: Object.keys(chromiumStoreMap),
            serverUrl,
        });
        let extensionVersion = '';
        try {
            if (process.platform !== 'win32' && process.platform !== 'linux') {
                throw new Error('Automatic browser extension deployment is currently supported on Windows and Linux agents only.');
            }
            // 1. Download the extension zip from the server
            const isLoopbackHost = (hostname) => {
                const host = String(hostname || '').toLowerCase();
                return host === 'localhost' || host === '127.0.0.1' || host === '::1';
            };
            const normalizeBaseUrl = (input) => {
                const raw = String(input || '').trim();
                if (!raw)
                    return '';
                try {
                    const parsed = new url_1.URL(raw);
                    if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
                        return '';
                    return raw.replace(/\/+$/, '');
                }
                catch {
                    return '';
                }
            };
            const rawServerUrlCandidates = [
                ...(Array.isArray(payload?.serverUrls) ? payload.serverUrls : []),
                serverUrl,
                config_1.config.server.url,
                this.wgDeployUrl,
            ].map((value) => String(value || '').trim()).filter(Boolean);
            const rawDownloadUrlCandidates = (Array.isArray(payload?.downloadUrls) ? payload.downloadUrls : [])
                .map((value) => String(value || '').trim())
                .filter(Boolean);
            const normalizedBaseUrls = Array.from(new Set(rawServerUrlCandidates
                .map((value) => normalizeBaseUrl(value))
                .filter(Boolean)));
            const directDownloadUrls = Array.from(new Set(rawDownloadUrlCandidates.filter((value) => {
                try {
                    const parsed = new url_1.URL(value);
                    return parsed.protocol === 'https:' || parsed.protocol === 'http:';
                }
                catch {
                    return false;
                }
            })));
            const baseUrl = normalizedBaseUrls[0] || '';
            if (!baseUrl) {
                throw new Error('No valid server URL available for extension package download');
            }
            const usingInsecureHttp = (() => {
                try {
                    const parsed = new url_1.URL(baseUrl);
                    return parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname);
                }
                catch {
                    return false;
                }
            })();
            if (usingInsecureHttp) {
                logger_1.logger.warn(`Extension install using HTTP endpoint: ${baseUrl}`);
            }
            const downloadCandidates = Array.from(new Set([
                ...directDownloadUrls,
                ...normalizedBaseUrls.map((u) => `${u}/api/upgrades/packages/appstats-browser-extension.zip`),
            ]));
            const extDir = (0, app_paths_1.ensureBrowserExtensionDataDir)();
            const zipPath = path_1.default.join(extDir, 'appstats-browser-extension.zip');
            // Create directory
            fs_1.default.mkdirSync(extDir, { recursive: true });
            const downloadToFile = (url, redirectDepth = 0) => new Promise((resolve, reject) => {
                if (redirectDepth > 5) {
                    reject(new Error('Too many redirects'));
                    return;
                }
                const urlObj = new url_1.URL(url);
                const client = urlObj.protocol === 'https:' ? https_1.default : http_1.default;
                const maxPackageBytes = 100 * 1024 * 1024;
                const req = client.get(url, {
                    headers: config_1.config.server.token ? { 'X-Agent-Token': config_1.config.server.token } : undefined,
                }, (res) => {
                    const status = Number(res.statusCode || 0);
                    if ((status === 301 || status === 302 || status === 307 || status === 308) && res.headers.location) {
                        const nextUrl = new url_1.URL(res.headers.location, url).toString();
                        res.resume();
                        downloadToFile(nextUrl, redirectDepth + 1).then(resolve).catch(reject);
                        return;
                    }
                    if (status !== 200) {
                        res.resume();
                        reject(new Error(`Download failed: HTTP ${status}`));
                        return;
                    }
                    const contentLength = Number(res.headers['content-length'] || 0);
                    if (contentLength > maxPackageBytes) {
                        res.resume();
                        reject(new Error(`Download failed: package too large (${contentLength} bytes)`));
                        return;
                    }
                    const file = fs_1.default.createWriteStream(zipPath);
                    file.on('error', reject);
                    res.on('error', reject);
                    file.on('finish', () => {
                        file.close();
                        resolve();
                    });
                    res.pipe(file);
                });
                req.setTimeout(25000, () => {
                    req.destroy(new Error(`Download request timed out for ${urlObj.host}`));
                });
                req.on('error', reject);
            });
            let downloadedFrom = '';
            const errors = [];
            for (const downloadUrl of downloadCandidates) {
                try {
                    logger_1.logger.info(`Downloading extension from ${downloadUrl}`);
                    await downloadToFile(downloadUrl);
                    downloadedFrom = downloadUrl;
                    break;
                }
                catch (e1) {
                    errors.push(`${downloadUrl} (${e1.message})`);
                }
            }
            if (!downloadedFrom) {
                throw new Error(`Extension download failed from all candidates: ${errors.join(' | ')}`);
            }
            logger_1.logger.info(`Extension downloaded from ${downloadedFrom}, extracting...`);
            // 2. Extract zip
            const extractDir = path_1.default.join(extDir, 'unpacked');
            if (fs_1.default.existsSync(extractDir)) {
                fs_1.default.rmSync(extractDir, { recursive: true, force: true });
            }
            fs_1.default.mkdirSync(extractDir, { recursive: true });
            const { execFileSync, execSync } = require('child_process');
            if (process.platform === 'win32') {
                execSync(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`, { timeout: 30000 });
            }
            else {
                const extractionAttempts = [
                    {
                        command: 'python3',
                        args: ['-c', 'import sys, zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])', zipPath, extractDir],
                    },
                    {
                        command: 'python',
                        args: ['-c', 'import sys, zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])', zipPath, extractDir],
                    },
                    {
                        command: 'unzip',
                        args: ['-o', zipPath, '-d', extractDir],
                    },
                ];
                const extractionErrors = [];
                let extracted = false;
                for (const attempt of extractionAttempts) {
                    try {
                        execFileSync(attempt.command, attempt.args, { timeout: 30000, stdio: 'ignore' });
                        extracted = true;
                        break;
                    }
                    catch (error) {
                        extractionErrors.push(`${attempt.command}: ${error?.message || String(error)}`);
                    }
                }
                if (!extracted) {
                    throw new Error(`Failed to extract extension package: ${extractionErrors.join(' | ')}`);
                }
            }
            logger_1.logger.info('Extension extracted to ' + extractDir);
            // 3. Configure extension — write/update config in the extension
            const configPath = path_1.default.join(extractDir, 'config.json');
            const extConfig = {
                serverUrl: baseUrl.replace(/\/+$/, ''),
                token: config_1.config.server.token,
                agentId: agentId,
                autoConnect: true,
                allowInsecureHttp: usingInsecureHttp,
            };
            fs_1.default.writeFileSync(configPath, JSON.stringify(extConfig, null, 2));
            // 4. Build extension artifacts for force-install/update.
            extensionVersion = '1.0.0';
            const manifestPath = path_1.default.join(extractDir, 'manifest.json');
            const manifestJson = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
            const requiredManifestKeys = ['manifest_version', 'name', 'version', 'permissions'];
            for (const key of requiredManifestKeys) {
                if (!(key in manifestJson)) {
                    throw new Error(`Invalid extension package: manifest is missing '${key}'`);
                }
            }
            if (Number(manifestJson.manifest_version) !== 3) {
                throw new Error(`Unsupported extension manifest_version: ${manifestJson.manifest_version}`);
            }
            if (!Array.isArray(manifestJson.permissions) || manifestJson.permissions.length === 0) {
                throw new Error('Invalid extension package: manifest permissions are empty');
            }
            extensionVersion = String(manifestJson?.version || extensionVersion);
            const chromiumTargets = targetBrowsers.filter((b) => b !== 'firefox');
            const chromiumPackerCandidates = [
                ...chromiumTargets.flatMap((browser) => (browserExecutableDetection[browser]?.foundPaths || [])),
                'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
                'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
                'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
                'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
                'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
                'C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
                'C:\\Program Files\\Opera\\opera.exe',
                'C:\\Program Files\\Opera\\launcher.exe',
                'C:\\Program Files (x86)\\Opera\\opera.exe',
                'C:\\Program Files (x86)\\Opera\\launcher.exe',
                'C:\\Program Files\\Opera GX\\opera.exe',
                'C:\\Program Files\\Opera GX\\launcher.exe',
                'C:\\Program Files (x86)\\Opera GX\\opera.exe',
                'C:\\Program Files (x86)\\Opera GX\\launcher.exe',
                path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
                path_1.default.join(process.env.PROGRAMFILES || '', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
                path_1.default.join(process.env.PROGRAMFILES || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
                path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
                path_1.default.join(process.env.PROGRAMFILES || '', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
                path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
                path_1.default.join(process.env.LOCALAPPDATA || '', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
                path_1.default.join(process.env.PROGRAMFILES || '', 'Opera', 'opera.exe'),
                path_1.default.join(process.env.PROGRAMFILES || '', 'Opera', 'launcher.exe'),
                path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Opera', 'opera.exe'),
                path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Opera', 'launcher.exe'),
                path_1.default.join(process.env.PROGRAMFILES || '', 'Opera GX', 'opera.exe'),
                path_1.default.join(process.env.PROGRAMFILES || '', 'Opera GX', 'launcher.exe'),
                path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Opera GX', 'opera.exe'),
                path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Opera GX', 'launcher.exe'),
                path_1.default.join(process.env.LOCALAPPDATA || '', 'Programs', 'Opera', 'opera.exe'),
                path_1.default.join(process.env.LOCALAPPDATA || '', 'Programs', 'Opera', 'launcher.exe'),
                path_1.default.join(process.env.LOCALAPPDATA || '', 'Programs', 'Opera GX', 'opera.exe'),
                path_1.default.join(process.env.LOCALAPPDATA || '', 'Programs', 'Opera GX', 'launcher.exe'),
            ];
            const chromiumPackerExecutables = Array.from(new Set(
            // launcher.exe is Opera's stub launcher and does not support --pack-extension
            chromiumPackerCandidates.filter((p) => p && fs_1.default.existsSync(p) && path_1.default.basename(p).toLowerCase() !== 'launcher.exe')));
            const extensionPemPath = path_1.default.join(extDir, 'appstats-browser-extension.pem');
            const generatedPemPath = `${extractDir}.pem`;
            const generatedCrxPath = `${extractDir}.crx`;
            const packagedCrxPath = path_1.default.join(extDir, 'appstats-browser-extension.crx');
            const packagedXpiPath = path_1.default.join(extDir, 'appstats-browser-extension.xpi');
            const updateXmlPath = path_1.default.join(extDir, 'updates.xml');
            let chromiumExtensionId = '';
            let chromiumUpdateUrl = '';
            let chromiumArtifactsReady = false;
            let chromiumArtifactsReason = '';
            const rebuildFirefoxPackage = (sourceDir, outputPath) => {
                try {
                    if (fs_1.default.existsSync(outputPath)) {
                        fs_1.default.rmSync(outputPath, { force: true });
                    }
                }
                catch { }
                const zipAttempts = process.platform === 'win32'
                    ? [
                        {
                            command: 'powershell',
                            args: [
                                '-NoProfile',
                                '-NonInteractive',
                                '-Command',
                                [
                                    '$src = [System.IO.Path]::GetFullPath($args[0])',
                                    '$dst = [System.IO.Path]::GetFullPath($args[1])',
                                    "if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }",
                                    "Compress-Archive -Path (Join-Path $src '*') -DestinationPath $dst -Force",
                                ].join('; '),
                                sourceDir,
                                outputPath,
                            ],
                        },
                    ]
                    : [
                        {
                            command: 'python3',
                            args: [
                                '-c',
                                [
                                    'import os, sys, zipfile',
                                    'source, target = sys.argv[1], sys.argv[2]',
                                    'with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as archive:',
                                    '    for root, _, files in os.walk(source):',
                                    '        for name in files:',
                                    '            file_path = os.path.join(root, name)',
                                    '            archive.write(file_path, os.path.relpath(file_path, source))',
                                ].join('\n'),
                                sourceDir,
                                outputPath,
                            ],
                        },
                        {
                            command: 'python',
                            args: [
                                '-c',
                                [
                                    'import os, sys, zipfile',
                                    'source, target = sys.argv[1], sys.argv[2]',
                                    'with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as archive:',
                                    '    for root, _, files in os.walk(source):',
                                    '        for name in files:',
                                    '            file_path = os.path.join(root, name)',
                                    '            archive.write(file_path, os.path.relpath(file_path, source))',
                                ].join('\n'),
                                sourceDir,
                                outputPath,
                            ],
                        },
                        {
                            command: 'zip',
                            args: ['-qr', outputPath, '.'],
                        },
                    ];
                const zipErrors = [];
                for (const attempt of zipAttempts) {
                    try {
                        const options = { timeout: 60000, stdio: 'ignore' };
                        if (attempt.command === 'zip') {
                            options.cwd = sourceDir;
                        }
                        execFileSync(attempt.command, attempt.args, options);
                        if (fs_1.default.existsSync(outputPath)) {
                            return true;
                        }
                    }
                    catch (error) {
                        zipErrors.push(`${attempt.command}: ${error?.message || String(error)}`);
                    }
                }
                logger_1.logger.warn(`Firefox XPI packaging fallback failed: ${zipErrors.join(' | ')}`);
                return false;
            };
            if (!rebuildFirefoxPackage(extractDir, packagedXpiPath)) {
                try {
                    fs_1.default.copyFileSync(zipPath, packagedXpiPath);
                }
                catch { }
            }
            if (chromiumTargets.length > 0) {
                if (chromiumPackerExecutables.length > 0) {
                    const linuxRootNoSandbox = process.platform === 'linux'
                        && typeof process.getuid === 'function'
                        && Number(process.getuid()) === 0;
                    for (const chromiumPackerExe of chromiumPackerExecutables) {
                        try {
                            if (fs_1.default.existsSync(generatedCrxPath))
                                fs_1.default.rmSync(generatedCrxPath, { force: true });
                            if (fs_1.default.existsSync(generatedPemPath))
                                fs_1.default.rmSync(generatedPemPath, { force: true });
                            const packProfileDir = path_1.default.join(extDir, 'packer-profile');
                            fs_1.default.mkdirSync(packProfileDir, { recursive: true });
                            const packCmd = fs_1.default.existsSync(extensionPemPath)
                                ? `"${chromiumPackerExe}" --user-data-dir="${packProfileDir}" --no-first-run${linuxRootNoSandbox ? ' --no-sandbox' : ''} --pack-extension="${extractDir}" --pack-extension-key="${extensionPemPath}"`
                                : `"${chromiumPackerExe}" --user-data-dir="${packProfileDir}" --no-first-run${linuxRootNoSandbox ? ' --no-sandbox' : ''} --pack-extension="${extractDir}"`;
                            execSync(packCmd, { timeout: 120000, windowsHide: true });
                            if (!fs_1.default.existsSync(extensionPemPath) && fs_1.default.existsSync(generatedPemPath)) {
                                fs_1.default.copyFileSync(generatedPemPath, extensionPemPath);
                            }
                            if (fs_1.default.existsSync(generatedCrxPath)) {
                                fs_1.default.copyFileSync(generatedCrxPath, packagedCrxPath);
                            }
                            if (fs_1.default.existsSync(extensionPemPath) && fs_1.default.existsSync(packagedCrxPath)) {
                                chromiumExtensionId = this.computeChromiumExtensionIdFromPem(fs_1.default.readFileSync(extensionPemPath, 'utf8'));
                                if (chromiumExtensionId && !/^[a-p]{32}$/.test(chromiumExtensionId)) {
                                    throw new Error(`Invalid Chromium extension ID generated: ${chromiumExtensionId}`);
                                }
                                if (chromiumExtensionId) {
                                    chromiumUpdateUrl = 'http://127.0.0.1:9898/extensions/updates.xml';
                                    const updateXml = `<?xml version='1.0' encoding='UTF-8'?>\n<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>\n  <app appid='${chromiumExtensionId}'>\n    <updatecheck codebase='http://127.0.0.1:9898/extensions/appstats-browser-extension.crx' version='${extensionVersion}' />\n  </app>\n</gupdate>\n`;
                                    fs_1.default.writeFileSync(updateXmlPath, updateXml);
                                    chromiumArtifactsReady = true;
                                    chromiumArtifactsReason = '';
                                    logger_1.logger.info(`Chromium CRX packaging succeeded via ${chromiumPackerExe}`);
                                    break;
                                }
                            }
                            chromiumArtifactsReason = `pack failed: ${path_1.default.basename(chromiumPackerExe)} produced no CRX artifacts`;
                            logger_1.logger.warn(`Chromium CRX packaging via ${chromiumPackerExe} produced no artifacts`);
                        }
                        catch (e) {
                            chromiumArtifactsReason = `pack failed: ${path_1.default.basename(chromiumPackerExe)} (${e.message})`;
                            logger_1.logger.warn(`Chromium CRX packaging failed via ${chromiumPackerExe}: ${e.message}`);
                        }
                    }
                }
                else {
                    chromiumArtifactsReason = 'pack skipped: no Chromium executable found';
                    logger_1.logger.warn('Chromium CRX packaging skipped: no browser executable found');
                }
                // Fallback: reuse previously packed artifacts if available.
                if (!chromiumArtifactsReady && fs_1.default.existsSync(extensionPemPath) && fs_1.default.existsSync(packagedCrxPath)) {
                    try {
                        const existingId = this.computeChromiumExtensionIdFromPem(fs_1.default.readFileSync(extensionPemPath, 'utf8'));
                        if (existingId && /^[a-p]{32}$/.test(existingId)) {
                            chromiumExtensionId = existingId;
                            chromiumUpdateUrl = 'http://127.0.0.1:9898/extensions/updates.xml';
                            if (!fs_1.default.existsSync(updateXmlPath)) {
                                const updateXml = `<?xml version='1.0' encoding='UTF-8'?>\n<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>\n  <app appid='${chromiumExtensionId}'>\n    <updatecheck codebase='http://127.0.0.1:9898/extensions/appstats-browser-extension.crx' version='${extensionVersion}' />\n  </app>\n</gupdate>\n`;
                                fs_1.default.writeFileSync(updateXmlPath, updateXml);
                            }
                            chromiumArtifactsReady = true;
                            chromiumArtifactsReason = '';
                            logger_1.logger.info('Chromium CRX packaging fallback: using existing CRX/PEM artifacts');
                        }
                    }
                    catch (e) {
                        chromiumArtifactsReason = chromiumArtifactsReason || `existing CRX reuse failed: ${e.message}`;
                    }
                }
            }
            const browserPolicies = {
                chrome: {
                    hklmDevMode: 'HKLM\\SOFTWARE\\Policies\\Google\\Chrome',
                    hkcuDevMode: 'HKCU\\SOFTWARE\\Policies\\Google\\Chrome',
                    hklmForceInstall: 'HKLM\\SOFTWARE\\Policies\\Google\\Chrome\\ExtensionInstallForcelist',
                    hkcuForceInstall: 'HKCU\\SOFTWARE\\Policies\\Google\\Chrome\\ExtensionInstallForcelist',
                    hklmInstallSources: 'HKLM\\SOFTWARE\\Policies\\Google\\Chrome\\ExtensionInstallSources',
                    hkcuInstallSources: 'HKCU\\SOFTWARE\\Policies\\Google\\Chrome\\ExtensionInstallSources',
                    hklmExtensionSettings: 'HKLM\\SOFTWARE\\Policies\\Google\\Chrome\\ExtensionSettings',
                    hkcuExtensionSettings: 'HKCU\\SOFTWARE\\Policies\\Google\\Chrome\\ExtensionSettings',
                },
                edge: {
                    hklmDevMode: 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Edge',
                    hkcuDevMode: 'HKCU\\SOFTWARE\\Policies\\Microsoft\\Edge',
                    hklmForceInstall: 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Edge\\ExtensionInstallForcelist',
                    hkcuForceInstall: 'HKCU\\SOFTWARE\\Policies\\Microsoft\\Edge\\ExtensionInstallForcelist',
                    hklmInstallSources: 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Edge\\ExtensionInstallSources',
                    hkcuInstallSources: 'HKCU\\SOFTWARE\\Policies\\Microsoft\\Edge\\ExtensionInstallSources',
                    hklmExtensionSettings: 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Edge\\ExtensionSettings',
                    hkcuExtensionSettings: 'HKCU\\SOFTWARE\\Policies\\Microsoft\\Edge\\ExtensionSettings',
                },
                brave: {
                    hklmDevMode: [
                        'HKLM\\SOFTWARE\\Policies\\BraveSoftware\\Brave',
                        'HKLM\\SOFTWARE\\Policies\\BraveSoftware\\Brave-Browser',
                    ],
                    hkcuDevMode: [
                        'HKCU\\SOFTWARE\\Policies\\BraveSoftware\\Brave',
                        'HKCU\\SOFTWARE\\Policies\\BraveSoftware\\Brave-Browser',
                    ],
                    hklmForceInstall: [
                        'HKLM\\SOFTWARE\\Policies\\BraveSoftware\\Brave\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\BraveSoftware\\Brave-Browser\\ExtensionInstallForcelist',
                    ],
                    hkcuForceInstall: [
                        'HKCU\\SOFTWARE\\Policies\\BraveSoftware\\Brave\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\BraveSoftware\\Brave-Browser\\ExtensionInstallForcelist',
                    ],
                    hklmInstallSources: [
                        'HKLM\\SOFTWARE\\Policies\\BraveSoftware\\Brave\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\BraveSoftware\\Brave-Browser\\ExtensionInstallSources',
                    ],
                    hkcuInstallSources: [
                        'HKCU\\SOFTWARE\\Policies\\BraveSoftware\\Brave\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\BraveSoftware\\Brave-Browser\\ExtensionInstallSources',
                    ],
                    hklmExtensionSettings: [
                        'HKLM\\SOFTWARE\\Policies\\BraveSoftware\\Brave\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\BraveSoftware\\Brave-Browser\\ExtensionSettings',
                    ],
                    hkcuExtensionSettings: [
                        'HKCU\\SOFTWARE\\Policies\\BraveSoftware\\Brave\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\BraveSoftware\\Brave-Browser\\ExtensionSettings',
                    ],
                },
                opera: {
                    hklmDevMode: [
                        'HKLM\\SOFTWARE\\Policies\\Opera Software',
                        'HKLM\\SOFTWARE\\Policies\\Opera',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera Stable',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera Stable',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera',
                    ],
                    hkcuDevMode: [
                        'HKCU\\SOFTWARE\\Policies\\Opera Software',
                        'HKCU\\SOFTWARE\\Policies\\Opera',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera Stable',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera Stable',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera',
                    ],
                    hklmForceInstall: [
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera Stable\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera Stable\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera\\ExtensionInstallForcelist',
                    ],
                    hkcuForceInstall: [
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera Stable\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera Stable\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera\\ExtensionInstallForcelist',
                    ],
                    hklmInstallSources: [
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera Stable\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera Stable\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera\\ExtensionInstallSources',
                    ],
                    hkcuInstallSources: [
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera Stable\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera Stable\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera\\ExtensionInstallSources',
                    ],
                    hklmExtensionSettings: [
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera Stable\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera Stable\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera\\ExtensionSettings',
                    ],
                    hkcuExtensionSettings: [
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera Stable\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera Stable\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera\\ExtensionSettings',
                    ],
                },
                operagx: {
                    hklmDevMode: [
                        'HKLM\\SOFTWARE\\Policies\\Opera Software',
                        'HKLM\\SOFTWARE\\Policies\\Opera',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera GX Stable',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera GX',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\OperaGX',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera GX Stable',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera GX',
                    ],
                    hkcuDevMode: [
                        'HKCU\\SOFTWARE\\Policies\\Opera Software',
                        'HKCU\\SOFTWARE\\Policies\\Opera',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera GX Stable',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera GX',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\OperaGX',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera GX Stable',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera GX',
                    ],
                    hklmForceInstall: [
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera GX Stable\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera GX\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\OperaGX\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera GX Stable\\ExtensionInstallForcelist',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera GX\\ExtensionInstallForcelist',
                    ],
                    hkcuForceInstall: [
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera GX Stable\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera GX\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\OperaGX\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera GX Stable\\ExtensionInstallForcelist',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera GX\\ExtensionInstallForcelist',
                    ],
                    hklmInstallSources: [
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera GX Stable\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera GX\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\OperaGX\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera GX Stable\\ExtensionInstallSources',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera GX\\ExtensionInstallSources',
                    ],
                    hkcuInstallSources: [
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera GX Stable\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera GX\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\OperaGX\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera GX Stable\\ExtensionInstallSources',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera GX\\ExtensionInstallSources',
                    ],
                    hklmExtensionSettings: [
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera GX Stable\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\Opera GX\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera Software\\OperaGX\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera GX Stable\\ExtensionSettings',
                        'HKLM\\SOFTWARE\\Policies\\Opera\\Opera GX\\ExtensionSettings',
                    ],
                    hkcuExtensionSettings: [
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera GX Stable\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\Opera GX\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera Software\\OperaGX\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera GX Stable\\ExtensionSettings',
                        'HKCU\\SOFTWARE\\Policies\\Opera\\Opera GX\\ExtensionSettings',
                    ],
                },
                firefox: {
                    hklmDevMode: 'HKLM\\SOFTWARE\\Policies\\Mozilla\\Firefox',
                    hkcuDevMode: 'HKCU\\SOFTWARE\\Policies\\Mozilla\\Firefox',
                },
            };
            const safeRegValue = (value) => String(value || '').replace(/"/g, '""');
            const addRegDword = (key, valueName, value) => {
                try {
                    execSync(`reg add "${key}" /v "${valueName}" /t REG_DWORD /d ${Math.trunc(value)} /f`, { timeout: 10000 });
                    return true;
                }
                catch {
                    return false;
                }
            };
            const addRegString = (key, valueName, value) => {
                try {
                    execSync(`reg add "${key}" /v "${valueName}" /t REG_SZ /d "${safeRegValue(value)}" /f`, { timeout: 10000 });
                    return true;
                }
                catch {
                    return false;
                }
            };
            const addRegDefaultString = (key, value) => {
                try {
                    execSync(`reg add "${key}" /ve /t REG_SZ /d "${safeRegValue(value)}" /f`, { timeout: 10000 });
                    return true;
                }
                catch {
                    return false;
                }
            };
            const normalizeRegKeys = (keys) => {
                if (!keys)
                    return [];
                const base = Array.isArray(keys)
                    ? keys.map((v) => String(v || '').trim()).filter(Boolean)
                    : [String(keys || '').trim()].filter(Boolean);
                if (!base.length)
                    return [];
                const expanded = new Set();
                for (const key of base) {
                    expanded.add(key);
                    if (key.startsWith('HKLM\\SOFTWARE\\') && !key.startsWith('HKLM\\SOFTWARE\\WOW6432Node\\')) {
                        expanded.add(key.replace('HKLM\\SOFTWARE\\', 'HKLM\\SOFTWARE\\WOW6432Node\\'));
                    }
                }
                return Array.from(expanded);
            };
            const queryRegValueRaw = (key, valueName) => {
                try {
                    return String(execSync(`reg query "${key}" /v "${valueName}"`, { timeout: 10000 }) || '');
                }
                catch {
                    return '';
                }
            };
            const hasRegValueInKeys = (keys, valueName, expectedContains) => {
                const expected = String(expectedContains || '').trim().toLowerCase();
                if (!expected)
                    return false;
                for (const key of normalizeRegKeys(keys)) {
                    const output = queryRegValueRaw(key, valueName).toLowerCase();
                    if (output.includes(expected))
                        return true;
                }
                return false;
            };
            const applyRegWithFallback = (label, hklmKey, hkcuKey, writer) => {
                for (const key of normalizeRegKeys(hklmKey)) {
                    if (writer(key)) {
                        logger_1.logger.info(`${label} applied in HKLM (${key})`);
                        return true;
                    }
                }
                for (const key of normalizeRegKeys(hkcuKey)) {
                    if (writer(key)) {
                        logger_1.logger.info(`${label} applied in HKCU (${key})`);
                        return true;
                    }
                }
                logger_1.logger.warn(`${label} failed in HKLM and HKCU`);
                return false;
            };
            const deleteRegValue = (key, valueName) => {
                try {
                    execSync(`reg delete "${key}" /v "${valueName}" /f`, { timeout: 10000 });
                    return true;
                }
                catch {
                    return false;
                }
            };
            const deleteRegKey = (key) => {
                try {
                    execSync(`reg delete "${key}" /f`, { timeout: 10000 });
                    return true;
                }
                catch {
                    return false;
                }
            };
            const queryRegValues = (key) => {
                try {
                    const raw = String(execSync(`reg query "${key}"`, { timeout: 10000 }) || '');
                    const rows = raw.split(/\r?\n/);
                    const values = [];
                    for (const row of rows) {
                        const trimmed = row.trim();
                        if (!trimmed || /^HKEY_/i.test(trimmed))
                            continue;
                        const match = trimmed.match(/^(\S+)\s+(REG_\S+)\s+(.*)$/i);
                        if (!match)
                            continue;
                        values.push({
                            name: String(match[1] || '').trim(),
                            type: String(match[2] || '').trim().toUpperCase(),
                            data: String(match[3] || '').trim(),
                        });
                    }
                    return values;
                }
                catch {
                    return [];
                }
            };
            const queryRegSubKeys = (key) => {
                try {
                    const raw = String(execSync(`reg query "${key}"`, { timeout: 10000 }) || '');
                    const rows = raw.split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
                    return rows.filter((row) => /^HKEY_/i.test(row));
                }
                catch {
                    return [];
                }
            };
            const normalizeExtensionId = (value) => {
                const raw = String(value || '').trim().toLowerCase();
                return /^[a-p]{32}$/.test(raw) ? raw : '';
            };
            const appStatsLocalUpdateMarkers = [
                '127.0.0.1:9898/extensions/updates.xml',
                'localhost:9898/extensions/updates.xml',
            ];
            const collectPolicyCleanup = (browser, policy, effectiveExtensionId, effectiveUpdateUrl, cleanupNotes) => {
                const expectedId = normalizeExtensionId(effectiveExtensionId);
                const expectedUpdate = String(effectiveUpdateUrl || '').trim().toLowerCase();
                const expectedIsLocalUpdate = appStatsLocalUpdateMarkers.some((marker) => expectedUpdate.includes(marker));
                if (!expectedId)
                    return 0;
                let removed = 0;
                const removeForceInstallEntries = (keys) => {
                    for (const key of normalizeRegKeys(keys)) {
                        const values = queryRegValues(key);
                        for (const value of values) {
                            const valueName = String(value.name || '').trim();
                            if (!valueName || valueName === '(Default)')
                                continue;
                            const rawData = String(value.data || '').trim();
                            const parts = rawData.split(';');
                            const entryId = normalizeExtensionId(parts[0] || '');
                            const entryUpdate = String(parts.slice(1).join(';') || '').trim().toLowerCase();
                            const rawDataLower = rawData.toLowerCase();
                            const managedEntry = entryId === expectedId
                                || appStatsLocalUpdateMarkers.some((marker) => rawDataLower.includes(marker));
                            if (!managedEntry)
                                continue;
                            const malformed = !entryId || parts.length < 2;
                            const wrongSlot = valueName !== '1';
                            const staleId = !!entryId && entryId !== expectedId;
                            const staleUpdate = expectedIsLocalUpdate
                                && entryId === expectedId
                                && !!entryUpdate
                                && entryUpdate !== expectedUpdate;
                            if (!malformed && !wrongSlot && !staleId && !staleUpdate)
                                continue;
                            if (deleteRegValue(key, valueName)) {
                                removed += 1;
                                cleanupNotes.push(`${browser}: cleared forcelist entry "${valueName}" from ${key}`);
                            }
                        }
                    }
                };
                const removeExtensionSettingsEntries = (keys) => {
                    for (const key of normalizeRegKeys(keys)) {
                        const values = queryRegValues(key);
                        for (const value of values) {
                            const valueName = normalizeExtensionId(value.name);
                            if (!valueName || valueName === expectedId)
                                continue;
                            const rawData = String(value.data || '').toLowerCase();
                            const managedEntry = appStatsLocalUpdateMarkers.some((marker) => rawData.includes(marker));
                            if (!managedEntry)
                                continue;
                            if (deleteRegValue(key, value.name)) {
                                removed += 1;
                                cleanupNotes.push(`${browser}: removed stale extension settings "${value.name}" from ${key}`);
                            }
                        }
                    }
                };
                removeForceInstallEntries(policy.hklmForceInstall);
                removeForceInstallEntries(policy.hkcuForceInstall);
                removeExtensionSettingsEntries(policy.hklmExtensionSettings);
                removeExtensionSettingsEntries(policy.hkcuExtensionSettings);
                return removed;
            };
            // 6. Native messaging host (Firefox gets explicit extension ID allowlist)
            const nativeHostDirs = process.platform === 'linux'
                ? {
                    chrome: [
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'google-chrome', 'NativeMessagingHosts')),
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'chromium', 'NativeMessagingHosts')),
                        '/etc/opt/chrome/native-messaging-hosts',
                        '/etc/chromium/native-messaging-hosts',
                    ],
                    edge: [
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'microsoft-edge', 'NativeMessagingHosts')),
                        '/etc/opt/edge/native-messaging-hosts',
                        '/etc/opt/microsoft/msedge/native-messaging-hosts',
                    ],
                    brave: [
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts')),
                        '/etc/brave/native-messaging-hosts',
                        '/etc/brave-browser/native-messaging-hosts',
                        '/etc/opt/brave.com/brave/native-messaging-hosts',
                    ],
                    opera: [
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'opera', 'NativeMessagingHosts')),
                        '/etc/opera/native-messaging-hosts',
                        '/etc/opt/opera/native-messaging-hosts',
                    ],
                    operagx: [
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'opera-gx', 'NativeMessagingHosts')),
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'operagx', 'NativeMessagingHosts')),
                        '/etc/opera-gx/native-messaging-hosts',
                    ],
                    firefox: [
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.mozilla', 'native-messaging-hosts')),
                        '/usr/lib/mozilla/native-messaging-hosts',
                        '/usr/lib64/mozilla/native-messaging-hosts',
                        '/etc/firefox/native-messaging-hosts',
                    ],
                }
                : {
                    chrome: [path_1.default.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'User Data', 'NativeMessagingHosts')],
                    edge: [path_1.default.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'Edge', 'User Data', 'NativeMessagingHosts')],
                    brave: [
                        path_1.default.join(process.env.LOCALAPPDATA || '', 'BraveSoftware', 'Brave-Browser', 'User Data', 'NativeMessagingHosts'),
                        path_1.default.join(process.env.APPDATA || '', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts'),
                    ],
                    opera: [
                        path_1.default.join(process.env.APPDATA || '', 'Opera Software', 'Opera Stable', 'NativeMessagingHosts'),
                        path_1.default.join(process.env.APPDATA || '', 'Opera Software', 'Opera', 'NativeMessagingHosts'),
                    ],
                    operagx: [
                        path_1.default.join(process.env.APPDATA || '', 'Opera Software', 'Opera GX Stable', 'NativeMessagingHosts'),
                        path_1.default.join(process.env.APPDATA || '', 'Opera Software', 'Opera GX', 'NativeMessagingHosts'),
                    ],
                    firefox: [
                        path_1.default.join(process.env.APPDATA || '', 'Mozilla', 'NativeMessagingHosts'),
                        path_1.default.join(process.env.LOCALAPPDATA || '', 'Mozilla', 'NativeMessagingHosts'),
                    ],
                };
            const nativeHostScript = path_1.default.join(extDir, process.platform === 'win32' ? 'native-host.bat' : 'native-host.sh');
            const nativeHostJs = path_1.default.join(extDir, 'native-host.js');
            const jsContent = `
const fs = require('fs');
const path = require('path');
const configPath = path.join(__dirname, 'unpacked', 'config.json');

// Read stdin length (first 4 bytes as uint32)
process.stdin.once('readable', () => {
  const lenBuf = process.stdin.read(4);
  if (!lenBuf) { process.exit(0); }
  const len = lenBuf.readUInt32LE(0);
  const msgBuf = process.stdin.read(len);
  if (!msgBuf) { process.exit(0); }

  try {
    const msg = JSON.parse(msgBuf.toString());
    let response = {};
    if (msg.type === 'get-config' && fs.existsSync(configPath)) {
      response = JSON.parse(fs.readFileSync(configPath, 'utf8'));
    }
    // Write response: 4-byte length prefix + JSON
    const out = Buffer.from(JSON.stringify(response));
    const header = Buffer.alloc(4);
    header.writeUInt32LE(out.length, 0);
    process.stdout.write(header);
    process.stdout.write(out);
  } catch (e) { /* ignore */ }
  process.exit(0);
});
`;
            fs_1.default.writeFileSync(nativeHostJs, jsContent.trim());
            if (process.platform === 'win32') {
                const nativeHostNode = String(process.execPath || 'node').replace(/"/g, '""');
                const nativeHostScriptPath = String(nativeHostJs || '').replace(/"/g, '""');
                fs_1.default.writeFileSync(nativeHostScript, `@echo off\r\n"${nativeHostNode}" "${nativeHostScriptPath}"`);
            }
            else {
                const shellQuote = (value) => `"${String(value || '').replace(/(["\\$`])/g, '\\$1')}"`;
                fs_1.default.writeFileSync(nativeHostScript, `#!/bin/sh\nexec ${shellQuote(String(process.execPath || 'node'))} ${shellQuote(nativeHostJs)}\n`, { mode: 0o755 });
                try {
                    fs_1.default.chmodSync(nativeHostScript, 0o755);
                }
                catch { }
            }
            const regKeys = {
                chrome: {
                    hklm: 'HKLM\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.appstats.agent',
                    hkcu: 'HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.appstats.agent',
                },
                edge: {
                    hklm: 'HKLM\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.appstats.agent',
                    hkcu: 'HKCU\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.appstats.agent',
                },
                brave: {
                    hklm: [
                        'HKLM\\SOFTWARE\\BraveSoftware\\Brave\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\BraveSoftware\\Brave-Browser\\NativeMessagingHosts\\com.appstats.agent',
                    ],
                    hkcu: [
                        'HKCU\\SOFTWARE\\BraveSoftware\\Brave\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\BraveSoftware\\Brave-Browser\\NativeMessagingHosts\\com.appstats.agent',
                    ],
                },
                opera: {
                    hklm: [
                        'HKLM\\SOFTWARE\\Opera Software\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera Software\\Opera Stable\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera Software\\Opera\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera\\Opera Stable\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera\\Opera\\NativeMessagingHosts\\com.appstats.agent',
                    ],
                    hkcu: [
                        'HKCU\\SOFTWARE\\Opera Software\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera Software\\Opera Stable\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera Software\\Opera\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera\\Opera Stable\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera\\Opera\\NativeMessagingHosts\\com.appstats.agent',
                    ],
                },
                operagx: {
                    hklm: [
                        'HKLM\\SOFTWARE\\Opera Software\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera Software\\Opera GX Stable\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera Software\\Opera GX\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera Software\\OperaGX\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera\\Opera GX Stable\\NativeMessagingHosts\\com.appstats.agent',
                        'HKLM\\SOFTWARE\\Opera\\Opera GX\\NativeMessagingHosts\\com.appstats.agent',
                    ],
                    hkcu: [
                        'HKCU\\SOFTWARE\\Opera Software\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera Software\\Opera GX Stable\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera Software\\Opera GX\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera Software\\OperaGX\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera\\Opera GX Stable\\NativeMessagingHosts\\com.appstats.agent',
                        'HKCU\\SOFTWARE\\Opera\\Opera GX\\NativeMessagingHosts\\com.appstats.agent',
                    ],
                },
                firefox: {
                    hklm: 'HKLM\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.appstats.agent',
                    hkcu: 'HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.appstats.agent',
                },
            };
            const listSubDirectories = (dirPath) => {
                if (!dirPath || !fs_1.default.existsSync(dirPath))
                    return [];
                try {
                    return fs_1.default.readdirSync(dirPath, { withFileTypes: true })
                        .filter((entry) => entry.isDirectory())
                        .map((entry) => path_1.default.join(dirPath, entry.name));
                }
                catch {
                    return [];
                }
            };
            const getWindowsUserProfileDirs = () => {
                const usersRoot = 'C:\\Users';
                if (!fs_1.default.existsSync(usersRoot))
                    return [];
                try {
                    const ignored = new Set([
                        'all users',
                        'default',
                        'default user',
                        'public',
                        'defaultapppool',
                        'wdagutilityaccount',
                    ]);
                    return fs_1.default.readdirSync(usersRoot, { withFileTypes: true })
                        .filter((entry) => entry.isDirectory())
                        .map((entry) => entry.name)
                        .filter((name) => !ignored.has(name.toLowerCase()))
                        .map((name) => path_1.default.join(usersRoot, name));
                }
                catch {
                    return [];
                }
            };
            const parseDsregFlag = (raw, label) => {
                const match = String(raw || '').match(new RegExp(`^\\s*${label}\\s*:\\s*(YES|NO)\\s*$`, 'im'));
                return !!match && String(match[1] || '').trim().toUpperCase() === 'YES';
            };
            const parseDsregValue = (raw, label) => {
                const match = String(raw || '').match(new RegExp(`^\\s*${label}\\s*:\\s*(.+)\\s*$`, 'im'));
                return match ? String(match[1] || '').trim() : '';
            };
            const hasMeaningfulDsregValue = (value) => {
                const normalized = String(value || '').trim().toLowerCase();
                return !!normalized && normalized !== 'no' && normalized !== 'none' && normalized !== 'n/a';
            };
            const getWindowsManagementState = () => {
                if (process.platform !== 'win32') {
                    return {
                        partOfDomain: false,
                        domain: '',
                        azureAdJoined: false,
                        enterpriseJoined: false,
                        mdmEnrolled: false,
                        managed: false,
                        managementSignals: [],
                    };
                }
                let partOfDomain = false;
                let domain = '';
                try {
                    const raw = String(execSync('powershell -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_ComputerSystem | Select-Object PartOfDomain,Domain | ConvertTo-Json -Compress)"', { timeout: 10000, windowsHide: true }) || '').trim();
                    if (raw) {
                        const parsed = JSON.parse(raw);
                        partOfDomain = parsed?.PartOfDomain === true || String(parsed?.PartOfDomain || '').toLowerCase() === 'true';
                        domain = String(parsed?.Domain || '').trim();
                    }
                }
                catch {
                    partOfDomain = false;
                    domain = '';
                }
                let dsregStatus = '';
                try {
                    dsregStatus = String(execSync('dsregcmd /status', {
                        timeout: 15000,
                        windowsHide: true,
                        stdio: ['ignore', 'pipe', 'ignore'],
                    }) || '').trim();
                }
                catch {
                    dsregStatus = '';
                }
                const azureAdJoined = parseDsregFlag(dsregStatus, 'AzureAdJoined');
                const enterpriseJoined = parseDsregFlag(dsregStatus, 'EnterpriseJoined');
                const dsregDomainJoined = parseDsregFlag(dsregStatus, 'DomainJoined');
                const mdmUrl = parseDsregValue(dsregStatus, 'MdmUrl');
                const deviceManagementSrvUrl = parseDsregValue(dsregStatus, 'DeviceManagementSrvUrl');
                const mdmEnrolled = hasMeaningfulDsregValue(mdmUrl) || hasMeaningfulDsregValue(deviceManagementSrvUrl);
                const managed = partOfDomain || dsregDomainJoined || azureAdJoined || enterpriseJoined || mdmEnrolled;
                const managementSignals = [];
                if (partOfDomain || dsregDomainJoined)
                    managementSignals.push('ad-domain');
                if (azureAdJoined)
                    managementSignals.push('azure-ad');
                if (enterpriseJoined)
                    managementSignals.push('enterprise');
                if (mdmEnrolled)
                    managementSignals.push('mdm');
                return {
                    partOfDomain: partOfDomain || dsregDomainJoined,
                    domain,
                    azureAdJoined,
                    enterpriseJoined,
                    mdmEnrolled,
                    managed,
                    managementSignals,
                };
            };
            const windowsUserProfiles = getWindowsUserProfileDirs();
            const localAppDataRoots = Array.from(new Set([
                String(process.env.LOCALAPPDATA || '').trim(),
                ...windowsUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Local')),
            ].filter((value) => value && fs_1.default.existsSync(value))));
            const roamingAppDataRoots = Array.from(new Set([
                String(process.env.APPDATA || '').trim(),
                ...windowsUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Roaming')),
            ].filter((value) => value && fs_1.default.existsSync(value))));
            const managementState = getWindowsManagementState();
            const unmanagedWindowsEndpoint = process.platform === 'win32' && !managementState.managed;
            const storeFallbackConfiguredBrowsers = Object.keys(chromiumStoreMap);
            const useStoreFallbackForUnmanaged = unmanagedWindowsEndpoint
                && storeFallbackConfiguredBrowsers.length > 0;
            const getChromiumProfileRoots = (browser) => {
                const perBrowser = (() => {
                    if (process.platform === 'linux') {
                        switch (browser) {
                            case 'edge':
                                return linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'microsoft-edge'));
                            case 'chrome':
                                return [
                                    ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'google-chrome')),
                                    ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'chromium')),
                                ];
                            case 'brave':
                                return linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'BraveSoftware', 'Brave-Browser'));
                            case 'opera':
                                return linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'opera'));
                            case 'operagx':
                                return [
                                    ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'opera-gx')),
                                    ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'operagx')),
                                ];
                            default:
                                return [];
                        }
                    }
                    switch (browser) {
                        case 'edge':
                            return localAppDataRoots.map((root) => path_1.default.join(root, 'Microsoft', 'Edge', 'User Data'));
                        case 'chrome':
                            return localAppDataRoots.map((root) => path_1.default.join(root, 'Google', 'Chrome', 'User Data'));
                        case 'brave':
                            return localAppDataRoots.map((root) => path_1.default.join(root, 'BraveSoftware', 'Brave-Browser', 'User Data'));
                        case 'opera':
                            return [
                                ...roamingAppDataRoots.map((root) => path_1.default.join(root, 'Opera Software', 'Opera Stable')),
                                ...roamingAppDataRoots.map((root) => path_1.default.join(root, 'Opera Software', 'Opera')),
                            ];
                        case 'operagx':
                            return [
                                ...roamingAppDataRoots.map((root) => path_1.default.join(root, 'Opera Software', 'Opera GX Stable')),
                                ...roamingAppDataRoots.map((root) => path_1.default.join(root, 'Opera Software', 'Opera GX')),
                            ];
                        default:
                            return [];
                    }
                })();
                return Array.from(new Set(perBrowser.filter((candidate) => candidate && fs_1.default.existsSync(candidate))));
            };
            const getChromiumProfileCandidates = (profileRoot) => {
                const candidates = new Set([
                    profileRoot,
                    path_1.default.join(profileRoot, 'Default'),
                    path_1.default.join(profileRoot, 'Guest Profile'),
                    path_1.default.join(profileRoot, 'System Profile'),
                ]);
                for (const subDir of listSubDirectories(profileRoot)) {
                    const name = path_1.default.basename(subDir);
                    if (/^profile\s+\d+$/i.test(name) || /^person\s+\d+$/i.test(name)) {
                        candidates.add(subDir);
                    }
                }
                return Array.from(candidates).filter((dir) => dir && fs_1.default.existsSync(dir));
            };
            const findInstalledChromiumExtensionPaths = (browser, extensionId) => {
                const found = new Set();
                if (!extensionId)
                    return [];
                for (const profileRoot of getChromiumProfileRoots(browser)) {
                    for (const profileDir of getChromiumProfileCandidates(profileRoot)) {
                        const extensionPath = path_1.default.join(profileDir, 'Extensions', extensionId);
                        if (!fs_1.default.existsSync(extensionPath))
                            continue;
                        const versionDirs = listSubDirectories(extensionPath);
                        if (versionDirs.length > 0) {
                            found.add(extensionPath);
                        }
                    }
                }
                return Array.from(found);
            };
            const waitForInstalledChromiumExtensionPaths = async (browser, extensionId, timeoutMs = 15000) => {
                const deadline = Date.now() + Math.max(0, timeoutMs);
                while (true) {
                    const paths = findInstalledChromiumExtensionPaths(browser, extensionId);
                    if (paths.length > 0)
                        return paths;
                    if (Date.now() >= deadline)
                        return [];
                    await new Promise((resolve) => setTimeout(resolve, 2500));
                }
            };
            const getChromiumProfileDirs = (browser) => {
                const found = new Set();
                for (const profileRoot of getChromiumProfileRoots(browser)) {
                    for (const candidate of getChromiumProfileCandidates(profileRoot)) {
                        const hasPreferences = fs_1.default.existsSync(path_1.default.join(candidate, 'Preferences'))
                            || fs_1.default.existsSync(path_1.default.join(candidate, 'Secure Preferences'));
                        if (hasPreferences) {
                            found.add(candidate);
                        }
                    }
                }
                return Array.from(found).filter((dir) => dir && fs_1.default.existsSync(dir));
            };
            const applyChromiumProfileExtensionToggles = (browser, extensionId) => {
                const normalizedId = String(extensionId || '').trim().toLowerCase();
                if (!normalizedId)
                    return { profilesChecked: 0, profilesUpdated: 0, filesUpdated: 0 };
                const profileDirs = getChromiumProfileDirs(browser);
                let profilesUpdated = 0;
                let filesUpdated = 0;
                for (const profileDir of profileDirs) {
                    let profileTouched = false;
                    for (const fileName of ['Preferences', 'Secure Preferences']) {
                        const prefPath = path_1.default.join(profileDir, fileName);
                        if (!fs_1.default.existsSync(prefPath))
                            continue;
                        try {
                            const raw = String(fs_1.default.readFileSync(prefPath, 'utf8') || '').trim();
                            if (!raw)
                                continue;
                            const parsed = JSON.parse(raw);
                            if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
                                continue;
                            const rootObj = parsed;
                            if (!rootObj.extensions || typeof rootObj.extensions !== 'object' || Array.isArray(rootObj.extensions)) {
                                rootObj.extensions = {};
                            }
                            if (!rootObj.extensions.settings || typeof rootObj.extensions.settings !== 'object' || Array.isArray(rootObj.extensions.settings)) {
                                rootObj.extensions.settings = {};
                            }
                            const settings = rootObj.extensions.settings;
                            const existing = (settings[normalizedId] && typeof settings[normalizedId] === 'object' && !Array.isArray(settings[normalizedId]))
                                ? { ...settings[normalizedId] }
                                : {};
                            let changed = false;
                            const setField = (key, value) => {
                                if (JSON.stringify(existing[key]) !== JSON.stringify(value)) {
                                    existing[key] = value;
                                    changed = true;
                                }
                            };
                            setField('state', 1);
                            setField('incognito', true);
                            setField('allow_file_access', true);
                            setField('allow_file_access_from_files', true);
                            if (!changed)
                                continue;
                            settings[normalizedId] = existing;
                            fs_1.default.writeFileSync(prefPath, JSON.stringify(rootObj, null, 2));
                            profileTouched = true;
                            filesUpdated += 1;
                        }
                        catch (profileError) {
                            logger_1.logger.debug('Failed to patch Chromium extension profile settings', {
                                browser,
                                profileDir,
                                fileName,
                                error: profileError?.message || String(profileError || 'unknown'),
                            });
                        }
                    }
                    if (profileTouched)
                        profilesUpdated += 1;
                }
                return { profilesChecked: profileDirs.length, profilesUpdated, filesUpdated };
            };
            const findInstalledFirefoxExtensionPaths = () => {
                const found = new Set();
                const profileRoots = Array.from(new Set([
                    ...roamingAppDataRoots.map((root) => path_1.default.join(root, 'Mozilla', 'Firefox', 'Profiles')),
                    ...localAppDataRoots.map((root) => path_1.default.join(root, 'Mozilla', 'Firefox', 'Profiles')),
                    ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.mozilla', 'firefox')),
                ].filter((candidate) => candidate && fs_1.default.existsSync(candidate))));
                for (const profileRoot of profileRoots) {
                    for (const profileDir of listSubDirectories(profileRoot)) {
                        const extensionDir = path_1.default.join(profileDir, 'extensions');
                        const xpiPath = path_1.default.join(extensionDir, 'appstats@emtelle.local.xpi');
                        const unpackedPath = path_1.default.join(extensionDir, 'appstats@emtelle.local');
                        if (fs_1.default.existsSync(xpiPath))
                            found.add(xpiPath);
                        if (fs_1.default.existsSync(unpackedPath))
                            found.add(unpackedPath);
                    }
                }
                return Array.from(found);
            };
            const externalChromiumRegistryRoots = {
                chrome: {
                    hklm: 'HKLM\\SOFTWARE\\Google\\Chrome\\Extensions',
                    hkcu: 'HKCU\\SOFTWARE\\Google\\Chrome\\Extensions',
                },
                edge: {
                    hklm: 'HKLM\\SOFTWARE\\Microsoft\\Edge\\Extensions',
                    hkcu: 'HKCU\\SOFTWARE\\Microsoft\\Edge\\Extensions',
                },
                brave: {
                    hklm: [
                        'HKLM\\SOFTWARE\\BraveSoftware\\Brave\\Extensions',
                        'HKLM\\SOFTWARE\\BraveSoftware\\Brave-Browser\\Extensions',
                    ],
                    hkcu: [
                        'HKCU\\SOFTWARE\\BraveSoftware\\Brave\\Extensions',
                        'HKCU\\SOFTWARE\\BraveSoftware\\Brave-Browser\\Extensions',
                    ],
                },
                opera: {
                    hklm: [
                        'HKLM\\SOFTWARE\\Opera Software\\Extensions',
                        'HKLM\\SOFTWARE\\Opera\\Extensions',
                        'HKLM\\SOFTWARE\\Opera Software\\Opera Stable\\Extensions',
                        'HKLM\\SOFTWARE\\Opera Software\\Opera\\Extensions',
                        'HKLM\\SOFTWARE\\Opera\\Opera Stable\\Extensions',
                        'HKLM\\SOFTWARE\\Opera\\Opera\\Extensions',
                    ],
                    hkcu: [
                        'HKCU\\SOFTWARE\\Opera Software\\Extensions',
                        'HKCU\\SOFTWARE\\Opera\\Extensions',
                        'HKCU\\SOFTWARE\\Opera Software\\Opera Stable\\Extensions',
                        'HKCU\\SOFTWARE\\Opera Software\\Opera\\Extensions',
                        'HKCU\\SOFTWARE\\Opera\\Opera Stable\\Extensions',
                        'HKCU\\SOFTWARE\\Opera\\Opera\\Extensions',
                    ],
                },
                operagx: {
                    hklm: [
                        'HKLM\\SOFTWARE\\Opera Software\\Extensions',
                        'HKLM\\SOFTWARE\\Opera\\Extensions',
                        'HKLM\\SOFTWARE\\Opera Software\\Opera GX Stable\\Extensions',
                        'HKLM\\SOFTWARE\\Opera Software\\Opera GX\\Extensions',
                        'HKLM\\SOFTWARE\\Opera Software\\OperaGX\\Extensions',
                        'HKLM\\SOFTWARE\\Opera\\Opera GX Stable\\Extensions',
                        'HKLM\\SOFTWARE\\Opera\\Opera GX\\Extensions',
                    ],
                    hkcu: [
                        'HKCU\\SOFTWARE\\Opera Software\\Extensions',
                        'HKCU\\SOFTWARE\\Opera\\Extensions',
                        'HKCU\\SOFTWARE\\Opera Software\\Opera GX Stable\\Extensions',
                        'HKCU\\SOFTWARE\\Opera Software\\Opera GX\\Extensions',
                        'HKCU\\SOFTWARE\\Opera Software\\OperaGX\\Extensions',
                        'HKCU\\SOFTWARE\\Opera\\Opera GX Stable\\Extensions',
                        'HKCU\\SOFTWARE\\Opera\\Opera GX\\Extensions',
                    ],
                },
            };
            const appendExtensionIdToRegKeys = (keys, extensionId) => {
                const baseKeys = Array.isArray(keys)
                    ? keys.map((value) => String(value || '').trim()).filter(Boolean)
                    : [String(keys || '').trim()].filter(Boolean);
                if (baseKeys.length < 1 || !extensionId)
                    return undefined;
                const withExtension = baseKeys
                    .map((key) => `${key.replace(/\\+$/, '')}\\${extensionId}`)
                    .filter(Boolean);
                if (withExtension.length < 1)
                    return undefined;
                return withExtension.length === 1 ? withExtension[0] : withExtension;
            };
            const applyExternalChromiumRegistryFallback = (browser, extensionId, crxPath, version, updateUrl) => {
                if (!browser || !extensionId || !crxPath || !version || !fs_1.default.existsSync(crxPath))
                    return false;
                const roots = externalChromiumRegistryRoots[browser];
                if (!roots)
                    return false;
                const hklmTarget = appendExtensionIdToRegKeys(roots.hklm, extensionId);
                const hkcuTarget = appendExtensionIdToRegKeys(roots.hkcu, extensionId);
                if (!hklmTarget && !hkcuTarget)
                    return false;
                const normalizedUpdateUrl = String(updateUrl || '').trim();
                return applyRegWithFallback(`External extension fallback for ${browser}`, hklmTarget, hkcuTarget, (key) => (addRegString(key, 'path', crxPath)
                    && addRegString(key, 'version', version)
                    && (!normalizedUpdateUrl
                        || addRegString(key, 'update_url', normalizedUpdateUrl))));
            };
            const applyExternalChromiumJsonFallback = (browser, extensionId, crxPath, version, allUserProfiles, machineDataRoots) => {
                const normalizedId = normalizeExtensionId(extensionId);
                const normalizedVersion = String(version || '').trim();
                if (!normalizedId || !crxPath || !fs_1.default.existsSync(crxPath) || !normalizedVersion)
                    return [];
                const browserRoots = browser === 'edge'
                    ? {
                        userRoots: allUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Local', 'Microsoft', 'Edge', 'User Data')),
                        machineRoots: [
                            path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Microsoft', 'Edge', 'Application', 'Extensions'),
                            path_1.default.join(process.env.PROGRAMFILES || '', 'Microsoft', 'Edge', 'Application', 'Extensions'),
                            ...machineDataRoots.map((root) => path_1.default.join(root, 'Microsoft', 'Edge', 'User Data', 'External Extensions')),
                        ],
                    }
                    : browser === 'chrome'
                        ? {
                            userRoots: allUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Local', 'Google', 'Chrome', 'User Data')),
                            machineRoots: [
                                path_1.default.join(process.env.PROGRAMFILES || '', 'Google', 'Chrome', 'Application', 'Extensions'),
                                path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Google', 'Chrome', 'Application', 'Extensions'),
                                ...machineDataRoots.map((root) => path_1.default.join(root, 'Google', 'Chrome', 'User Data', 'External Extensions')),
                            ],
                        }
                        : browser === 'brave'
                            ? {
                                userRoots: allUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Local', 'BraveSoftware', 'Brave-Browser', 'User Data')),
                                machineRoots: [
                                    path_1.default.join(process.env.PROGRAMFILES || '', 'BraveSoftware', 'Brave-Browser', 'Application', 'Extensions'),
                                    path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'BraveSoftware', 'Brave-Browser', 'Application', 'Extensions'),
                                    ...machineDataRoots.map((root) => path_1.default.join(root, 'BraveSoftware', 'Brave-Browser', 'User Data', 'External Extensions')),
                                ],
                            }
                            : browser === 'opera'
                                ? {
                                    userRoots: [
                                        ...allUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Roaming', 'Opera Software', 'Opera Stable')),
                                        ...allUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Roaming', 'Opera Software', 'Opera')),
                                    ],
                                    machineRoots: [
                                        path_1.default.join(process.env.PROGRAMFILES || '', 'Opera', 'Extensions'),
                                        path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Opera', 'Extensions'),
                                        ...machineDataRoots.map((root) => path_1.default.join(root, 'Opera Software', 'Opera Stable', 'External Extensions')),
                                        ...machineDataRoots.map((root) => path_1.default.join(root, 'Opera Software', 'Opera', 'External Extensions')),
                                    ],
                                }
                                : browser === 'operagx'
                                    ? {
                                        userRoots: [
                                            ...allUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Roaming', 'Opera Software', 'Opera GX Stable')),
                                            ...allUserProfiles.map((userDir) => path_1.default.join(userDir, 'AppData', 'Roaming', 'Opera Software', 'Opera GX')),
                                        ],
                                        machineRoots: [
                                            path_1.default.join(process.env.PROGRAMFILES || '', 'Opera GX', 'Extensions'),
                                            path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Opera GX', 'Extensions'),
                                            ...machineDataRoots.map((root) => path_1.default.join(root, 'Opera Software', 'Opera GX Stable', 'External Extensions')),
                                            ...machineDataRoots.map((root) => path_1.default.join(root, 'Opera Software', 'Opera GX', 'External Extensions')),
                                        ],
                                    }
                                    : null;
                if (!browserRoots)
                    return [];
                const payload = JSON.stringify({
                    external_crx: crxPath,
                    external_version: normalizedVersion,
                }, null, 2);
                const targets = new Set();
                for (const userRoot of browserRoots.userRoots) {
                    targets.add(path_1.default.join(userRoot, 'External Extensions'));
                }
                for (const machineRoot of browserRoots.machineRoots) {
                    targets.add(machineRoot);
                }
                const written = [];
                for (const target of targets) {
                    if (!target)
                        continue;
                    try {
                        fs_1.default.mkdirSync(target, { recursive: true });
                        const jsonPath = path_1.default.join(target, `${normalizedId}.json`);
                        fs_1.default.writeFileSync(jsonPath, payload);
                        written.push(jsonPath);
                    }
                    catch (error) {
                        logger_1.logger.debug('Failed writing Chromium external extension JSON fallback', {
                            browser,
                            extensionId: normalizedId,
                            target,
                            error: error?.message || String(error || 'unknown'),
                        });
                    }
                }
                return written;
            };
            const collectExternalRegistryCleanup = (browser, roots, effectiveExtensionId, cleanupNotes) => {
                if (!roots)
                    return 0;
                const expectedId = normalizeExtensionId(effectiveExtensionId);
                if (!expectedId)
                    return 0;
                let removed = 0;
                const removeFromRoots = (keyRoots) => {
                    for (const rootKey of normalizeRegKeys(keyRoots)) {
                        const subKeys = queryRegSubKeys(rootKey);
                        for (const subKey of subKeys) {
                            const suffix = String(subKey).split('\\').pop() || '';
                            const keyId = normalizeExtensionId(suffix);
                            if (!keyId || keyId === expectedId)
                                continue;
                            const rawValues = queryRegValues(subKey);
                            const rawBlob = rawValues.map((value) => `${value.name} ${value.data}`).join('\n').toLowerCase();
                            const managedEntry = rawBlob.includes('appstats-browser-extension.crx')
                                || rawBlob.includes('\\appstats\\browser-extension\\')
                                || appStatsLocalUpdateMarkers.some((marker) => rawBlob.includes(marker));
                            if (!managedEntry)
                                continue;
                            if (deleteRegKey(subKey)) {
                                removed += 1;
                                cleanupNotes.push(`${browser}: removed stale external root key ${subKey}`);
                            }
                        }
                    }
                };
                removeFromRoots(roots.hklm);
                removeFromRoots(roots.hkcu);
                return removed;
            };
            const configuredBrowsers = [];
            const policyConfiguredBrowsers = [];
            const skippedBrowsers = [...unavailableBrowsers.map((browser) => `${browser} (browser executable not found)`)];
            const verifiedInstallPaths = {};
            const storePolicyBrowsers = [];
            const externalFallbackBrowsers = [];
            const externalJsonFallbackBrowsers = [];
            const platformBlockedBrowsers = [];
            const policyCleanupNotes = [];
            const fallbackArtifactPaths = {};
            const perBrowserTelemetry = {};
            for (const browser of requestedTargets) {
                const detection = browserExecutableDetection[browser] || { checkedPaths: [], foundPaths: [], profileEvidencePaths: [] };
                perBrowserTelemetry[browser] = {
                    ...(perBrowserTelemetry[browser] || {}),
                    browserExecutableCheckedPaths: detection.checkedPaths,
                    browserExecutableFoundPaths: detection.foundPaths,
                    managementSignals: managementState.managementSignals,
                };
            }
            const machineDataRoots = Array.from(new Set([
                String(process.env.PROGRAMDATA || '').trim(),
                'C:\\ProgramData',
            ].filter((value) => value && fs_1.default.existsSync(value))));
            if (process.platform === 'linux') {
                const linuxChromiumPolicyDirs = {
                    chrome: dedupeNonEmpty([
                        '/etc/opt/chrome/policies/managed',
                        '/etc/chromium/policies/managed',
                    ]),
                    edge: dedupeNonEmpty([
                        '/etc/opt/edge/policies/managed',
                        '/etc/opt/microsoft/msedge/policies/managed',
                    ]),
                    brave: dedupeNonEmpty([
                        '/etc/brave/policies/managed',
                        '/etc/brave-browser/policies/managed',
                        '/etc/opt/brave.com/brave/policies/managed',
                    ]),
                    opera: dedupeNonEmpty([
                        '/etc/opera/policies/managed',
                        '/etc/opt/opera/policies/managed',
                    ]),
                    operagx: dedupeNonEmpty([
                        '/etc/opera-gx/policies/managed',
                        '/etc/opt/opera/policies/managed',
                    ]),
                };
                const linuxExternalExtensionRoots = {
                    chrome: dedupeNonEmpty([
                        '/opt/google/chrome/extensions',
                        '/usr/share/google-chrome/extensions',
                        '/usr/share/chromium/extensions',
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'google-chrome', 'External Extensions')),
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'chromium', 'External Extensions')),
                    ]),
                    edge: dedupeNonEmpty([
                        '/opt/microsoft/msedge/extensions',
                        '/usr/share/microsoft-edge/extensions',
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'microsoft-edge', 'External Extensions')),
                    ]),
                    brave: dedupeNonEmpty([
                        '/opt/brave.com/brave/extensions',
                        '/usr/share/brave-browser/extensions',
                        '/usr/share/brave/extensions',
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'BraveSoftware', 'Brave-Browser', 'External Extensions')),
                    ]),
                    opera: dedupeNonEmpty([
                        '/usr/share/opera/extensions',
                        '/opt/opera/extensions',
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'opera', 'External Extensions')),
                    ]),
                    operagx: dedupeNonEmpty([
                        '/usr/share/opera-gx/extensions',
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'opera-gx', 'External Extensions')),
                        ...linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.config', 'operagx', 'External Extensions')),
                    ]),
                };
                const linuxFirefoxDistributionDirs = dedupeNonEmpty([
                    '/etc/firefox/policies',
                    '/usr/lib/firefox/distribution',
                    '/usr/lib64/firefox/distribution',
                    '/usr/lib/firefox-esr/distribution',
                ]);
                const writeJsonFiles = (dirs, fileName, payload) => {
                    const written = [];
                    const body = JSON.stringify(payload, null, 2);
                    for (const dir of dirs) {
                        if (!dir)
                            continue;
                        try {
                            fs_1.default.mkdirSync(dir, { recursive: true });
                            const filePath = path_1.default.join(dir, fileName);
                            fs_1.default.writeFileSync(filePath, body);
                            written.push(filePath);
                        }
                        catch (error) {
                            logger_1.logger.debug('Failed writing Linux extension file', {
                                dir,
                                fileName,
                                error: error?.message || String(error || 'unknown'),
                            });
                        }
                    }
                    return written;
                };
                const writeLinuxFirefoxPolicies = () => {
                    if (!fs_1.default.existsSync(packagedXpiPath))
                        return [];
                    const firefoxInstallUrl = (0, url_1.pathToFileURL)(packagedXpiPath).toString();
                    const policyPayload = {
                        policies: {
                            Extensions: {
                                Install: [firefoxInstallUrl],
                            },
                            Preferences: {
                                'xpinstall.signatures.required': { Value: false, Status: 'locked' },
                                'extensions.langpacks.signatures.required': { Value: false, Status: 'locked' },
                            },
                            ExtensionSettings: {
                                'appstats@emtelle.local': {
                                    installation_mode: 'force_installed',
                                    install_url: firefoxInstallUrl,
                                    private_browsing: true,
                                },
                            },
                        },
                    };
                    return writeJsonFiles(linuxFirefoxDistributionDirs, 'policies.json', policyPayload);
                };
                const copyLinuxFirefoxExtensionToProfiles = () => {
                    if (!fs_1.default.existsSync(packagedXpiPath))
                        return [];
                    const copied = [];
                    const profileRoots = dedupeNonEmpty(linuxHomeDirsForDetection.map((root) => path_1.default.join(root, '.mozilla', 'firefox'))).filter((candidate) => fs_1.default.existsSync(candidate));
                    for (const profileRoot of profileRoots) {
                        for (const profileDir of listSubDirectories(profileRoot)) {
                            const extensionsDir = path_1.default.join(profileDir, 'extensions');
                            const xpiPath = path_1.default.join(extensionsDir, 'appstats@emtelle.local.xpi');
                            try {
                                fs_1.default.mkdirSync(extensionsDir, { recursive: true });
                                fs_1.default.copyFileSync(packagedXpiPath, xpiPath);
                                copied.push(xpiPath);
                            }
                            catch (error) {
                                logger_1.logger.debug('Failed copying Firefox extension into Linux profile', {
                                    profileDir,
                                    error: error?.message || String(error || 'unknown'),
                                });
                            }
                        }
                    }
                    return copied;
                };
                for (const target of targetBrowsers) {
                    if (target !== 'firefox') {
                        if (!chromiumArtifactsReady || !chromiumExtensionId || !chromiumUpdateUrl) {
                            skippedBrowsers.push(`${target} (${chromiumArtifactsReason || 'missing CRX/update artifacts'})`);
                            continue;
                        }
                        const policyFiles = writeJsonFiles(linuxChromiumPolicyDirs[target] || [], 'appstats-browser-extension.json', {
                            ExtensionInstallForcelist: [`${chromiumExtensionId};${chromiumUpdateUrl}`],
                            ExtensionInstallSources: [
                                'http://127.0.0.1:9898/*',
                                'http://localhost:9898/*',
                                'https://127.0.0.1:9898/*',
                                'https://localhost:9898/*',
                                '*://*/*',
                            ],
                            ExtensionSettings: {
                                [chromiumExtensionId]: {
                                    installation_mode: 'force_installed',
                                    update_url: chromiumUpdateUrl,
                                    override_update_url: true,
                                    minimum_version_required: extensionVersion,
                                    incognito: 'split',
                                    allow_file_access: true,
                                    file_url_navigation_allowed: true,
                                },
                            },
                        });
                        const externalJsonFiles = writeJsonFiles(linuxExternalExtensionRoots[target] || [], `${chromiumExtensionId}.json`, { external_update_url: chromiumUpdateUrl });
                        if (policyFiles.length < 1 && externalJsonFiles.length < 1) {
                            skippedBrowsers.push(`${target} (failed to write Linux extension metadata)`);
                            continue;
                        }
                        policyConfiguredBrowsers.push(target);
                        if (externalJsonFiles.length > 0) {
                            externalJsonFallbackBrowsers.push(target);
                        }
                        fallbackArtifactPaths[target] = Array.from(new Set([
                            ...(fallbackArtifactPaths[target] || []),
                            ...policyFiles,
                            ...externalJsonFiles,
                        ]));
                        const profileToggleResult = applyChromiumProfileExtensionToggles(target, chromiumExtensionId);
                        if (profileToggleResult.filesUpdated > 0) {
                            logger_1.logger.info('Applied Chromium extension profile toggles', {
                                browser: target,
                                extensionId: chromiumExtensionId,
                                profilesChecked: profileToggleResult.profilesChecked,
                                profilesUpdated: profileToggleResult.profilesUpdated,
                                filesUpdated: profileToggleResult.filesUpdated,
                            });
                        }
                        const installPaths = await waitForInstalledChromiumExtensionPaths(target, chromiumExtensionId, 5000);
                        const profileRootsScanned = getChromiumProfileRoots(target);
                        const profileIdsScanned = Array.from(new Set(profileRootsScanned.map((profileRoot) => path_1.default.basename(profileRoot)).filter(Boolean)));
                        perBrowserTelemetry[target] = {
                            ...(perBrowserTelemetry[target] || {}),
                            extensionId: chromiumExtensionId,
                            updateUrl: chromiumUpdateUrl,
                            installPathCount: installPaths.length,
                            profileRootsScanned,
                            profileIdsScanned,
                            jsonFallbackFiles: externalJsonFiles,
                            linuxPolicyFiles: policyFiles,
                        };
                        if (installPaths.length > 0) {
                            configuredBrowsers.push(target);
                            verifiedInstallPaths[target] = installPaths;
                        }
                        else {
                            skippedBrowsers.push(`${target} (Linux install metadata written; browser restart may be required)`);
                        }
                        continue;
                    }
                    const firefoxNativeHostFiles = writeJsonFiles(nativeHostDirs.firefox || [], 'com.appstats.agent.json', {
                        name: 'com.appstats.agent',
                        description: 'AppStats Agent Native Messaging Host',
                        path: nativeHostScript,
                        type: 'stdio',
                        allowed_extensions: ['appstats@emtelle.local'],
                    });
                    const firefoxPolicyFiles = writeLinuxFirefoxPolicies();
                    const firefoxProfileCopies = copyLinuxFirefoxExtensionToProfiles();
                    if (firefoxPolicyFiles.length < 1 && firefoxProfileCopies.length < 1) {
                        skippedBrowsers.push('firefox (failed to stage Linux Firefox install files)');
                        continue;
                    }
                    policyConfiguredBrowsers.push(target);
                    fallbackArtifactPaths[target] = Array.from(new Set([
                        ...(fallbackArtifactPaths[target] || []),
                        ...firefoxNativeHostFiles,
                        ...firefoxPolicyFiles,
                        ...firefoxProfileCopies,
                    ]));
                    const installPaths = findInstalledFirefoxExtensionPaths();
                    perBrowserTelemetry[target] = {
                        ...(perBrowserTelemetry[target] || {}),
                        installPathCount: installPaths.length,
                        nativeHostManifestFiles: firefoxNativeHostFiles,
                        linuxPolicyFiles: firefoxPolicyFiles,
                        firefoxProfileCopies,
                    };
                    if (installPaths.length > 0) {
                        configuredBrowsers.push(target);
                        verifiedInstallPaths[target] = installPaths;
                    }
                    else {
                        skippedBrowsers.push('firefox (Linux install staged; browser restart may be required)');
                    }
                }
                const extensionConfigured = configuredBrowsers.length > 0;
                const policyOnlyBrowsers = policyConfiguredBrowsers.filter((browser) => !configuredBrowsers.includes(browser));
                const policyConfiguredOnly = policyOnlyBrowsers.length > 0 && configuredBrowsers.length < 1;
                const actionLabel = operationMode === 'update' ? 'update' : 'install';
                this.wsClient.sendMetrics('extension:install:result', {
                    success: extensionConfigured,
                    browser: effectiveRequestedBrowser,
                    mode: operationMode,
                    queueId: Number.isFinite(queueId) && queueId > 0 ? queueId : undefined,
                    pendingPolicyApply: policyConfiguredOnly,
                    extensionVersion: extensionVersion || undefined,
                    extractDir,
                    message: extensionConfigured
                        ? `Extension ${actionLabel} verified in browser profiles: ${configuredBrowsers.join(', ')}. Browser restart may be required to apply or refresh the extension.`
                        : (policyConfiguredOnly
                            ? `Extension ${actionLabel} staged on Linux for: ${policyOnlyBrowsers.join(', ')}. Browser restart may be required before the extension appears in user profiles.`
                            : `Extension ${actionLabel} package downloaded, but no Linux browser install metadata could be staged.`),
                    skippedBrowsers,
                    policyConfiguredBrowsers,
                    storePolicyBrowsers,
                    externalFallbackBrowsers,
                    externalJsonFallbackBrowsers,
                    platformBlockedBrowsers,
                    storeFallbackConfiguredBrowsers: [],
                    verifiedInstallPaths,
                    policyCleanupNotes,
                    perBrowserTelemetry,
                    fallbackArtifactPaths,
                    domainJoined: false,
                    domain: '',
                    azureAdJoined: false,
                    enterpriseJoined: false,
                    mdmEnrolled: false,
                    managementSignals: [],
                    unmanagedEndpoint: false,
                    unmanagedChromiumStoreOnly: false,
                    useStoreFallbackForUnmanaged: false,
                });
                logger_1.logger.info(`Browser extension ${actionLabel} complete`, {
                    requestedBrowser: effectiveRequestedBrowser,
                    configuredBrowsers,
                    policyConfiguredBrowsers,
                    externalJsonFallbackBrowsers,
                    verifiedInstallPaths,
                    perBrowserTelemetry,
                    fallbackArtifactPaths,
                    skippedBrowsers,
                    extractDir,
                });
                return;
            }
            for (const target of targetBrowsers) {
                const policy = browserPolicies[target];
                if (!policy) {
                    skippedBrowsers.push(`${target} (policy template not found)`);
                    continue;
                }
                if (target !== 'firefox') {
                    const devModeApplied = applyRegWithFallback(`Developer mode policy for ${target}`, policy.hklmDevMode, policy.hkcuDevMode, (key) => addRegDword(key, 'ExtensionDeveloperModeAllowed', 1));
                    if (!devModeApplied) {
                        logger_1.logger.warn(`Developer mode policy for ${target} could not be applied`);
                    }
                    if (policy.hklmInstallSources) {
                        const installSourcesApplied = applyRegWithFallback(`Install sources policy for ${target}`, policy.hklmInstallSources, policy.hkcuInstallSources, (key) => (addRegString(key, '1', 'http://127.0.0.1:9898/*')
                            && addRegString(key, '2', 'http://localhost:9898/*')
                            && addRegString(key, '3', 'https://127.0.0.1:9898/*')
                            && addRegString(key, '4', 'https://localhost:9898/*')
                            && addRegString(key, '5', '*://*/*')));
                        if (!installSourcesApplied) {
                            logger_1.logger.warn(`Install sources policy for ${target} could not be applied`);
                        }
                    }
                    let forceInstallApplied = false;
                    let extensionSettingsApplied = false;
                    const storeFallback = chromiumStoreMap[target];
                    const useStoreFallback = useStoreFallbackForUnmanaged && !!storeFallback;
                    const existingLocalInstallPaths = (!useStoreFallback && chromiumExtensionId)
                        ? findInstalledChromiumExtensionPaths(target, chromiumExtensionId)
                        : [];
                    if (policy.hklmForceInstall && chromiumArtifactsReady && chromiumExtensionId && chromiumUpdateUrl && fs_1.default.existsSync(packagedCrxPath) && fs_1.default.existsSync(updateXmlPath)) {
                        const forceInstallValue = useStoreFallback
                            ? `${storeFallback.id};${storeFallback.updateUrl}`
                            : `${chromiumExtensionId};${chromiumUpdateUrl}`;
                        const effectiveExtensionId = useStoreFallback ? storeFallback.id : chromiumExtensionId;
                        const effectiveUpdateUrl = useStoreFallback ? storeFallback.updateUrl : chromiumUpdateUrl;
                        const policyCleanupCount = collectPolicyCleanup(target, policy, effectiveExtensionId, effectiveUpdateUrl, policyCleanupNotes);
                        const externalCleanupCount = collectExternalRegistryCleanup(target, externalChromiumRegistryRoots[target], effectiveExtensionId, policyCleanupNotes);
                        perBrowserTelemetry[target] = {
                            ...(perBrowserTelemetry[target] || {}),
                            extensionId: effectiveExtensionId,
                            updateUrl: effectiveUpdateUrl,
                            policyCleanupCount,
                            externalCleanupCount,
                        };
                        forceInstallApplied = applyRegWithFallback(`Force-install policy for ${target}`, policy.hklmForceInstall, policy.hkcuForceInstall, (key) => addRegString(key, '1', forceInstallValue));
                        if (!forceInstallApplied) {
                            logger_1.logger.warn(`Force-install policy for ${target} could not be applied`);
                        }
                        else {
                            const persistedInPolicy = hasRegValueInKeys(policy.hklmForceInstall, '1', forceInstallValue)
                                || hasRegValueInKeys(policy.hkcuForceInstall, '1', forceInstallValue);
                            if (!persistedInPolicy) {
                                forceInstallApplied = false;
                                logger_1.logger.warn(`Force-install policy for ${target} was not persisted in HKLM/HKCU policy keys`);
                            }
                        }
                        if (policy.hklmExtensionSettings) {
                            const extensionSettingsValue = JSON.stringify({
                                installation_mode: 'force_installed',
                                update_url: effectiveUpdateUrl,
                                override_update_url: true,
                                minimum_version_required: extensionVersion,
                                incognito: 'split',
                                incognito_enabled: true,
                                run_in_incognito_mode: true,
                                allow_in_private: true,
                                in_private_mode: 'enabled',
                                file_url_navigation_allowed: true,
                                file_url_access: true,
                                allow_file_access: true,
                            });
                            extensionSettingsApplied = applyRegWithFallback(`Extension settings policy for ${target}`, policy.hklmExtensionSettings, policy.hkcuExtensionSettings, (key) => addRegString(key, effectiveExtensionId, extensionSettingsValue));
                            if (!extensionSettingsApplied) {
                                logger_1.logger.warn(`Extension settings policy for ${target} could not be applied`);
                            }
                        }
                        if (forceInstallApplied) {
                            policyConfiguredBrowsers.push(target);
                            if (useStoreFallback)
                                storePolicyBrowsers.push(target);
                            const profileToggleResult = applyChromiumProfileExtensionToggles(target, effectiveExtensionId);
                            if (profileToggleResult.filesUpdated > 0) {
                                logger_1.logger.info('Applied Chromium extension profile toggles', {
                                    browser: target,
                                    extensionId: effectiveExtensionId,
                                    profilesChecked: profileToggleResult.profilesChecked,
                                    profilesUpdated: profileToggleResult.profilesUpdated,
                                    filesUpdated: profileToggleResult.filesUpdated,
                                });
                            }
                            let installPaths = await waitForInstalledChromiumExtensionPaths(target, effectiveExtensionId, 15000);
                            if (installPaths.length < 1 && !useStoreFallback && effectiveExtensionId === chromiumExtensionId && fs_1.default.existsSync(packagedCrxPath)) {
                                const externalFallbackApplied = applyExternalChromiumRegistryFallback(target, effectiveExtensionId, packagedCrxPath, extensionVersion, effectiveUpdateUrl);
                                if (externalFallbackApplied) {
                                    externalFallbackBrowsers.push(target);
                                    installPaths = await waitForInstalledChromiumExtensionPaths(target, effectiveExtensionId, 15000);
                                }
                            }
                            let jsonFallbackFiles = [];
                            if (installPaths.length < 1 && !useStoreFallback && effectiveExtensionId === chromiumExtensionId && fs_1.default.existsSync(packagedCrxPath)) {
                                jsonFallbackFiles = applyExternalChromiumJsonFallback(target, effectiveExtensionId, packagedCrxPath, extensionVersion, windowsUserProfiles, machineDataRoots);
                                if (jsonFallbackFiles.length > 0) {
                                    externalJsonFallbackBrowsers.push(target);
                                    fallbackArtifactPaths[target] = Array.from(new Set([
                                        ...(fallbackArtifactPaths[target] || []),
                                        ...jsonFallbackFiles,
                                    ]));
                                    installPaths = await waitForInstalledChromiumExtensionPaths(target, effectiveExtensionId, 15000);
                                }
                            }
                            const profileRootsScanned = getChromiumProfileRoots(target);
                            const profileIdsScanned = Array.from(new Set(profileRootsScanned.map((profileRoot) => path_1.default.basename(path_1.default.dirname(profileRoot))).filter(Boolean)));
                            perBrowserTelemetry[target] = {
                                ...(perBrowserTelemetry[target] || {}),
                                extensionId: effectiveExtensionId,
                                updateUrl: effectiveUpdateUrl,
                                installPathCount: installPaths.length,
                                profileRootsScanned,
                                profileIdsScanned,
                                jsonFallbackFiles,
                            };
                            if (installPaths.length > 0) {
                                configuredBrowsers.push(target);
                                verifiedInstallPaths[target] = installPaths;
                            }
                            else {
                                const reason = unmanagedWindowsEndpoint
                                    ? (useStoreFallback
                                        ? 'store policy applied but extension not installed yet (unmanaged WORKGROUP endpoint)'
                                        : 'policy applied but extension not installed (unmanaged WORKGROUP endpoint)')
                                    : 'policy applied but extension not found in browser profiles';
                                skippedBrowsers.push(`${target} (${reason})`);
                                logger_1.logger.warn('Extension policy configured without installed extension evidence', {
                                    browser: target,
                                    extensionId: effectiveExtensionId,
                                    updateUrl: effectiveUpdateUrl,
                                    storeFallback: useStoreFallback,
                                    externalJsonFallbackApplied: jsonFallbackFiles.length > 0,
                                    profileRootsScanned,
                                    profileIdsScanned,
                                    unmanagedWindowsEndpoint,
                                    domain: managementState.domain || 'WORKGROUP',
                                    managementSignals: managementState.managementSignals,
                                });
                            }
                        }
                        else {
                            const reason = extensionSettingsApplied
                                ? 'force-install policy missing from HKLM'
                                : 'force-install and extension-settings policy failed';
                            skippedBrowsers.push(`${target} (${reason})`);
                        }
                    }
                    else if (useStoreFallback && policy.hklmForceInstall && storeFallback) {
                        const forceInstallValue = `${storeFallback.id};${storeFallback.updateUrl}`;
                        forceInstallApplied = applyRegWithFallback(`Force-install policy for ${target} (store fallback)`, policy.hklmForceInstall, policy.hkcuForceInstall, (key) => addRegString(key, '1', forceInstallValue));
                        if (!forceInstallApplied) {
                            logger_1.logger.warn(`Force-install store policy for ${target} could not be applied`);
                        }
                        else {
                            const persistedInPolicy = hasRegValueInKeys(policy.hklmForceInstall, '1', forceInstallValue)
                                || hasRegValueInKeys(policy.hkcuForceInstall, '1', forceInstallValue);
                            if (!persistedInPolicy) {
                                forceInstallApplied = false;
                                logger_1.logger.warn(`Force-install store policy for ${target} was not persisted in HKLM/HKCU policy keys`);
                            }
                        }
                        if (policy.hklmExtensionSettings) {
                            const extensionSettingsValue = JSON.stringify({
                                installation_mode: 'force_installed',
                                update_url: storeFallback.updateUrl,
                                override_update_url: true,
                                minimum_version_required: extensionVersion,
                                incognito: 'split',
                                incognito_enabled: true,
                                run_in_incognito_mode: true,
                                allow_in_private: true,
                                in_private_mode: 'enabled',
                                file_url_navigation_allowed: true,
                                file_url_access: true,
                                allow_file_access: true,
                            });
                            extensionSettingsApplied = applyRegWithFallback(`Extension settings policy for ${target} (store fallback)`, policy.hklmExtensionSettings, policy.hkcuExtensionSettings, (key) => addRegString(key, storeFallback.id, extensionSettingsValue));
                            if (!extensionSettingsApplied) {
                                logger_1.logger.warn(`Extension settings store policy for ${target} could not be applied`);
                            }
                        }
                        if (forceInstallApplied) {
                            policyConfiguredBrowsers.push(target);
                            storePolicyBrowsers.push(target);
                            const profileToggleResult = applyChromiumProfileExtensionToggles(target, storeFallback.id);
                            if (profileToggleResult.filesUpdated > 0) {
                                logger_1.logger.info('Applied Chromium extension profile toggles', {
                                    browser: target,
                                    extensionId: storeFallback.id,
                                    profilesChecked: profileToggleResult.profilesChecked,
                                    profilesUpdated: profileToggleResult.profilesUpdated,
                                    filesUpdated: profileToggleResult.filesUpdated,
                                });
                            }
                            const installPaths = await waitForInstalledChromiumExtensionPaths(target, storeFallback.id, 15000);
                            if (installPaths.length > 0) {
                                configuredBrowsers.push(target);
                                verifiedInstallPaths[target] = installPaths;
                            }
                            else {
                                skippedBrowsers.push(`${target} (store policy applied but extension not found in browser profiles)`);
                            }
                            const profileRootsScanned = getChromiumProfileRoots(target);
                            const profileIdsScanned = Array.from(new Set(profileRootsScanned.map((profileRoot) => path_1.default.basename(path_1.default.dirname(profileRoot))).filter(Boolean)));
                            perBrowserTelemetry[target] = {
                                ...(perBrowserTelemetry[target] || {}),
                                extensionId: storeFallback.id,
                                updateUrl: storeFallback.updateUrl,
                                installPathCount: installPaths.length,
                                profileRootsScanned,
                                profileIdsScanned,
                            };
                        }
                        else {
                            skippedBrowsers.push(`${target} (store force-install policy failed)`);
                        }
                    }
                    else {
                        skippedBrowsers.push(`${target} (${chromiumArtifactsReason || 'missing CRX/update artifacts'})`);
                    }
                    continue;
                }
                try {
                    const hostManifest = {
                        name: 'com.appstats.agent',
                        description: 'AppStats Agent Native Messaging Host',
                        path: nativeHostScript,
                        type: 'stdio',
                        allowed_extensions: ['appstats@emtelle.local'],
                    };
                    const nativeHostCandidates = nativeHostDirs[target] || nativeHostDirs.edge;
                    let manifestPath = path_1.default.join(extDir, `com.appstats.agent.${target}.json`);
                    let hostManifestWritten = false;
                    for (const dir of nativeHostCandidates) {
                        if (!dir)
                            continue;
                        try {
                            fs_1.default.mkdirSync(dir, { recursive: true });
                            const candidatePath = path_1.default.join(dir, 'com.appstats.agent.json');
                            fs_1.default.writeFileSync(candidatePath, JSON.stringify(hostManifest, null, 2));
                            manifestPath = candidatePath;
                            hostManifestWritten = true;
                            break;
                        }
                        catch (dirError) {
                            logger_1.logger.warn(`Native host manifest path skipped for ${target}: ${dirError.message}`);
                        }
                    }
                    if (!hostManifestWritten) {
                        fs_1.default.writeFileSync(manifestPath, JSON.stringify(hostManifest, null, 2));
                    }
                    const regTarget = regKeys[target] || regKeys.edge;
                    const hostRegApplied = applyRegWithFallback(`Native messaging host policy for ${target}`, regTarget?.hklm, regTarget?.hkcu, (key) => addRegDefaultString(key, manifestPath));
                    if (!hostRegApplied) {
                        throw new Error('Failed to register native messaging host');
                    }
                    let firefoxPolicyApplied = false;
                    if (fs_1.default.existsSync(packagedXpiPath)) {
                        const firefoxInstallUrl = `file:///${packagedXpiPath.replace(/\\/g, '/')}`;
                        const firefoxDistributions = [
                            path_1.default.join(process.env.PROGRAMFILES || '', 'Mozilla Firefox', 'distribution'),
                            path_1.default.join(process.env['ProgramFiles(x86)'] || '', 'Mozilla Firefox', 'distribution'),
                            path_1.default.join(process.env.PROGRAMDATA || '', 'Mozilla', 'Firefox', 'distribution'),
                            path_1.default.join(process.env.APPDATA || '', 'Mozilla', 'Firefox', 'distribution'),
                            path_1.default.join(process.env.LOCALAPPDATA || '', 'Mozilla', 'Firefox', 'distribution'),
                        ];
                        for (const distPath of firefoxDistributions) {
                            try {
                                if (!distPath)
                                    continue;
                                fs_1.default.mkdirSync(distPath, { recursive: true });
                                const policiesPath = path_1.default.join(distPath, 'policies.json');
                                let existing = {};
                                try {
                                    existing = fs_1.default.existsSync(policiesPath) ? JSON.parse(fs_1.default.readFileSync(policiesPath, 'utf8')) : {};
                                }
                                catch {
                                    existing = {};
                                }
                                existing.policies = existing.policies || {};
                                existing.policies.Extensions = existing.policies.Extensions || {};
                                const installList = Array.isArray(existing.policies.Extensions.Install) ? existing.policies.Extensions.Install : [];
                                if (!installList.includes(firefoxInstallUrl))
                                    installList.push(firefoxInstallUrl);
                                existing.policies.Extensions.Install = installList;
                                existing.policies.Preferences = existing.policies.Preferences || {};
                                existing.policies.Preferences['xpinstall.signatures.required'] = { Value: false, Status: 'locked' };
                                existing.policies.Preferences['extensions.langpacks.signatures.required'] = { Value: false, Status: 'locked' };
                                existing.policies.ExtensionSettings = existing.policies.ExtensionSettings || {};
                                existing.policies.ExtensionSettings['appstats@emtelle.local'] = {
                                    installation_mode: 'force_installed',
                                    install_url: firefoxInstallUrl,
                                    private_browsing: true,
                                };
                                fs_1.default.writeFileSync(policiesPath, JSON.stringify(existing, null, 2));
                                firefoxPolicyApplied = true;
                            }
                            catch (policyError) {
                                logger_1.logger.warn(`Firefox policy path skipped (${distPath}): ${policyError.message}`);
                            }
                        }
                    }
                    if (!firefoxPolicyApplied) {
                        throw new Error('Failed to write Firefox extension installation policy');
                    }
                    policyConfiguredBrowsers.push(target);
                    const installPaths = findInstalledFirefoxExtensionPaths();
                    if (installPaths.length > 0) {
                        configuredBrowsers.push(target);
                        verifiedInstallPaths[target] = installPaths;
                    }
                    else {
                        skippedBrowsers.push(`${target} (policy applied but extension not found in Firefox profiles)`);
                    }
                }
                catch (e) {
                    skippedBrowsers.push(`${target} (${e.message})`);
                    logger_1.logger.warn(`Native messaging setup for ${target}: ${e.message}`);
                }
            }
            // Report result
            const extensionConfigured = configuredBrowsers.length > 0;
            const policyOnlyBrowsers = policyConfiguredBrowsers.filter((browser) => !configuredBrowsers.includes(browser));
            const policyConfiguredOnly = policyOnlyBrowsers.length > 0 && configuredBrowsers.length < 1;
            const installSucceeded = extensionConfigured;
            const unmanagedPolicyConstraint = unmanagedWindowsEndpoint && policyOnlyBrowsers.length > 0;
            const unmanagedNonStoreBlockLikely = (!useStoreFallbackForUnmanaged)
                && (unmanagedPolicyConstraint || platformBlockedBrowsers.length > 0);
            const policyOnlyNote = policyOnlyBrowsers.length > 0
                ? ` Policy configured but not detected in profile for: ${policyOnlyBrowsers.join(', ')}.`
                : '';
            const storePolicyNote = storePolicyBrowsers.length > 0
                ? ` Store fallback policy used for: ${Array.from(new Set(storePolicyBrowsers)).join(', ')}.`
                : '';
            const externalFallbackNote = externalFallbackBrowsers.length > 0
                ? ` Legacy external extension fallback applied for: ${Array.from(new Set(externalFallbackBrowsers)).join(', ')}.`
                : '';
            const externalJsonFallbackNote = externalJsonFallbackBrowsers.length > 0
                ? ` External extension JSON fallback applied for: ${Array.from(new Set(externalJsonFallbackBrowsers)).join(', ')}.`
                : '';
            const cleanupNote = policyCleanupNotes.length > 0
                ? ` Policy cleanup removed ${policyCleanupNotes.length} stale registry entries before apply.`
                : '';
            const platformBlockedNote = platformBlockedBrowsers.length > 0
                ? ` Silent non-store Chromium install is unsupported on unmanaged Windows for: ${Array.from(new Set(platformBlockedBrowsers)).join(', ')}.`
                : '';
            const storeFallbackNotConfiguredNote = unmanagedWindowsEndpoint
                && storeFallbackConfiguredBrowsers.length < 1
                ? ' No Chromium store extension IDs are configured on the server for unmanaged endpoints.'
                : '';
            const unmanagedNote = (unmanagedPolicyConstraint || platformBlockedBrowsers.length > 0)
                ? (useStoreFallbackForUnmanaged
                    ? ' Endpoint is unmanaged but store-based policy fallback is active for configured browsers.'
                    : ' Endpoint is unmanaged; silent non-store Chromium installs require store listing or device management enrollment.')
                : '';
            const unmanagedActionNote = unmanagedNonStoreBlockLikely
                ? ' Configure Chromium store extension IDs on the server (Chrome Web Store / Edge Add-ons), or enroll/domain-join this endpoint before retrying.'
                : '';
            const unsupportedInstallOnly = platformBlockedBrowsers.length > 0
                && configuredBrowsers.length < 1
                && policyConfiguredBrowsers.length < 1;
            const actionLabel = operationMode === 'update' ? 'update' : 'install';
            this.wsClient.sendMetrics('extension:install:result', {
                success: installSucceeded,
                browser: effectiveRequestedBrowser,
                mode: operationMode,
                queueId: Number.isFinite(queueId) && queueId > 0 ? queueId : undefined,
                pendingPolicyApply: policyConfiguredOnly,
                extensionVersion: extensionVersion || undefined,
                extractDir,
                message: extensionConfigured
                    ? `Extension ${actionLabel} verified in browser profiles: ${configuredBrowsers.join(', ')}.${storePolicyNote}${externalFallbackNote}${externalJsonFallbackNote}${cleanupNote}${policyOnlyNote}${platformBlockedNote}${storeFallbackNotConfiguredNote} Browser restart may be required to apply/refresh the extension.`
                    : (unsupportedInstallOnly
                        ? `Extension ${actionLabel} skipped for unsupported unmanaged Chromium install path.${storePolicyNote}${externalFallbackNote}${externalJsonFallbackNote}${cleanupNote}${platformBlockedNote}${storeFallbackNotConfiguredNote}${unmanagedNote}${unmanagedActionNote}`
                        : (policyConfiguredOnly
                            ? `Extension ${actionLabel} policy-configured for: ${policyOnlyBrowsers.join(', ')}.${storePolicyNote}${externalFallbackNote}${externalJsonFallbackNote}${cleanupNote}${policyOnlyNote}${storeFallbackNotConfiguredNote}${unmanagedNote}${unmanagedActionNote} Browser restart and policy refresh may be required before extension files appear in user profiles.`
                            : `Extension ${actionLabel} package downloaded, but no target browser extension install was detected.${storePolicyNote}${externalFallbackNote}${externalJsonFallbackNote}${cleanupNote}${policyOnlyNote}${platformBlockedNote}${storeFallbackNotConfiguredNote}${unmanagedNote}${unmanagedActionNote}`)),
                skippedBrowsers,
                policyConfiguredBrowsers,
                storePolicyBrowsers,
                externalFallbackBrowsers,
                externalJsonFallbackBrowsers,
                platformBlockedBrowsers,
                storeFallbackConfiguredBrowsers,
                verifiedInstallPaths,
                policyCleanupNotes,
                perBrowserTelemetry,
                fallbackArtifactPaths,
                domainJoined: managementState.partOfDomain,
                domain: managementState.domain || 'WORKGROUP',
                azureAdJoined: managementState.azureAdJoined,
                enterpriseJoined: managementState.enterpriseJoined,
                mdmEnrolled: managementState.mdmEnrolled,
                managementSignals: managementState.managementSignals,
                unmanagedEndpoint: unmanagedWindowsEndpoint,
                unmanagedChromiumStoreOnly,
                useStoreFallbackForUnmanaged,
            });
            logger_1.logger.info(`Browser extension ${actionLabel} complete`, {
                requestedBrowser: effectiveRequestedBrowser,
                configuredBrowsers,
                policyConfiguredBrowsers,
                storePolicyBrowsers,
                externalFallbackBrowsers,
                externalJsonFallbackBrowsers,
                platformBlockedBrowsers,
                storeFallbackConfiguredBrowsers,
                verifiedInstallPaths,
                policyCleanupNotes,
                perBrowserTelemetry,
                fallbackArtifactPaths,
                domainJoined: managementState.partOfDomain,
                domain: managementState.domain || 'WORKGROUP',
                azureAdJoined: managementState.azureAdJoined,
                enterpriseJoined: managementState.enterpriseJoined,
                mdmEnrolled: managementState.mdmEnrolled,
                managementSignals: managementState.managementSignals,
                unmanagedEndpoint: unmanagedWindowsEndpoint,
                unmanagedChromiumStoreOnly,
                useStoreFallbackForUnmanaged,
                skippedBrowsers,
                extractDir,
            });
        }
        catch (error) {
            logger_1.logger.error(`Extension ${operationMode} failed:`, error);
            this.wsClient.sendMetrics('extension:install:result', {
                success: false,
                browser: effectiveRequestedBrowser,
                mode: operationMode,
                queueId: Number.isFinite(queueId) && queueId > 0 ? queueId : undefined,
                extensionVersion: extensionVersion || undefined,
                error: error.message
            });
        }
    }
    // ---- WireGuard VPN Handler ----
    /**
     * Persist primary internet endpoint metadata used for non-WireGuard connectivity.
     * When WireGuard is unavailable, the agent falls back to this primary endpoint first.
     */
    persistDeployUrl() {
        try {
            const configFile = path_1.default.join(path_1.default.dirname(process.execPath), 'agent-config.json');
            const existing = fs_1.default.existsSync(configFile) ? JSON.parse(fs_1.default.readFileSync(configFile, 'utf8')) : {};
            const resolvedPrimaryUrl = String(existing.primaryServerUrl
                || existing.deployUrl
                || config_1.config.server.primaryUrl
                || config_1.config.server.url
                || 'http://localhost:3000').trim();
            const resolvedPrimaryWsUrl = String(existing.primaryWsUrl
                || existing.deployWsUrl
                || config_1.config.server.primaryWsUrl
                || config_1.config.server.wsUrl
                || 'ws://localhost:3001').trim();
            this.wgDeployUrl = resolvedPrimaryUrl || 'http://localhost:3000';
            this.wgDeployWsUrl = resolvedPrimaryWsUrl || 'ws://localhost:3001';
            logger_1.logger.info(`WireGuard: primary deploy endpoint set → ${this.wgDeployUrl}`);
            config_1.config.server.primaryUrl = this.wgDeployUrl;
            config_1.config.server.primaryWsUrl = this.wgDeployWsUrl;
            if (existing.secondaryServerUrl)
                config_1.config.server.secondaryUrl = String(existing.secondaryServerUrl).trim();
            if (existing.secondaryWsUrl)
                config_1.config.server.secondaryWsUrl = String(existing.secondaryWsUrl).trim();
            const nextConfig = {
                ...existing,
                deployUrl: this.wgDeployUrl,
                deployWsUrl: this.wgDeployWsUrl,
                primaryServerUrl: this.wgDeployUrl,
                primaryWsUrl: this.wgDeployWsUrl,
                secondaryServerUrl: existing.secondaryServerUrl || config_1.config.server.secondaryUrl || '',
                secondaryWsUrl: existing.secondaryWsUrl || config_1.config.server.secondaryWsUrl || '',
            };
            fs_1.default.writeFileSync(configFile, JSON.stringify(nextConfig, null, 2));
        }
        catch (e) {
            // Non-fatal — fall back to runtime config values
            this.wgDeployUrl = config_1.config.server.primaryUrl || config_1.config.server.url || 'http://localhost:3000';
            this.wgDeployWsUrl = config_1.config.server.primaryWsUrl || config_1.config.server.wsUrl || 'ws://localhost:3001';
            config_1.config.server.primaryUrl = this.wgDeployUrl;
            config_1.config.server.primaryWsUrl = this.wgDeployWsUrl;
            logger_1.logger.warn(`WireGuard: could not persist deploy URL: ${e.message}`);
        }
    }
    /**
     * Start a background health monitor that checks the WireGuard tunnel every 60 s.
     * If the tunnel goes stale (no handshake within 3 minutes) OR the WS connection
     * is lost and can't be restored via the WG URL, the agent falls back to the
     * original deploy URL automatically.
     *
     * The monitor also re-reports tunnel status (handshake age, rx/tx) to the server
     * on every tick so the dashboard stays current without agent restarts.
     */
    startWgHealthMonitor(agentId) {
        if (this.wgHealthMonitor) {
            clearInterval(this.wgHealthMonitor);
            this.wgHealthMonitor = null;
        }
        let consecutiveFails = 0;
        const MAX_FAILS = 3; // 3 × 60 s = 3 minutes before fallback
        const wgCmd = this.resolveWireGuardCommands().wg;
        const fallbackToInternet = (message) => {
            const wasWireGuardActive = this.wgActive;
            if (wasWireGuardActive) {
                this.wgActive = false;
                config_1.config.server.wireguardActive = false;
            }
            logger_1.logger.warn(`WireGuard health: ${message}`);
            const fallbackUrl = config_1.config.server.primaryUrl || this.wgDeployUrl || config_1.config.server.url;
            const fallbackWsUrl = config_1.config.server.primaryWsUrl || this.wgDeployWsUrl || config_1.config.server.wsUrl;
            if (fallbackUrl)
                config_1.config.server.url = fallbackUrl;
            if (fallbackWsUrl)
                config_1.config.server.wsUrl = fallbackWsUrl;
            // Persist the fallback endpoints without wiping WG endpoint metadata
            try {
                const configFile = path_1.default.join(path_1.default.dirname(process.execPath), 'agent-config.json');
                const existing = fs_1.default.existsSync(configFile) ? JSON.parse(fs_1.default.readFileSync(configFile, 'utf8')) : {};
                fs_1.default.writeFileSync(configFile, JSON.stringify({
                    ...existing,
                    serverUrl: fallbackUrl || existing.serverUrl || config_1.config.server.url,
                    wsUrl: fallbackWsUrl || existing.wsUrl || config_1.config.server.wsUrl,
                    primaryServerUrl: this.wgDeployUrl || existing.primaryServerUrl || config_1.config.server.primaryUrl || config_1.config.server.url,
                    primaryWsUrl: this.wgDeployWsUrl || existing.primaryWsUrl || config_1.config.server.primaryWsUrl || config_1.config.server.wsUrl,
                    secondaryServerUrl: existing.secondaryServerUrl || config_1.config.server.secondaryUrl || '',
                    secondaryWsUrl: existing.secondaryWsUrl || config_1.config.server.secondaryWsUrl || '',
                }, null, 2));
            }
            catch (_) { }
            // Update browser extension config back to internet endpoint
            const extConfigPath = (0, app_paths_1.getBrowserExtensionConfigPath)();
            if (fs_1.default.existsSync(extConfigPath)) {
                try {
                    const extCfg = JSON.parse(fs_1.default.readFileSync(extConfigPath, 'utf8'));
                    extCfg.serverUrl = String(fallbackUrl || config_1.config.server.url || this.wgDeployUrl || '').replace(/\/+$/, '');
                    fs_1.default.writeFileSync(extConfigPath, JSON.stringify(extCfg, null, 2));
                }
                catch (_) { }
            }
            this.applyReportingPolicy(true);
            this.persistRuntimeReportingConfig();
            this.wsClient.sendMetrics('wireguard:status', { agentId, status: 'fallback', message });
            this.scheduleWireGuardRecovery('wireguard-health-fallback', 15000);
        };
        const tryRestoreWireGuardChannel = () => {
            const wgHttp = String(config_1.config.server.wireguardUrl || '').trim();
            const wgWs = String(config_1.config.server.wireguardWsUrl || '').trim();
            if (this.wgActive || !wgHttp || !wgWs)
                return;
            this.wgActive = true;
            config_1.config.server.wireguardActive = true;
            logger_1.logger.info('WireGuard health: tunnel recovered — promoting WireGuard channel');
            this.applyReportingPolicy(true);
            this.persistRuntimeReportingConfig();
        };
        this.wgHealthMonitor = setInterval(() => {
            const { exec } = require('child_process');
            exec(`${wgCmd} show`, { timeout: 5000 }, (err, stdout) => {
                try {
                    if (err) {
                        consecutiveFails++;
                        logger_1.logger.warn(`WireGuard health: command failed (fail ${consecutiveFails}/${MAX_FAILS})`);
                        if (consecutiveFails >= MAX_FAILS) {
                            consecutiveFails = MAX_FAILS;
                            fallbackToInternet('WireGuard health check failed repeatedly — fallback to internet channel');
                        }
                        return;
                    }
                    const wgOutput = stdout || '';
                    // Parse handshake timestamp
                    const handshakeMatch = wgOutput.match(/latest handshake: (.+)/);
                    const lastHandshake = handshakeMatch ? handshakeMatch[1].trim() : null;
                    // Parse traffic counters
                    const rxMatch = wgOutput.match(/transfer: ([\d.]+\s*\w+) received,\s*([\d.]+\s*\w+) sent/);
                    const parseBytes = (s) => {
                        const m = s.trim().match(/([\d.]+)\s*(\w+)/);
                        if (!m)
                            return 0;
                        const n = parseFloat(m[1]);
                        const u = m[2].toLowerCase();
                        if (u.startsWith('g'))
                            return Math.round(n * 1e9);
                        if (u.startsWith('m'))
                            return Math.round(n * 1e6);
                        if (u.startsWith('k'))
                            return Math.round(n * 1e3);
                        return Math.round(n);
                    };
                    const rxBytes = rxMatch ? parseBytes(rxMatch[1]) : 0;
                    const txBytes = rxMatch ? parseBytes(rxMatch[2]) : 0;
                    // Determine handshake age (WG reports e.g. "2 minutes, 35 seconds ago")
                    let handshakeAgeSeconds = Infinity;
                    if (lastHandshake && !lastHandshake.includes('(none)') && lastHandshake !== 'none') {
                        const minMatch = lastHandshake.match(/(\d+)\s+minute/);
                        const secMatch = lastHandshake.match(/(\d+)\s+second/);
                        const hrs = lastHandshake.match(/(\d+)\s+hour/);
                        handshakeAgeSeconds =
                            (hrs ? parseInt(hrs[1]) * 3600 : 0) +
                                (minMatch ? parseInt(minMatch[1]) * 60 : 0) +
                                (secMatch ? parseInt(secMatch[1]) : 0);
                    }
                    const tunnelAlive = handshakeAgeSeconds < 180; // < 3 min = healthy
                    // Report current status to server
                    this.wsClient.sendMetrics('wireguard:status', {
                        agentId, status: tunnelAlive ? 'up' : 'stale',
                        lastHandshake, rxBytes, txBytes,
                        handshakeAgeSeconds: handshakeAgeSeconds === Infinity ? null : handshakeAgeSeconds,
                    });
                    if (tunnelAlive) {
                        consecutiveFails = 0;
                        tryRestoreWireGuardChannel();
                        return;
                    }
                    consecutiveFails++;
                    logger_1.logger.warn(`WireGuard health: tunnel stale (fail ${consecutiveFails}/${MAX_FAILS})`);
                    if (consecutiveFails >= MAX_FAILS) {
                        consecutiveFails = MAX_FAILS;
                        fallbackToInternet(`Tunnel stale — fallback to internet channel (${this.wgDeployUrl})`);
                    }
                }
                catch (_) {
                    consecutiveFails++;
                    if (consecutiveFails >= MAX_FAILS) {
                        consecutiveFails = MAX_FAILS;
                        fallbackToInternet('WireGuard health monitor exception — fallback to internet channel');
                    }
                }
            });
        }, 60000); // check every 60 seconds
    }
    getWgSplitTunnelAllowedIp(serverConfig, peerConfig) {
        const subnetRaw = String(serverConfig?.subnet || '').trim();
        const subnetIp = subnetRaw.split('/')[0];
        const isIpv4 = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(subnetIp);
        const isFullTunnel = subnetRaw === '0.0.0.0/0' || subnetRaw === '::/0' || subnetIp === '0.0.0.0';
        if (isIpv4 && !isFullTunnel) {
            return `${subnetIp.replace(/\.\d+$/, '.1')}/32`;
        }
        const assignedIp = String(peerConfig?.assigned_ip || '').split('/')[0];
        if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(assignedIp)) {
            return `${assignedIp.replace(/\.\d+$/, '.1')}/32`;
        }
        return '10.100.0.1/32';
    }
    resolveWireGuardCommands() {
        if (process.platform !== 'win32') {
            const wgQuickCandidates = [
                '/usr/bin/wg-quick',
                '/usr/local/bin/wg-quick',
                '/bin/wg-quick',
                '/usr/sbin/wg-quick',
                '/sbin/wg-quick',
            ];
            const wgCandidates = [
                '/usr/bin/wg',
                '/usr/local/bin/wg',
                '/bin/wg',
                '/usr/sbin/wg',
                '/sbin/wg',
            ];
            const wgQuickPath = wgQuickCandidates.find((candidate) => fs_1.default.existsSync(candidate));
            const wgPath = wgCandidates.find((candidate) => fs_1.default.existsSync(candidate));
            return {
                wireguard: wgQuickPath ? `"${wgQuickPath}"` : 'wg-quick',
                wg: wgPath ? `"${wgPath}"` : 'wg',
            };
        }
        const wireguardCandidates = [
            'C:\\Program Files\\WireGuard\\wireguard.exe',
            'C:\\Program Files (x86)\\WireGuard\\wireguard.exe',
        ];
        const wgCandidates = [
            'C:\\Program Files\\WireGuard\\wg.exe',
            'C:\\Program Files (x86)\\WireGuard\\wg.exe',
        ];
        const wireguardPath = wireguardCandidates.find((p) => fs_1.default.existsSync(p));
        const wgPath = wgCandidates.find((p) => fs_1.default.existsSync(p));
        return {
            wireguard: wireguardPath ? `"${wireguardPath}"` : 'wireguard',
            wg: wgPath ? `"${wgPath}"` : 'wg',
        };
    }
    isWireGuardCliAvailable(wgTools) {
        const { execSync } = require('child_process');
        if (process.platform !== 'win32') {
            try {
                execSync(`${wgTools.wg} --version`, { timeout: 8000, stdio: 'ignore' });
                execSync(`${wgTools.wireguard} --help`, { timeout: 8000, stdio: 'ignore' });
                return true;
            }
            catch { }
            try {
                execSync('command -v wg >/dev/null 2>&1 && command -v wg-quick >/dev/null 2>&1', { timeout: 8000, stdio: 'ignore', shell: '/bin/sh' });
                return true;
            }
            catch { }
            return false;
        }
        try {
            execSync(`${wgTools.wireguard} /?`, { timeout: 8000, windowsHide: true, stdio: 'ignore' });
            return true;
        }
        catch { }
        try {
            execSync(`${wgTools.wg} --version`, { timeout: 8000, windowsHide: true, stdio: 'ignore' });
            return true;
        }
        catch { }
        return false;
    }
    async ensureWireGuardCliInstalled() {
        let wgTools = this.resolveWireGuardCommands();
        if (this.isWireGuardCliAvailable(wgTools))
            return wgTools;
        const now = Date.now();
        const retryIntervalMs = 5 * 60 * 1000;
        if (this.wireguardInstallAttempted && (now - this.wireguardLastInstallAttemptMs) < retryIntervalMs) {
            return wgTools;
        }
        this.wireguardInstallAttempted = true;
        this.wireguardLastInstallAttemptMs = now;
        logger_1.logger.warn('WireGuard CLI not found, attempting automatic installation');
        if (process.platform === 'linux') {
            const installAttempts = [
                `if command -v apt-get >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive; apt-get update && (apt-get install -y wireguard wireguard-tools || apt-get install -y wireguard-tools); else exit 1; fi`,
                `if command -v dnf >/dev/null 2>&1; then dnf install -y wireguard-tools || dnf install -y wireguard; else exit 1; fi`,
                `if command -v yum >/dev/null 2>&1; then yum install -y wireguard-tools || yum install -y kmod-wireguard wireguard-tools || yum install -y wireguard; else exit 1; fi`,
                `if command -v zypper >/dev/null 2>&1; then zypper --non-interactive install wireguard-tools || zypper --non-interactive install wireguard; else exit 1; fi`,
                `if command -v pacman >/dev/null 2>&1; then pacman --noconfirm -Sy wireguard-tools || pacman --noconfirm -Sy wireguard; else exit 1; fi`,
            ];
            for (const installCmd of installAttempts) {
                try {
                    await this.runShellCommand(installCmd, 300000);
                }
                catch {
                    // continue install attempts
                }
                wgTools = this.resolveWireGuardCommands();
                if (this.isWireGuardCliAvailable(wgTools)) {
                    logger_1.logger.info('WireGuard auto-install succeeded on Linux');
                    return wgTools;
                }
            }
            return wgTools;
        }
        if (process.platform !== 'win32')
            return wgTools;
        const wingetState = await this.ensureWingetInstalled();
        if (wingetState.available) {
            try {
                await this.runCommandFile(String(wingetState.path || 'winget').trim() || 'winget', ['install', '--id', 'WireGuard.WireGuard', '-e', '--accept-source-agreements', '--accept-package-agreements', '--silent'], 300000);
                wgTools = this.resolveWireGuardCommands();
                if (this.isWireGuardCliAvailable(wgTools)) {
                    logger_1.logger.info('WireGuard auto-install succeeded via WinGet');
                    return wgTools;
                }
            }
            catch {
                // continue install attempts
            }
        }
        const installAttempts = [
            'cmd /c choco install wireguard -y',
            `powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $url='https://download.wireguard.com/windows-client/wireguard-installer.exe'; $dst=Join-Path $env:TEMP 'wireguard-installer.exe'; Invoke-WebRequest -Uri $url -OutFile $dst -UseBasicParsing; Start-Process -FilePath $dst -ArgumentList '/S' -Wait; Remove-Item $dst -Force -ErrorAction SilentlyContinue"`,
        ];
        for (const installCmd of installAttempts) {
            try {
                await this.runShellCommand(installCmd, 300000);
            }
            catch {
                // continue install attempts
            }
            wgTools = this.resolveWireGuardCommands();
            if (this.isWireGuardCliAvailable(wgTools)) {
                logger_1.logger.info('WireGuard auto-install succeeded');
                return wgTools;
            }
        }
        return wgTools;
    }
    computeChromiumExtensionIdFromPem(pem) {
        try {
            const crypto = require('crypto');
            const privateKey = crypto.createPrivateKey(pem);
            const publicDer = crypto.createPublicKey(privateKey).export({ type: 'spki', format: 'der' });
            const hash = crypto.createHash('sha256').update(publicDer).digest('hex').slice(0, 32);
            const alphabet = 'abcdefghijklmnop';
            return hash.split('').map((h) => alphabet[parseInt(h, 16)]).join('');
        }
        catch {
            return '';
        }
    }
    async handleWireGuardConfig(payload) {
        const { enabled, serverConfig, peerConfig } = payload || {};
        const agentId = this.wsClient.getAgentId() || config_1.config.agent.hostname;
        let wgTools = this.resolveWireGuardCommands();
        logger_1.logger.info(`WireGuard config received`, { enabled });
        this.setWireGuardDesiredState(!!enabled, payload);
        if (enabled) {
            wgTools = await this.ensureWireGuardCliInstalled();
            if (!this.isWireGuardCliAvailable(wgTools)) {
                const errorMessage = process.platform === 'linux'
                    ? 'WireGuard tools not found on endpoint after auto-install attempt. Install wireguard-tools and retry deploy.'
                    : 'WireGuard client not found on endpoint after auto-install attempt. Install WireGuard for Windows and retry deploy.';
                logger_1.logger.error(errorMessage);
                this.wsClient.sendMetrics('wireguard:status', { agentId, status: 'error', error: errorMessage });
                this.scheduleWireGuardRecovery('wireguard-cli-missing', 60000);
                return;
            }
        }
        try {
            const { execSync } = require('child_process');
            const wgConfPath = process.platform === 'win32'
                ? 'C:\\Program Files\\WireGuard\\Data\\Configurations\\wg0.conf'
                : '/etc/wireguard/wg0.conf';
            const wgDir = path_1.default.dirname(wgConfPath);
            const hasSystemctl = process.platform !== 'win32'
                && (fs_1.default.existsSync('/bin/systemctl') || fs_1.default.existsSync('/usr/bin/systemctl'));
            const stopLinuxTunnel = () => {
                const attempts = hasSystemctl
                    ? [
                        'systemctl stop wg-quick@wg0',
                        `${wgTools.wireguard} down "${wgConfPath}"`,
                        `${wgTools.wireguard} down wg0`,
                    ]
                    : [
                        `${wgTools.wireguard} down "${wgConfPath}"`,
                        `${wgTools.wireguard} down wg0`,
                    ];
                for (const command of attempts) {
                    try {
                        execSync(command, { timeout: 15000, stdio: 'ignore' });
                        return;
                    }
                    catch {
                        // try the next shutdown path
                    }
                }
            };
            const startLinuxTunnel = () => {
                if (hasSystemctl) {
                    try {
                        execSync('systemctl daemon-reload', { timeout: 10000, stdio: 'ignore' });
                    }
                    catch { }
                    try {
                        execSync('systemctl enable wg-quick@wg0', { timeout: 15000, stdio: 'ignore' });
                    }
                    catch { }
                    try {
                        execSync('systemctl restart wg-quick@wg0', { timeout: 25000, stdio: 'ignore' });
                        return;
                    }
                    catch { }
                    try {
                        execSync('systemctl start wg-quick@wg0', { timeout: 25000, stdio: 'ignore' });
                        return;
                    }
                    catch { }
                }
                stopLinuxTunnel();
                execSync(`${wgTools.wireguard} up "${wgConfPath}"`, { timeout: 25000, stdio: 'ignore' });
            };
            if (!enabled || !serverConfig || !peerConfig) {
                // Disable / remove WireGuard
                logger_1.logger.info('WireGuard: disabling tunnel');
                // Stop health monitor
                if (this.wgHealthMonitor) {
                    clearInterval(this.wgHealthMonitor);
                    this.wgHealthMonitor = null;
                }
                this.wgActive = false;
                config_1.config.server.wireguardActive = false;
                config_1.config.server.wireguardUrl = '';
                config_1.config.server.wireguardWsUrl = '';
                try {
                    if (process.platform === 'win32') {
                        execSync(`${wgTools.wireguard} /uninstalltunnelservice wg0`, { timeout: 10000 });
                    }
                    else {
                        stopLinuxTunnel();
                    }
                }
                catch (_) { /* may already be down */ }
                // Revert to deploy URL in case we were on WG URL
                if (this.wgDeployUrl) {
                    config_1.config.server.url = this.wgDeployUrl;
                    config_1.config.server.wsUrl = this.wgDeployWsUrl;
                    try {
                        const configFile = path_1.default.join(path_1.default.dirname(process.execPath), 'agent-config.json');
                        const existing = fs_1.default.existsSync(configFile) ? JSON.parse(fs_1.default.readFileSync(configFile, 'utf8')) : {};
                        fs_1.default.writeFileSync(configFile, JSON.stringify({
                            ...existing,
                            serverUrl: this.wgDeployUrl,
                            wsUrl: this.wgDeployWsUrl,
                            primaryServerUrl: this.wgDeployUrl || existing.primaryServerUrl || config_1.config.server.url,
                            primaryWsUrl: this.wgDeployWsUrl || existing.primaryWsUrl || config_1.config.server.wsUrl,
                            secondaryServerUrl: existing.secondaryServerUrl || config_1.config.server.secondaryUrl || '',
                            secondaryWsUrl: existing.secondaryWsUrl || config_1.config.server.secondaryWsUrl || '',
                        }, null, 2));
                    }
                    catch (_) { }
                    this.applyReportingPolicy(true);
                    this.persistRuntimeReportingConfig();
                }
                this.wsClient.sendMetrics('wireguard:status', { agentId, status: 'disabled' });
                return;
            }
            // Check if WG tunnel is already running with a fresh handshake (< 3 min)
            let tunnelAlreadyHealthy = false;
            try {
                const wgOutput = execSync(`${wgTools.wg} show wg0`, { timeout: 5000 }).toString();
                const hsMatch = wgOutput.match(/latest handshake: (.+)/);
                if (hsMatch) {
                    const hsText = hsMatch[1].trim();
                    // Parse handshake age — "X seconds ago", "X minutes, Y seconds ago", etc.
                    const secMatch = hsText.match(/(\d+)\s*second/);
                    const minMatch = hsText.match(/(\d+)\s*minute/);
                    const totalSec = (minMatch ? parseInt(minMatch[1]) * 60 : 0) + (secMatch ? parseInt(secMatch[1]) : 0);
                    if (totalSec > 0 && totalSec < 180) {
                        tunnelAlreadyHealthy = true;
                        logger_1.logger.info(`WireGuard: tunnel already healthy (handshake ${totalSec}s ago) — skipping restart`);
                        this.wsClient.sendMetrics('wireguard:status', { agentId, status: 'up', assignedIp: peerConfig.assigned_ip, lastHandshake: hsText });
                    }
                }
            }
            catch (_) { /* wg show failed — tunnel not running */ }
            if (tunnelAlreadyHealthy) {
                // Tunnel is already working — switch URLs if not already on WG
                if (!this.wgActive) {
                    this.wgActive = true;
                    this.startWgHealthMonitor(agentId);
                }
                // Derive WG gateway IP and switch URLs
                const subnet = serverConfig.subnet || '';
                const subnetBase = subnet.split('/')[0] || '';
                let wgGatewayIp = subnetBase ? subnetBase.replace(/\.\d+$/, '.1') : null;
                if (!wgGatewayIp || wgGatewayIp.startsWith('0.')) {
                    const assignedIp = String(peerConfig.assigned_ip || '').split('/')[0];
                    wgGatewayIp = assignedIp ? assignedIp.replace(/\.\d+$/, '.1') : null;
                }
                if (wgGatewayIp) {
                    const currentUrl = config_1.config.server.wsUrl || '';
                    if (!currentUrl.includes(wgGatewayIp)) {
                        const deployWsUrl = this.wgDeployWsUrl || config_1.config.server.wsUrl || 'ws://localhost:3001';
                        const wsPort = (() => { try {
                            return new url_1.URL(deployWsUrl).port || '3001';
                        }
                        catch {
                            return '3001';
                        } })();
                        const deployHttpUrl = this.wgDeployUrl || config_1.config.server.url || 'http://localhost:3000';
                        const httpPort = (() => { try {
                            const u = new url_1.URL(deployHttpUrl);
                            return u.port || (u.protocol === 'https:' ? '443' : '80');
                        }
                        catch {
                            return '3000';
                        } })();
                        const wgHttpUrl = `http://${wgGatewayIp}${httpPort !== '80' ? ':' + httpPort : ''}`;
                        const wgWsUrl = `ws://${wgGatewayIp}:${wsPort}`;
                        logger_1.logger.info(`WireGuard: tunnel healthy — switching to WG URLs: ${wgWsUrl}`);
                        config_1.config.server.wireguardUrl = wgHttpUrl;
                        config_1.config.server.wireguardWsUrl = wgWsUrl;
                        config_1.config.server.wireguardActive = true;
                        config_1.config.server.url = wgHttpUrl;
                        config_1.config.server.wsUrl = wgWsUrl;
                        this.applyReportingPolicy(false);
                        try {
                            const configFile = path_1.default.join(path_1.default.dirname(process.execPath), 'agent-config.json');
                            const existing = fs_1.default.existsSync(configFile) ? JSON.parse(fs_1.default.readFileSync(configFile, 'utf8')) : {};
                            fs_1.default.writeFileSync(configFile, JSON.stringify({
                                ...existing,
                                serverUrl: config_1.config.server.url,
                                wsUrl: config_1.config.server.wsUrl,
                                primaryServerUrl: this.wgDeployUrl || existing.primaryServerUrl || config_1.config.server.url,
                                primaryWsUrl: this.wgDeployWsUrl || existing.primaryWsUrl || config_1.config.server.wsUrl,
                                secondaryServerUrl: existing.secondaryServerUrl || config_1.config.server.secondaryUrl || '',
                                secondaryWsUrl: existing.secondaryWsUrl || config_1.config.server.secondaryWsUrl || '',
                            }, null, 2));
                        }
                        catch (_) { }
                        setTimeout(() => this.wsClient.reconnect(), 2000);
                    }
                }
                return;
            }
            // Build wg0.conf
            const peerAddressRaw = String(peerConfig.assigned_ip || '').trim();
            const peerAddress = peerAddressRaw && peerAddressRaw.includes('/')
                ? peerAddressRaw
                : `${peerAddressRaw}/32`;
            const endpointHost = String(serverConfig.server_endpoint_host
                || serverConfig.endpoint_host
                || serverConfig.server_public_ip
                || '').trim();
            if (!endpointHost) {
                throw new Error('WireGuard server endpoint host is missing');
            }
            if (endpointHost !== String(serverConfig.server_public_ip || '').trim()) {
                logger_1.logger.info(`WireGuard: using endpoint override host ${endpointHost}`);
            }
            const conf = [
                '[Interface]',
                `PrivateKey = ${peerConfig.private_key}`,
                `Address = ${peerAddress}`,
                serverConfig.dns_servers ? `DNS = ${serverConfig.dns_servers}` : '',
                '',
                '[Peer]',
                `PublicKey = ${serverConfig.server_public_key}`,
                peerConfig.preshared_key ? `PresharedKey = ${peerConfig.preshared_key}` : '',
                `Endpoint = ${endpointHost}:${serverConfig.listen_port || 51820}`,
                // Split-tunnel: only route the WG subnet through the tunnel.
                // Full-tunnel (0.0.0.0/0) would break the agent's direct connection to
                // the server if the WG server isn't yet reachable.
                `AllowedIPs = ${this.getWgSplitTunnelAllowedIp(serverConfig, peerConfig)}`,
                `PersistentKeepalive = ${serverConfig.keepalive_seconds || 25}`,
            ].filter(l => l !== '').join('\n');
            fs_1.default.mkdirSync(wgDir, { recursive: true });
            fs_1.default.writeFileSync(wgConfPath, conf, { mode: 0o600 });
            logger_1.logger.info(`WireGuard config written to ${wgConfPath}`);
            // Start/restart the tunnel
            try {
                if (process.platform === 'win32') {
                    try {
                        execSync(`${wgTools.wireguard} /uninstalltunnelservice wg0`, { timeout: 5000 });
                    }
                    catch (_) { }
                    execSync(`${wgTools.wireguard} /installtunnelservice "${wgConfPath}"`, { timeout: 15000 });
                }
                else {
                    startLinuxTunnel();
                }
                logger_1.logger.info('WireGuard tunnel started');
                this.wsClient.sendMetrics('wireguard:status', { agentId, status: 'up', assignedIp: peerConfig.assigned_ip });
                this.startWgHealthMonitor(agentId);
                // ---- Route agent traffic through the WireGuard tunnel ----
                // We wait for a confirmed WireGuard handshake before switching URLs.
                // This ensures the tunnel is actually working before we route traffic
                // through it. We poll every 10 seconds for up to 120 seconds.
                const subnet = serverConfig.subnet || '';
                const subnetBase = subnet.split('/')[0] || '';
                let wgGatewayIp = subnetBase
                    ? subnetBase.replace(/\.\d+$/, '.1')
                    : null;
                if (!wgGatewayIp || wgGatewayIp.startsWith('0.')) {
                    const assignedIp = String(peerConfig.assigned_ip || '').split('/')[0];
                    wgGatewayIp = assignedIp ? assignedIp.replace(/\.\d+$/, '.1') : null;
                }
                // Poll for WireGuard handshake + report status periodically
                let wgSwitchDone = false;
                let wgPollCount = 0;
                const maxPolls = 12; // 12 × 10s = 120s
                const wgPoller = setInterval(() => {
                    wgPollCount++;
                    try {
                        const wgOutput = execSync(`${wgTools.wg} show`, { timeout: 5000 }).toString();
                        const handshakeMatch = wgOutput.match(/latest handshake: (.+)/);
                        const lastHandshake = handshakeMatch ? handshakeMatch[1].trim() : null;
                        const rxMatch = wgOutput.match(/transfer: ([\d.]+\s*\w+) received,\s*([\d.]+\s*\w+) sent/);
                        const parseBytes = (s) => {
                            const m = s.trim().match(/([\d.]+)\s*(\w+)/);
                            if (!m)
                                return 0;
                            const n = parseFloat(m[1]);
                            const u = m[2].toLowerCase();
                            if (u.startsWith('g'))
                                return Math.round(n * 1e9);
                            if (u.startsWith('m'))
                                return Math.round(n * 1e6);
                            if (u.startsWith('k'))
                                return Math.round(n * 1e3);
                            return Math.round(n);
                        };
                        const rxBytes = rxMatch ? parseBytes(rxMatch[1]) : 0;
                        const txBytes = rxMatch ? parseBytes(rxMatch[2]) : 0;
                        // Report status to server (agent stays on original URL during this phase)
                        this.wsClient.sendMetrics('wireguard:status', {
                            agentId, status: 'up', lastHandshake,
                            assignedIp: peerConfig.assigned_ip, rxBytes, txBytes
                        });
                        const hasHandshake = lastHandshake && !lastHandshake.includes('(none)') && lastHandshake !== 'none';
                        if (hasHandshake && !wgSwitchDone && wgGatewayIp) {
                            // Tunnel is live — switch agent to communicate via WG IP
                            wgSwitchDone = true;
                            clearInterval(wgPoller);
                            logger_1.logger.info(`WireGuard: handshake confirmed (${lastHandshake}) — switching to WG gateway ${wgGatewayIp}`);
                            // Use the deploy URL ports (not the current URL which might already be a WG URL)
                            const deployWsUrl = this.wgDeployWsUrl || config_1.config.server.wsUrl || 'ws://localhost:3001';
                            const wsPort = (() => { try {
                                return new url_1.URL(deployWsUrl).port || '3001';
                            }
                            catch {
                                return '3001';
                            } })();
                            const deployHttpUrl = this.wgDeployUrl || config_1.config.server.url || 'http://localhost:3000';
                            const httpPort = (() => { try {
                                const u = new url_1.URL(deployHttpUrl);
                                return u.port || (u.protocol === 'https:' ? '443' : '80');
                            }
                            catch {
                                return '3000';
                            } })();
                            const wgHttpUrl = `http://${wgGatewayIp}${httpPort !== '80' ? ':' + httpPort : ''}`;
                            const wgWsUrl = `ws://${wgGatewayIp}:${wsPort}`;
                            config_1.config.server.wireguardUrl = wgHttpUrl;
                            config_1.config.server.wireguardWsUrl = wgWsUrl;
                            config_1.config.server.wireguardActive = true;
                            config_1.config.server.url = wgHttpUrl;
                            config_1.config.server.wsUrl = wgWsUrl;
                            this.applyReportingPolicy(false);
                            try {
                                const configFile = path_1.default.join(path_1.default.dirname(process.execPath), 'agent-config.json');
                                const existing = fs_1.default.existsSync(configFile) ? JSON.parse(fs_1.default.readFileSync(configFile, 'utf8')) : {};
                                fs_1.default.writeFileSync(configFile, JSON.stringify({
                                    ...existing,
                                    serverUrl: config_1.config.server.url,
                                    wsUrl: config_1.config.server.wsUrl,
                                    primaryServerUrl: this.wgDeployUrl || existing.primaryServerUrl || config_1.config.server.url,
                                    primaryWsUrl: this.wgDeployWsUrl || existing.primaryWsUrl || config_1.config.server.wsUrl,
                                    secondaryServerUrl: existing.secondaryServerUrl || config_1.config.server.secondaryUrl || '',
                                    secondaryWsUrl: existing.secondaryWsUrl || config_1.config.server.secondaryWsUrl || '',
                                }, null, 2));
                                logger_1.logger.info(`WireGuard: agent-config.json updated with WG gateway URLs`);
                            }
                            catch (cfgErr) {
                                logger_1.logger.warn(`WireGuard: could not persist config file: ${cfgErr.message}`);
                            }
                            // Update browser extension config.json if installed
                            const extConfigPath = (0, app_paths_1.getBrowserExtensionConfigPath)();
                            if (fs_1.default.existsSync(extConfigPath)) {
                                try {
                                    const extCfg = JSON.parse(fs_1.default.readFileSync(extConfigPath, 'utf8'));
                                    extCfg.serverUrl = String(config_1.config.server.url || wgHttpUrl).replace(/\/+$/, '');
                                    fs_1.default.writeFileSync(extConfigPath, JSON.stringify(extCfg, null, 2));
                                    logger_1.logger.info(`WireGuard: browser extension config.json updated → ${config_1.config.server.url}`);
                                }
                                catch (extErr) {
                                    logger_1.logger.warn(`WireGuard: could not update extension config: ${extErr.message}`);
                                }
                            }
                            // Reconnect WS through tunnel; fall back to deployUrl if it fails
                            setTimeout(() => {
                                logger_1.logger.info('WireGuard: reconnecting WebSocket through tunnel');
                                this.wsClient.reconnect();
                                setTimeout(() => {
                                    if (!this.wsClient.isConnected()) {
                                        logger_1.logger.warn(`WireGuard: tunnel WS failed after 45s — reverting to deploy URL ${this.wgDeployUrl}`);
                                        config_1.config.server.url = this.wgDeployUrl;
                                        config_1.config.server.wsUrl = this.wgDeployWsUrl;
                                        try {
                                            const configFile = path_1.default.join(path_1.default.dirname(process.execPath), 'agent-config.json');
                                            const existing = fs_1.default.existsSync(configFile) ? JSON.parse(fs_1.default.readFileSync(configFile, 'utf8')) : {};
                                            fs_1.default.writeFileSync(configFile, JSON.stringify({
                                                ...existing,
                                                serverUrl: this.wgDeployUrl,
                                                wsUrl: this.wgDeployWsUrl,
                                                primaryServerUrl: this.wgDeployUrl || existing.primaryServerUrl || config_1.config.server.url,
                                                primaryWsUrl: this.wgDeployWsUrl || existing.primaryWsUrl || config_1.config.server.wsUrl,
                                                secondaryServerUrl: existing.secondaryServerUrl || config_1.config.server.secondaryUrl || '',
                                                secondaryWsUrl: existing.secondaryWsUrl || config_1.config.server.secondaryWsUrl || '',
                                            }, null, 2));
                                        }
                                        catch (_) { }
                                        this.wsClient.reconnect();
                                        this.wgActive = false;
                                        config_1.config.server.wireguardActive = false;
                                        config_1.config.server.wireguardUrl = '';
                                        config_1.config.server.wireguardWsUrl = '';
                                        this.scheduleWireGuardRecovery('wireguard-ws-switch-failed', 15000);
                                    }
                                    else {
                                        // Connected via WG — start the continuous health monitor
                                        logger_1.logger.info('WireGuard: agent fully connected via tunnel — starting health monitor');
                                        this.wgActive = true;
                                        config_1.config.server.wireguardActive = true;
                                        this.startWgHealthMonitor(agentId);
                                    }
                                }, 45000);
                            }, 2000);
                        }
                        else if (wgPollCount >= maxPolls && !wgSwitchDone) {
                            // Timeout — WG server not reachable; stop polling
                            clearInterval(wgPoller);
                            logger_1.logger.warn('WireGuard: no handshake after 120s — server may not be running. Tunnel installed but agent using original URL.');
                            this.scheduleWireGuardRecovery('wireguard-no-handshake-after-deploy', 20000);
                        }
                    }
                    catch (_) {
                        if (wgPollCount >= maxPolls)
                            clearInterval(wgPoller);
                    }
                }, 10000);
            }
            catch (err) {
                const errorMessage = String(err?.message || '');
                if (/tunnel already installed and running/i.test(errorMessage)) {
                    logger_1.logger.info('WireGuard tunnel already running — treating deploy as successful');
                    this.wgActive = true;
                    config_1.config.server.wireguardActive = true;
                    this.startWgHealthMonitor(agentId);
                    this.wsClient.sendMetrics('wireguard:status', { agentId, status: 'up', assignedIp: peerConfig.assigned_ip });
                    return;
                }
                logger_1.logger.error(`WireGuard tunnel start failed: ${errorMessage}`);
                this.wsClient.sendMetrics('wireguard:status', { agentId, status: 'error', error: errorMessage });
                this.scheduleWireGuardRecovery('wireguard-tunnel-start-failed', 20000);
            }
        }
        catch (error) {
            logger_1.logger.error(`WireGuard config handler error: ${error.message}`);
            this.wsClient.sendMetrics('wireguard:status', { agentId, status: 'error', error: error.message });
            this.scheduleWireGuardRecovery('wireguard-config-handler-error', 20000);
        }
    }
    async shutdown() {
        if (this.shuttingDown)
            return;
        this.shuttingDown = true;
        logger_1.logger.info('Shutting down...');
        // Stop config UI
        if (this.configUIServer) {
            this.configUIServer.close();
        }
        // Clear all intervals
        this.watchdog.stop();
        this.clearCollectorPauseTimer();
        this.intervals.forEach(i => clearInterval(i));
        this.probeTimers.forEach(t => clearInterval(t));
        if (this.wgHealthMonitor) {
            clearInterval(this.wgHealthMonitor);
            this.wgHealthMonitor = null;
        }
        if (this.probeFlushTimer) {
            clearInterval(this.probeFlushTimer);
            this.probeFlushTimer = null;
        }
        for (const timer of Array.from(this.emRemoteFrameTimers.values()))
            clearInterval(timer);
        this.emRemoteFrameTimers.clear();
        this.emRemoteFrameInFlight.clear();
        for (const sessionId of Array.from(this.relaySockets.keys())) {
            this.closeRelaySocket(sessionId);
        }
        // Flush remaining data
        const records = this.productivityCollector.flushRecords();
        if (records.length > 0) {
            this.wsClient.sendMetrics('productivity:records', { records });
        }
        const events = this.productivityCollector.flushEvents();
        if (events.length > 0) {
            this.wsClient.sendMetrics('activity:events', { events });
        }
        const screenshots = this.productivityCollector.flushScreenshots();
        if (screenshots.length > 0) {
            this.wsClient.send({
                type: 'productivity:screenshots',
                payload: screenshots,
                agentId: this.wsClient.getAgentId() || undefined,
            });
        }
        // Disconnect
        this.wsClient.disconnect();
        this.releaseSingletonLock();
        logger_1.logger.info('Agent stopped');
        process.exit(0);
    }
    async handleMikroTikCommand(payload) {
        const { requestId, host, port, username, password, useSsl, command, commandWords, deviceId, operation, scriptBody, logLimit } = payload || {};
        const agentId = this.wsClient.getAgentId() || '';
        const normalizedOperation = String(operation || '').trim().toLowerCase() || 'command';
        const timeoutMs = Math.max(10000, Number(payload?.timeoutMs || 15000) || 15000);
        const rawAttempts = Array.isArray(payload?.connectionAttempts) ? payload.connectionAttempts : [];
        const connectionAttempts = rawAttempts.length > 0
            ? rawAttempts.map((entry) => ({
                port: Number(entry?.port || (entry?.useSsl ? 8729 : 8728)) || (entry?.useSsl ? 8729 : 8728),
                useSsl: entry?.useSsl === true,
                label: String(entry?.label || '').trim() || 'candidate',
            }))
            : [{
                    port: Number(port || (useSsl ? 8729 : 8728)) || (useSsl ? 8729 : 8728),
                    useSsl: !!useSsl,
                    label: 'configured',
                }];
        let lastError = null;
        for (const attempt of connectionAttempts) {
            const client = new routeros_api_1.RouterOSApiClient({
                host: String(host || '').trim(),
                port: attempt.port,
                username: String(username || '').trim(),
                password: String(password || ''),
                useSsl: !!attempt.useSsl,
                timeoutMs,
            });
            try {
                await client.open();
                let data = '';
                let records = [];
                let metadata = {};
                if (normalizedOperation === 'probe') {
                    const probe = await client.probeDevice();
                    records = [probe.identity, probe.resource, probe.routerboard].filter(Boolean);
                    metadata = {
                        identity: probe.identity || null,
                        resource: probe.resource || null,
                        routerboard: probe.routerboard || null,
                    };
                    data = probe.rawText || JSON.stringify(metadata);
                }
                else if (normalizedOperation === 'backup') {
                    data = await client.exportConfig(`emtelle-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
                }
                else if (normalizedOperation === 'logs-sync') {
                    const logResult = await client.printLogs(20000);
                    if (!logResult.success) {
                        throw new Error(logResult.error || 'RouterOS log sync failed');
                    }
                    records = logResult.records.slice(-Math.max(1, Number(logLimit || 250) || 250));
                    data = (0, routeros_api_1.extractRouterOSText)(logResult);
                }
                else if (normalizedOperation === 'config-apply' || normalizedOperation === 'restore') {
                    const runResult = await client.runInlineScript(String(scriptBody || command || '').trim(), `emtelle-${normalizedOperation}`, 30000);
                    if (!runResult.success) {
                        throw new Error(runResult.error || `RouterOS ${normalizedOperation} failed`);
                    }
                    records = runResult.records;
                    data = (0, routeros_api_1.extractRouterOSText)(runResult);
                }
                else {
                    const words = Array.isArray(commandWords)
                        ? commandWords.map((word) => String(word || '').trim()).filter(Boolean)
                        : String(command || '/system/identity/print').split('\n').map((word) => String(word || '').trim()).filter(Boolean);
                    const commandResult = await client.executeWords(words);
                    if (!commandResult.success) {
                        throw new Error(commandResult.error || 'RouterOS command failed');
                    }
                    records = commandResult.records;
                    data = (0, routeros_api_1.extractRouterOSText)(commandResult);
                }
                this.wsClient.sendMetrics('mikrotik:result', {
                    requestId,
                    deviceId,
                    agentId,
                    operation: normalizedOperation,
                    success: true,
                    data,
                    records,
                    metadata: {
                        ...metadata,
                        connectionAttempt: attempt,
                        connectionAttempts,
                    },
                    command: command || null,
                    timestamp: new Date().toISOString(),
                });
                return;
            }
            catch (error) {
                lastError = error instanceof Error ? error : new Error(String(error));
                logger_1.logger.warn('MikroTik proxy connection attempt failed', {
                    agentId,
                    deviceId,
                    host,
                    port: attempt.port,
                    useSsl: attempt.useSsl,
                    label: attempt.label,
                    operation: normalizedOperation,
                    error: lastError.message,
                });
            }
            finally {
                await client.close().catch(() => undefined);
            }
        }
        this.wsClient.sendMetrics('mikrotik:result', {
            requestId,
            deviceId,
            agentId,
            operation: normalizedOperation,
            success: false,
            error: lastError?.message || 'Unable to establish a RouterOS management session',
            metadata: { connectionAttempts },
            command: command || null,
            timestamp: new Date().toISOString(),
        });
    }
    async handleMikroTikDiscover(payload) {
        const { requestId, subnets: customSubnets } = payload;
        const agentId = this.wsClient.getAgentId() || '';
        const discoveredByHost = new Map();
        try {
            const os = await Promise.resolve().then(() => __importStar(require('os')));
            const net = await Promise.resolve().then(() => __importStar(require('net')));
            // Build subnet list: custom subnets first, then auto-detected
            const subnetsToScan = [];
            if (Array.isArray(customSubnets) && customSubnets.length > 0) {
                // Parse CIDR notation: "10.56.102.0/24", "192.168.0.0/16"
                for (const cidr of customSubnets) {
                    const parts = String(cidr).match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
                    if (!parts)
                        continue;
                    const [, ip, prefixStr] = parts;
                    const prefix = parseInt(prefixStr);
                    const octets = ip.split('.').map(Number);
                    const ipNum = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3];
                    const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
                    const network = (ipNum & mask) >>> 0;
                    const hostCount = Math.min((~mask >>> 0) - 1, 4094); // Cap at 4094 hosts for safety
                    if (hostCount <= 0)
                        continue;
                    for (let h = 1; h <= hostCount; h++) {
                        const hostIp = network + h;
                        const hostOctets = [
                            (hostIp >>> 24) & 0xff,
                            (hostIp >>> 16) & 0xff,
                            (hostIp >>> 8) & 0xff,
                            hostIp & 0xff
                        ];
                        // Group by /24 for scanning
                        const base = hostOctets.slice(0, 3).join('.');
                        let entry = subnetsToScan.find(s => s.base === base);
                        if (!entry) {
                            entry = { base, start: hostOctets[3], end: hostOctets[3] };
                            subnetsToScan.push(entry);
                        }
                        else {
                            entry.start = Math.min(entry.start, hostOctets[3]);
                            entry.end = Math.max(entry.end, hostOctets[3]);
                        }
                    }
                }
            }
            else {
                // Auto-detect from local interfaces
                const interfaces = os.networkInterfaces();
                for (const [, nets] of Object.entries(interfaces)) {
                    for (const iface of nets || []) {
                        if (iface.family === 'IPv4' && !iface.internal) {
                            const parts = iface.address.split('.');
                            subnetsToScan.push({ base: `${parts[0]}.${parts[1]}.${parts[2]}`, start: 1, end: 254 });
                        }
                    }
                }
            }
            // Scan RouterOS API, API-SSL, and Winbox for MikroTik candidates.
            const ports = [
                { port: 8728, label: 'API', confidence: 'high' },
                { port: 8729, label: 'API-SSL', confidence: 'high' },
                { port: 8291, label: 'Winbox', confidence: 'medium' },
            ];
            const batch = [];
            const flushBatch = async () => {
                if (!batch.length) {
                    return;
                }
                const current = batch.splice(0, batch.length);
                await Promise.all(current);
            };
            for (const subnet of subnetsToScan) {
                for (let i = subnet.start; i <= subnet.end; i++) {
                    const host = `${subnet.base}.${i}`;
                    for (const target of ports) {
                        batch.push(new Promise((resolve) => {
                            const start = Date.now();
                            const socket = new net.Socket();
                            socket.setTimeout(1500);
                            socket.on('connect', () => {
                                const responseTime = Date.now() - start;
                                const existing = discoveredByHost.get(host);
                                if (!existing) {
                                    discoveredByHost.set(host, {
                                        host,
                                        port: target.port,
                                        ports: [target.port],
                                        services: [{ port: target.port, label: target.label, confidence: target.confidence }],
                                        reachable: true,
                                        responseTime,
                                        confidence: target.confidence,
                                    });
                                }
                                else if (!existing.ports.includes(target.port)) {
                                    existing.ports.push(target.port);
                                    existing.services.push({ port: target.port, label: target.label, confidence: target.confidence });
                                    existing.responseTime = Math.min(existing.responseTime, responseTime);
                                    if (target.port === 8728 || (target.port === 8729 && existing.port !== 8728)) {
                                        existing.port = target.port;
                                    }
                                    if (target.confidence === 'high') {
                                        existing.confidence = 'high';
                                    }
                                }
                                socket.destroy();
                                resolve();
                            });
                            socket.on('timeout', () => { socket.destroy(); resolve(); });
                            socket.on('error', () => { socket.destroy(); resolve(); });
                            socket.connect(target.port, host);
                        }));
                        if (batch.length >= 150) {
                            await flushBatch();
                        }
                    }
                }
            }
            await flushBatch();
            const discovered = Array.from(discoveredByHost.values()).sort((a, b) => {
                if (a.confidence === b.confidence) {
                    return a.host.localeCompare(b.host);
                }
                return a.confidence === 'high' ? -1 : 1;
            });
            this.wsClient.sendMetrics('mikrotik:discover:result', {
                requestId,
                agentId,
                discovered,
                subnetsScanned: subnetsToScan.map(s => `${s.base}.${s.start}-${s.end}`),
                timestamp: new Date().toISOString(),
            });
        }
        catch (error) {
            this.wsClient.sendMetrics('mikrotik:discover:result', {
                requestId,
                agentId,
                discovered: Array.from(discoveredByHost.values()),
                error: error.message,
                timestamp: new Date().toISOString(),
            });
        }
    }
}
// Start the agent
const agent = new AppStatsAgent();
agent.start().catch(err => {
    logger_1.logger.error('Failed to start agent:', err);
    process.exit(1);
});
//# sourceMappingURL=index.js.map