"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 });
exports.AgentWSClient = void 0;
const ws_1 = __importDefault(require("ws"));
const os = __importStar(require("os"));
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const config_1 = require("../config");
const logger_1 = require("../logger");
const events_1 = require("events");
const os_1 = require("os");
const version_1 = require("../version");
function getIPAddress() {
    const nets = (0, os_1.networkInterfaces)();
    for (const name of Object.keys(nets)) {
        for (const net of nets[name] || []) {
            if (net.family === 'IPv4' && !net.internal) {
                return net.address;
            }
        }
    }
    return '127.0.0.1';
}
function getMACAddress() {
    const nets = (0, os_1.networkInterfaces)();
    for (const name of Object.keys(nets)) {
        for (const net of nets[name] || []) {
            if (net.family === 'IPv4' && !net.internal && net.mac && net.mac !== '00:00:00:00:00:00') {
                return net.mac;
            }
        }
    }
    return '00:00:00:00:00:00';
}
function getAgentId() {
    // Use hostname as the agent ID for backward compatibility and readability
    // Falls back to hash-based ID only if hostname is empty
    const hostname = os.hostname();
    if (hostname)
        return hostname;
    const mac = getMACAddress();
    return crypto.createHash('sha256').update(`${hostname}-${mac}`).digest('hex').substring(0, 32);
}
class AgentWSClient extends events_1.EventEmitter {
    constructor() {
        super();
        this.ws = null;
        this.reconnectTimer = null;
        this.forcedReconnectTimer = null;
        this.heartbeatTimer = null;
        this.pingTimer = null;
        this.socketHealthTimer = null;
        this.lastInboundActivityAt = 0;
        this.reconnectDelay = 1000;
        this.maxReconnectDelay = 30000;
        this.connected = false;
        this.agentId = null;
        this.serverConfig = {};
        this.rotateEndpointOnReconnect = false;
        this.lastAttemptedWsUrl = '';
        this.forceImmediateWsUrl = null;
        this.skipNextEndpointRotation = false;
        this.connectionEventQueue = [];
        this.maxConnectionEventQueue = 300;
        this.flushingConnectionEvents = false;
        this.wsConnectEpoch = 0;
        this.lastForcedReconnectAt = 0;
        this.endpointBackoffUntil = new Map();
        this.currentConnectedWsUrl = '';
        this.lastPreferredRecoveryAttemptAt = 0;
        this.metricsBatch = [];
        this.metricsBatchEnabled = true;
        this.metricsBatchSize = 10;
        this.metricsBatchTimer = null;
        this.connectionEventQueuePath = this.resolveConnectionEventQueuePath();
        this.loadConnectionEventQueue();
    }
    resolveConnectionEventQueuePath() {
        const fallbackLogFile = path.join(process.cwd(), 'logs', 'agent.log');
        const configuredLogFile = String((config_1.config.logging && config_1.config.logging.file) || fallbackLogFile).trim() || fallbackLogFile;
        const logDir = path.dirname(configuredLogFile);
        try {
            fs.mkdirSync(logDir, { recursive: true });
        }
        catch { }
        return path.join(logDir, 'connection-events-queue.json');
    }
    loadConnectionEventQueue() {
        try {
            if (!fs.existsSync(this.connectionEventQueuePath))
                return;
            const raw = fs.readFileSync(this.connectionEventQueuePath, 'utf8');
            const parsed = JSON.parse(raw);
            if (!Array.isArray(parsed))
                return;
            this.connectionEventQueue = parsed
                .filter((entry) => entry && typeof entry === 'object')
                .map((entry) => ({
                id: String(entry.id || '').trim() || `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
                timestamp: String(entry.timestamp || new Date().toISOString()),
                level: (String(entry.level || 'info').toLowerCase() === 'error'
                    ? 'error'
                    : String(entry.level || 'info').toLowerCase() === 'warn'
                        ? 'warn'
                        : 'info'),
                eventType: String(entry.eventType || entry.type || 'unknown').trim() || 'unknown',
                message: String(entry.message || '').trim() || 'Connection event',
                details: entry.details && typeof entry.details === 'object' ? entry.details : undefined,
            }))
                .slice(-this.maxConnectionEventQueue);
        }
        catch (error) {
            logger_1.logger.debug('Failed to load connection event queue', { error: error?.message || String(error) });
            this.connectionEventQueue = [];
        }
    }
    persistConnectionEventQueue() {
        try {
            fs.writeFileSync(this.connectionEventQueuePath, JSON.stringify(this.connectionEventQueue, null, 2));
        }
        catch (error) {
            logger_1.logger.debug('Failed to persist connection event queue', { error: error?.message || String(error) });
        }
    }
    queueConnectionEvent(eventType, message, level = 'info', details) {
        const recordId = typeof crypto.randomUUID === 'function'
            ? String(crypto.randomUUID())
            : `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
        const safeDetails = {};
        if (details && typeof details === 'object') {
            for (const [key, value] of Object.entries(details)) {
                if (value === undefined || value === null || value === '')
                    continue;
                if (typeof value === 'string') {
                    safeDetails[key] = value.slice(0, 512);
                }
                else if (typeof value === 'number' || typeof value === 'boolean') {
                    safeDetails[key] = value;
                }
                else {
                    try {
                        safeDetails[key] = JSON.stringify(value).slice(0, 512);
                    }
                    catch {
                        safeDetails[key] = String(value).slice(0, 512);
                    }
                }
            }
        }
        this.connectionEventQueue.push({
            id: recordId,
            timestamp: new Date().toISOString(),
            level,
            eventType: String(eventType || 'unknown').trim().slice(0, 80) || 'unknown',
            message: String(message || 'Connection event').trim().slice(0, 512) || 'Connection event',
            details: Object.keys(safeDetails).length > 0 ? safeDetails : undefined,
        });
        if (this.connectionEventQueue.length > this.maxConnectionEventQueue) {
            this.connectionEventQueue.splice(0, this.connectionEventQueue.length - this.maxConnectionEventQueue);
        }
        this.persistConnectionEventQueue();
        if (this.connected) {
            this.flushConnectionEventQueue();
        }
    }
    flushConnectionEventQueue() {
        if (this.flushingConnectionEvents)
            return;
        if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
            return;
        if (this.connectionEventQueue.length === 0)
            return;
        this.flushingConnectionEvents = true;
        const batch = this.connectionEventQueue.slice(0, 40);
        const frame = JSON.stringify({
            type: 'agent:connection-events',
            timestamp: new Date().toISOString(),
            payload: {
                agentId: this.agentId || getAgentId(),
                events: batch,
            },
        });
        this.ws.send(frame, (error) => {
            this.flushingConnectionEvents = false;
            if (error) {
                logger_1.logger.warn('Failed to flush connection diagnostics', { error: error.message });
                return;
            }
            this.connectionEventQueue.splice(0, batch.length);
            this.persistConnectionEventQueue();
            if (this.connectionEventQueue.length > 0) {
                setTimeout(() => this.flushConnectionEventQueue(), 100);
            }
        });
    }
    markInboundActivity() {
        this.lastInboundActivityAt = Date.now();
    }
    normalizeEndpointUrl(value) {
        return String(value || '').trim();
    }
    pruneEndpointBackoff(now = Date.now()) {
        for (const [url, until] of this.endpointBackoffUntil.entries()) {
            if (!Number.isFinite(until) || until <= now) {
                this.endpointBackoffUntil.delete(url);
            }
        }
    }
    markEndpointFailure(url, errorMessage, source = 'error') {
        const endpoint = this.normalizeEndpointUrl(url);
        if (!endpoint)
            return;
        const normalizedError = String(errorMessage || '').toLowerCase();
        let penaltyMs = 45000;
        if (normalizedError.includes('enotfound')
            || normalizedError.includes('eai_again')
            || normalizedError.includes('name not resolved')
            || normalizedError.includes('dns')) {
            penaltyMs = 10 * 60 * 1000;
        }
        else if (normalizedError.includes('econnrefused')
            || normalizedError.includes('ehostunreach')
            || normalizedError.includes('enetunreach')
            || normalizedError.includes('etimedout')
            || normalizedError.includes('timeout')) {
            penaltyMs = 5 * 60 * 1000;
        }
        else if (normalizedError.includes('unexpected server response: 502')
            || normalizedError.includes('unexpected server response: 503')
            || normalizedError.includes('unexpected server response: 504')) {
            penaltyMs = 3 * 60 * 1000;
        }
        else if (source === 'tls_mismatch') {
            penaltyMs = 15 * 60 * 1000;
        }
        else if (source === 'close') {
            penaltyMs = 90 * 1000;
        }
        const now = Date.now();
        const existingUntil = Number(this.endpointBackoffUntil.get(endpoint) || 0);
        const nextUntil = Math.max(existingUntil, now + penaltyMs);
        if (nextUntil <= existingUntil && existingUntil > now)
            return;
        this.endpointBackoffUntil.set(endpoint, nextUntil);
        this.queueConnectionEvent('endpoint_penalized', 'Temporarily suppressing failing WebSocket endpoint', 'warn', {
            url: endpoint,
            source,
            penaltyMs,
            retryAt: new Date(nextUntil).toISOString(),
            reason: String(errorMessage || '').slice(0, 180),
        });
    }
    clearEndpointFailure(url) {
        const endpoint = this.normalizeEndpointUrl(url);
        if (!endpoint)
            return;
        this.endpointBackoffUntil.delete(endpoint);
    }
    startSocketHealthMonitor() {
        this.stopSocketHealthMonitor();
        this.markInboundActivity();
        const staleThresholdMs = Math.max(config_1.config.intervals.heartbeat * 3, 120000);
        this.socketHealthTimer = setInterval(() => {
            if (!this.connected || !this.ws || this.ws.readyState !== ws_1.default.OPEN)
                return;
            const idleMs = Date.now() - this.lastInboundActivityAt;
            if (idleMs >= staleThresholdMs) {
                logger_1.logger.warn(`WebSocket stale for ${Math.round(idleMs / 1000)}s; forcing reconnect`);
                this.queueConnectionEvent('stale_connection', 'No inbound WebSocket traffic; forcing reconnect', 'warn', { idleMs, url: this.lastAttemptedWsUrl || '' });
                this.connected = false;
                this.stopHeartbeat();
                this.stopSocketHealthMonitor();
                this.rotateEndpointOnReconnect = true;
                this.reconnectDelay = 1000;
                if (this.ws) {
                    this.ws.removeAllListeners('close');
                    try {
                        this.ws.terminate();
                    }
                    catch { }
                    this.ws = null;
                }
                this.currentConnectedWsUrl = '';
                this.scheduleReconnect();
                this.emit('disconnected');
                return;
            }
            this.maybeRecoverPreferredEndpoint();
        }, 15000);
    }
    stopSocketHealthMonitor() {
        if (this.socketHealthTimer) {
            clearInterval(this.socketHealthTimer);
            this.socketHealthTimer = null;
        }
    }
    getWsCandidates() {
        const parsePriority = (value) => {
            const defaults = ['wireguard', 'primary', 'secondary'];
            const items = 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 items) {
                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;
        };
        const modeRaw = String(config_1.config.server.transportMode || 'auto').trim().toLowerCase();
        const mode = modeRaw === 'force_wss' || modeRaw === 'force_wireguard' ? modeRaw : 'auto';
        const priority = parsePriority(config_1.config.server.channelPriority);
        let orderedChannels = [...priority];
        if (mode === 'force_wss') {
            orderedChannels = orderedChannels.filter((name) => name !== 'wireguard');
        }
        else if (mode === 'force_wireguard') {
            orderedChannels = ['wireguard', ...orderedChannels.filter((name) => name !== 'wireguard')];
        }
        const wireguardAvailable = config_1.config.server.wireguardActive === true;
        const wsByChannel = {
            wireguard: wireguardAvailable ? String(config_1.config.server.wireguardWsUrl || '').trim() : '',
            primary: String(config_1.config.server.primaryWsUrl || config_1.config.server.wsUrl || '').trim(),
            secondary: String(config_1.config.server.secondaryWsUrl || '').trim(),
        };
        const ordered = [];
        for (const channel of orderedChannels) {
            const endpoint = wsByChannel[channel];
            if (!endpoint)
                continue;
            if (!ordered.includes(endpoint))
                ordered.push(endpoint);
        }
        const fallbackWs = mode === 'force_wss'
            ? (wsByChannel.primary || wsByChannel.secondary || 'ws://localhost:3001')
            : (String(config_1.config.server.wsUrl || '').trim()
                || wsByChannel.primary
                || wsByChannel.secondary
                || 'ws://localhost:3001');
        if (!ordered.includes(fallbackWs))
            ordered.push(fallbackWs);
        return ordered;
    }
    getPreferredWsCandidate(now = Date.now()) {
        const candidates = this.getWsCandidates();
        this.pruneEndpointBackoff(now);
        for (const candidate of candidates) {
            const until = Number(this.endpointBackoffUntil.get(candidate) || 0);
            if (!Number.isFinite(until) || until <= now) {
                return candidate;
            }
        }
        return candidates[0] || '';
    }
    maybeRecoverPreferredEndpoint() {
        if (!this.connected || !this.ws || this.ws.readyState !== ws_1.default.OPEN)
            return;
        const now = Date.now();
        const preferred = this.getPreferredWsCandidate(now);
        const current = this.normalizeEndpointUrl(this.currentConnectedWsUrl || this.lastAttemptedWsUrl);
        if (!preferred || !current || preferred === current)
            return;
        if ((now - this.lastPreferredRecoveryAttemptAt) < 60000)
            return;
        this.lastPreferredRecoveryAttemptAt = now;
        this.queueConnectionEvent('preferred_endpoint_recovery', 'Attempting to restore preferred WebSocket endpoint', 'info', { from: current, to: preferred });
        this.reconnect(preferred);
    }
    selectWsUrlForConnect() {
        const candidates = this.getWsCandidates();
        const now = Date.now();
        this.pruneEndpointBackoff(now);
        const preferredCandidates = candidates.filter((candidate) => {
            const until = Number(this.endpointBackoffUntil.get(candidate) || 0);
            return !Number.isFinite(until) || until <= now;
        });
        const selectionPool = preferredCandidates.length > 0 ? preferredCandidates : candidates;
        if (preferredCandidates.length === 0 && candidates.length > 0) {
            this.queueConnectionEvent('all_endpoints_penalized', 'All configured WebSocket endpoints are in backoff; retrying best available endpoint', 'warn', { candidates });
        }
        const immediate = String(this.forceImmediateWsUrl || '').trim();
        if (immediate && (selectionPool.includes(immediate) || candidates.includes(immediate))) {
            this.forceImmediateWsUrl = null;
            this.rotateEndpointOnReconnect = false;
            this.lastAttemptedWsUrl = immediate;
            return immediate;
        }
        this.forceImmediateWsUrl = null;
        let selected = selectionPool[0];
        if (this.rotateEndpointOnReconnect && selectionPool.length > 1 && this.lastAttemptedWsUrl) {
            const idx = selectionPool.findIndex((u) => u === this.lastAttemptedWsUrl);
            const nextIdx = idx >= 0 ? (idx + 1) % selectionPool.length : 0;
            selected = selectionPool[nextIdx];
            if (selected !== this.lastAttemptedWsUrl) {
                this.queueConnectionEvent('endpoint_rotated', 'Rotating WebSocket endpoint after disconnect', 'warn', { from: this.lastAttemptedWsUrl, to: selected });
            }
        }
        this.rotateEndpointOnReconnect = false;
        this.lastAttemptedWsUrl = selected;
        return selected;
    }
    connect() {
        this.stopSocketHealthMonitor();
        if (this.forcedReconnectTimer) {
            clearTimeout(this.forcedReconnectTimer);
            this.forcedReconnectTimer = null;
        }
        const previousSocket = this.ws;
        if (previousSocket) {
            previousSocket.removeAllListeners();
            try {
                previousSocket.close();
            }
            catch { }
            try {
                previousSocket.terminate?.();
            }
            catch { }
        }
        const url = this.selectWsUrlForConnect();
        logger_1.logger.info(`Connecting to server: ${url}`);
        this.queueConnectionEvent('connect_attempt', 'Attempting WebSocket connection', 'info', { url });
        const socket = new ws_1.default(url, {
            headers: {
                'X-Agent-Token': config_1.config.server.token,
                'X-Agent-Hostname': config_1.config.agent.hostname,
                'X-Agent-Platform': config_1.config.agent.platform,
                'X-Agent-Version': version_1.AGENT_VERSION,
            },
            perMessageDeflate: true,
        });
        this.ws = socket;
        const connectEpoch = ++this.wsConnectEpoch;
        const isCurrentSocket = () => this.ws === socket && this.wsConnectEpoch === connectEpoch;
        socket.on('open', () => {
            if (!isCurrentSocket()) {
                try {
                    socket.close(1000, 'Superseded connection');
                }
                catch { }
                return;
            }
            logger_1.logger.info('WebSocket connected');
            this.connected = true;
            this.currentConnectedWsUrl = url;
            this.clearEndpointFailure(url);
            this.reconnectDelay = 1000;
            this.rotateEndpointOnReconnect = false;
            this.markInboundActivity();
            this.queueConnectionEvent('connected', 'WebSocket connected', 'info', { url });
            this.register();
            this.startHeartbeat();
            this.startMetricsBatchFlush();
            this.startSocketHealthMonitor();
            this.emit('connected', { url });
            this.flushConnectionEventQueue();
        });
        socket.on('message', (data) => {
            if (!isCurrentSocket())
                return;
            this.markInboundActivity();
            try {
                const message = JSON.parse(data.toString());
                this.handleMessage(message);
            }
            catch (e) {
                logger_1.logger.error('Failed to parse message:', e);
            }
        });
        socket.on('close', (code, reason) => {
            if (!isCurrentSocket())
                return;
            const reasonText = reason?.toString() || '';
            logger_1.logger.warn(`WebSocket closed: ${code} - ${reasonText}`);
            this.connected = false;
            this.currentConnectedWsUrl = '';
            this.stopHeartbeat();
            this.stopSocketHealthMonitor();
            const closeDetails = {
                code,
                reason: reasonText,
                url: this.lastAttemptedWsUrl || '',
            };
            const duplicateConflict = code === 4001
                || code === 4002
                || /duplicate|older|newer|replaced/i.test(reasonText);
            if (/unauthor/i.test(reasonText) || code === 1008) {
                this.queueConnectionEvent('auth_rejected', 'WebSocket authorization failed', 'error', closeDetails);
            }
            else if (duplicateConflict) {
                this.queueConnectionEvent('duplicate_instance', 'Duplicate/superseded agent instance detected', 'error', closeDetails);
            }
            else if (!/agent shutting down/i.test(reasonText)) {
                this.queueConnectionEvent('disconnected', 'WebSocket disconnected', 'warn', closeDetails);
                if (code === 1006) {
                    this.markEndpointFailure(this.lastAttemptedWsUrl, `close:${code}:${reasonText}`, 'close');
                }
            }
            if (duplicateConflict) {
                if (this.reconnectTimer) {
                    clearTimeout(this.reconnectTimer);
                    this.reconnectTimer = null;
                }
                this.rotateEndpointOnReconnect = false;
                this.reconnectDelay = Math.max(this.reconnectDelay, 15000);
                logger_1.logger.warn('Duplicate/superseded agent instance detected; scheduling guarded reconnect');
                this.scheduleReconnect();
                this.emit('duplicate-instance', { code, reason: reasonText, recovery: 'scheduled_reconnect' });
                this.emit('disconnected', { code, reason: reasonText, url: this.lastAttemptedWsUrl || '' });
                return;
            }
            if (this.skipNextEndpointRotation) {
                this.rotateEndpointOnReconnect = false;
                this.skipNextEndpointRotation = false;
            }
            else {
                this.rotateEndpointOnReconnect = true;
            }
            this.scheduleReconnect();
            this.emit('disconnected', { code, reason: reasonText, url: this.lastAttemptedWsUrl || '' });
        });
        socket.on('error', (error) => {
            if (!isCurrentSocket())
                return;
            logger_1.logger.error('WebSocket error:', error);
            const errorMessage = error?.message || String(error);
            this.applyTlsMismatchFallback(errorMessage);
            this.markEndpointFailure(this.lastAttemptedWsUrl, errorMessage, 'error');
            this.queueConnectionEvent('ws_error', 'WebSocket error encountered', 'error', {
                error: errorMessage,
                url: this.lastAttemptedWsUrl || '',
            });
        });
        socket.on('pong', () => {
            if (!isCurrentSocket())
                return;
            this.markInboundActivity();
        });
        socket.on('ping', () => {
            if (!isCurrentSocket())
                return;
            this.markInboundActivity();
        });
    }
    applyTlsMismatchFallback(errorMessage) {
        const attemptedUrl = String(this.lastAttemptedWsUrl || '').trim();
        if (!attemptedUrl || !/^wss:\/\//i.test(attemptedUrl))
            return;
        const normalizedError = String(errorMessage || '').toLowerCase();
        if (!normalizedError.includes('eproto')
            && !normalizedError.includes('wrong version number')
            && !normalizedError.includes('ssl3_get_record')) {
            return;
        }
        // Strict TLS policy: block silent WSS->WS downgrade in either of:
        //  - transportMode === 'force_wss' (operator forced WSS)
        //  - agent.allowTlsDowngrade === false (operator disabled fallback)
        // This prevents an attacker who can stage a TLS error from forcing the
        // agent into plaintext.
        const transportModeRaw = String(config_1.config.server?.transportMode || 'auto').trim().toLowerCase();
        const transportMode = transportModeRaw === 'force_wss' || transportModeRaw === 'force_wireguard'
            ? transportModeRaw
            : 'auto';
        const allowDowngrade = config_1.config.agent?.allowTlsDowngrade !== false;
        if (transportMode === 'force_wss' || !allowDowngrade) {
            this.markEndpointFailure(attemptedUrl, errorMessage, 'tls_mismatch_blocked');
            this.queueConnectionEvent('ws_protocol_downgrade_blocked', 'Refused to downgrade websocket URL to plaintext due to strict TLS policy', 'error', {
                url: attemptedUrl,
                error: errorMessage,
                transportMode,
                allowTlsDowngrade: allowDowngrade,
            });
            logger_1.logger.error(`Refusing WSS->WS downgrade (strict TLS): url=${attemptedUrl} ` +
                `transportMode=${transportMode} allowTlsDowngrade=${allowDowngrade}`);
            return;
        }
        let parsed;
        try {
            parsed = new URL(attemptedUrl);
        }
        catch {
            return;
        }
        if (parsed.port !== '3001')
            return;
        const fallbackUrl = `ws://${parsed.host}${parsed.pathname || ''}${parsed.search || ''}${parsed.hash || ''}`;
        this.markEndpointFailure(attemptedUrl, errorMessage, 'tls_mismatch');
        const rewriteField = (field) => {
            if (String(config_1.config.server?.[field] || '').trim() === attemptedUrl) {
                config_1.config.server[field] = fallbackUrl;
            }
        };
        rewriteField('wsUrl');
        rewriteField('primaryWsUrl');
        rewriteField('secondaryWsUrl');
        rewriteField('wireguardWsUrl');
        this.forceImmediateWsUrl = fallbackUrl;
        this.skipNextEndpointRotation = true;
        this.reconnectDelay = 1000;
        this.queueConnectionEvent('ws_protocol_downgrade', 'Downgrading websocket URL to non-TLS after protocol mismatch', 'warn', { from: attemptedUrl, to: fallbackUrl, error: errorMessage });
        logger_1.logger.warn(`Downgrading websocket URL after TLS mismatch: ${attemptedUrl} -> ${fallbackUrl}`);
    }
    register() {
        const userInfo = os.userInfo();
        this.send({
            type: 'agent:register',
            payload: {
                id: getAgentId(),
                hostname: config_1.config.agent.hostname,
                platform: config_1.config.agent.platform,
                arch: config_1.config.agent.arch,
                osVersion: `${os.type()} ${os.release()}`,
                agentVersion: version_1.AGENT_VERSION,
                ipAddress: getIPAddress(),
                macAddress: getMACAddress(),
                username: userInfo.username,
                domain: process.env.USERDOMAIN || process.env.HOSTNAME || null,
                tags: config_1.config.agent.tags,
                groups: config_1.config.agent.groups,
                capabilities: {
                    systemMonitoring: true,
                    processMonitoring: true,
                    networkMonitoring: true,
                    productivityTracking: true,
                    granularProductivityTracking: true,
                    browserMonitoring: true,
                    mtrProbes: true,
                    pingProbes: true,
                    trafficFiltering: true,
                    remoteTermination: true,
                    selfUpgrade: true,
                    wireGuard: true,
                    screenshotCapture: true,
                },
                systemInfo: {
                    totalMemory: os.totalmem(),
                    cpuCount: os.cpus().length,
                    cpuModel: os.cpus()[0]?.model || 'Unknown',
                    uptime: os.uptime(),
                    username: userInfo.username,
                    homeDir: os.homedir(),
                },
            },
        });
    }
    handleMessage(message) {
        logger_1.logger.debug(`Received: ${message.type}`);
        switch (message.type) {
            case 'agent:ack':
            case 'registration:ack':
                this.agentId = message.agentId || message.payload?.agentId || getAgentId();
                this.serverConfig = message.payload?.config || {};
                logger_1.logger.info(`Registered as agent: ${this.agentId}`);
                this.emit('registered', this.agentId, this.serverConfig);
                break;
            case 'config:update':
                this.serverConfig = { ...this.serverConfig, ...message.payload };
                logger_1.logger.info('Configuration updated from server');
                this.emit('config-update', this.serverConfig);
                break;
            case 'process:terminate':
                this.emit('process-terminate', message.payload);
                break;
            case 'monitoring:toggle':
                this.emit('monitoring-toggle', message.payload);
                break;
            case 'probe:start':
            case 'probe:config:update':
                this.emit('probe-start', message.payload);
                break;
            case 'probe:stop':
                this.emit('probe-stop', message.payload);
                break;
            case 'upgrade:command':
            case 'upgrade:force':
                this.emit('upgrade-command', message.payload);
                break;
            case 'upgrade:available':
                this.emit('upgrade-available', message.payload);
                break;
            case 'traffic:rule:update':
            case 'traffic:rules-update':
                this.emit('traffic-rules-update', message.payload);
                break;
            case 'url:filter:update':
            case 'traffic:filters-update':
                this.emit('traffic-filters-update', message.payload);
                break;
            case 'system:metrics:request':
            case 'request:system-info':
                this.emit('request-system-info', message.payload);
                break;
            case 'process:list:request':
            case 'request:processes':
                this.emit('request-processes', message.payload);
                break;
            case 'software:list:request':
            case 'request:software':
                this.emit('request-software', message.payload);
                break;
            case 'software:uninstall':
            case 'software:remove':
                this.emit('software-uninstall', message.payload);
                break;
            case 'software:install':
                this.emit('software-install', message.payload);
                break;
            case 'software:update':
                this.emit('software-update', message.payload);
                break;
            case 'maintenance:run':
                this.emit('maintenance-run', message.payload);
                break;
            case 'maintenance:schedule':
                this.emit('maintenance-schedule', message.payload);
                break;
            case 'extension:install':
            case 'browser:extension:install':
                this.emit('extension-install', message.payload);
                break;
            case 'security:scan:start':
            case 'security:scan':
                this.emit('security-scan', message.payload);
                break;
            case 'remote:session:start':
            case 'remote:session':
                this.emit('remote-session-start', message.payload);
                break;
            case 'remote:session:end':
                this.emit('remote-session-end', message.payload);
                break;
            case 'remote:shell:input':
                this.emit('remote-shell-input', message.payload);
                break;
            case 'remote:shell:control':
                this.emit('remote-shell-control', message.payload);
                break;
            case 'remote:relay:start':
                this.emit('remote-relay-start', message.payload);
                break;
            case 'remote:relay:data':
                this.emit('remote-relay-data', message.payload);
                break;
            case 'remote:relay:end':
                this.emit('remote-relay-end', message.payload);
                break;
            case 'remote:emremote:input':
                this.emit('remote-emremote-input', message.payload);
                break;
            case 'wireguard:config':
                this.emit('wireguard-config', message.payload);
                break;
            case 'speedtest:run':
            case 'speedtest:request':
                this.emit('speedtest-run', message.payload);
                break;
            case 'mikrotik:command':
                this.emit('mikrotik-command', message.payload);
                break;
            case 'mikrotik:discover':
                this.emit('mikrotik-discover', message.payload);
                break;
            default:
                logger_1.logger.debug(`Unhandled message type: ${message.type}`);
                this.emit('message', message);
        }
    }
    send(message) {
        if (!this.ws || !this.connected) {
            logger_1.logger.warn('Cannot send: not connected');
            return;
        }
        try {
            this.ws.send(JSON.stringify({
                ...message,
                timestamp: new Date().toISOString(),
            }));
        }
        catch (e) {
            logger_1.logger.error('Send error:', e);
        }
    }
    sendMetrics(type, data) {
        // Batch non-critical metrics to reduce WebSocket overhead
        const criticalTypes = ['agent:heartbeat', 'agent:register', 'security:scan', 'probe:result', 'critical:probe-update', 'mikrotik:result', 'mikrotik:discover:result'];
        if (!criticalTypes.includes(type) && this.metricsBatchEnabled) {
            this.metricsBatch.push({ type, payload: { agentId: this.agentId, ...data }, timestamp: new Date().toISOString() });
            if (this.metricsBatch.length >= this.metricsBatchSize) {
                this.flushMetricsBatch();
            }
            return;
        }
        this.send({ type, payload: { agentId: this.agentId, ...data } });
    }
    startMetricsBatchFlush() {
        if (this.metricsBatchTimer)
            return;
        this.metricsBatchTimer = setInterval(() => this.flushMetricsBatch(), 15000);
    }
    stopMetricsBatchFlush() {
        if (this.metricsBatchTimer) {
            clearInterval(this.metricsBatchTimer);
            this.metricsBatchTimer = null;
        }
    }
    flushMetricsBatch() {
        if (this.metricsBatch.length === 0)
            return;
        const batch = this.metricsBatch.splice(0);
        this.send({ type: 'agent:metrics-batch', payload: { agentId: this.agentId, batch } });
    }
    startHeartbeat() {
        this.stopHeartbeat();
        const keepAliveIntervalMs = 4000;
        // Application-level heartbeat
        this.heartbeatTimer = setInterval(() => {
            this.send({
                type: 'agent:heartbeat',
                payload: {
                    agentId: this.agentId || getAgentId(),
                    uptime: process.uptime(),
                    memoryUsage: process.memoryUsage(),
                    version: version_1.AGENT_VERSION,
                    status: 'healthy',
                    activeCollectors: ['system', 'process', 'productivity', 'network'],
                    pendingDataCount: 0,
                    errors: [],
                    timestamp: new Date().toISOString(),
                },
            });
        }, config_1.config.intervals.heartbeat);
        // Some deployments sit behind aggressive idle timeouts that tear down
        // quiet WebSocket sessions in ~5 seconds, so keep a small protocol-level
        // heartbeat running independently of the normal metrics heartbeat.
        if (this.ws?.readyState === ws_1.default.OPEN) {
            this.ws.ping();
        }
        this.pingTimer = setInterval(() => {
            if (this.ws?.readyState === ws_1.default.OPEN) {
                this.ws.ping();
            }
        }, keepAliveIntervalMs);
    }
    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
        if (this.pingTimer) {
            clearInterval(this.pingTimer);
            this.pingTimer = null;
        }
        this.stopMetricsBatchFlush();
        this.flushMetricsBatch(); // flush any remaining batched metrics
    }
    scheduleReconnect() {
        if (this.reconnectTimer)
            return;
        // Don't schedule reconnect if we're already connected (prevents stale close handlers from looping)
        if (this.connected)
            return;
        logger_1.logger.info(`Reconnecting in ${this.reconnectDelay / 1000}s...`);
        this.queueConnectionEvent('reconnect_scheduled', 'Scheduled reconnect attempt', 'warn', {
            delayMs: this.reconnectDelay,
            lastUrl: this.lastAttemptedWsUrl || '',
        });
        this.reconnectTimer = setTimeout(() => {
            this.reconnectTimer = null;
            this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
            this.connect();
        }, this.reconnectDelay);
    }
    isConnected() {
        return this.connected;
    }
    getCurrentWsUrl() {
        return this.currentConnectedWsUrl;
    }
    getAgentId() {
        return this.agentId;
    }
    getServerConfig() {
        return this.serverConfig;
    }
    disconnect() {
        this.stopHeartbeat();
        this.stopSocketHealthMonitor();
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
        if (this.forcedReconnectTimer) {
            clearTimeout(this.forcedReconnectTimer);
            this.forcedReconnectTimer = null;
        }
        this.rotateEndpointOnReconnect = false;
        this.lastAttemptedWsUrl = '';
        this.queueConnectionEvent('shutdown_disconnect', 'Disconnecting WebSocket client', 'info', {});
        if (this.ws) {
            this.ws.removeAllListeners();
            this.ws.close(1000, 'Agent shutting down');
            this.ws = null;
        }
        this.connected = false;
        this.currentConnectedWsUrl = '';
    }
    /**
     * Reconnect the WebSocket using the current config.server.wsUrl.
     * Call this after updating config.server.wsUrl at runtime (e.g. after
     * WireGuard tunnel is established) to route traffic through the tunnel.
     */
    setPreferredWsUrl(targetWsUrl) {
        const target = this.normalizeEndpointUrl(targetWsUrl);
        if (!target)
            return;
        this.clearEndpointFailure(target);
        this.forceImmediateWsUrl = target;
    }
    reconnect(targetWsUrl) {
        const now = Date.now();
        if ((now - this.lastForcedReconnectAt) < 1500) {
            logger_1.logger.warn('Skipping duplicate reconnect request (already reconnecting)');
            return;
        }
        const target = this.normalizeEndpointUrl(String(targetWsUrl || ''));
        if (target) {
            this.setPreferredWsUrl(target);
        }
        this.lastForcedReconnectAt = now;
        logger_1.logger.info('WS reconnect requested — closing current connection');
        this.queueConnectionEvent('reconnect_requested', 'WebSocket reconnect requested', 'info', {
            targetUrl: target || '',
            lastUrl: this.lastAttemptedWsUrl || '',
        });
        this.stopHeartbeat();
        this.stopSocketHealthMonitor();
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
        if (this.forcedReconnectTimer) {
            clearTimeout(this.forcedReconnectTimer);
            this.forcedReconnectTimer = null;
        }
        this.rotateEndpointOnReconnect = false;
        this.lastAttemptedWsUrl = '';
        if (this.ws) {
            this.ws.removeAllListeners();
            try {
                this.ws.close(1000, 'Reconnecting with updated URL');
            }
            catch { }
            this.ws = null;
        }
        this.connected = false;
        this.currentConnectedWsUrl = '';
        // Reset backoff so reconnect is immediate
        this.reconnectDelay = 1000;
        // connect() reads config.server.wsUrl fresh each time — picks up the new URL
        this.forcedReconnectTimer = setTimeout(() => {
            this.forcedReconnectTimer = null;
            this.connect();
        }, 500);
    }
}
exports.AgentWSClient = AgentWSClient;
//# sourceMappingURL=client.js.map