"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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.UpgradeService = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const http = __importStar(require("http"));
const https = __importStar(require("https"));
const child_process_1 = require("child_process");
const tar = require('tar');
const config_1 = require("../config");
const logger_1 = require("../logger");
const version_1 = require("../version");
class UpgradeService {
    constructor(wsClient) {
        this.upgrading = false;
        this.upgradingVersion = null;
        this.pendingUpgrade = null;
        this.wsClient = wsClient;
    }
    async sanitizeLocalCache(reason = 'scheduled') {
        if (this.upgrading)
            return;
        const installPath = config_1.config.upgrade.installPath;
        const backupRoot = path.join(installPath, 'backup');
        const updatesRoot = path.join(installPath, 'updates');
        const maxCacheMb = Math.max(100, parseInt(String(process.env.APPSTATS_LOCAL_CACHE_MAX_MB || '300'), 10) || 300);
        const keepBackups = Math.max(1, Math.min(5, parseInt(String(process.env.APPSTATS_UPGRADE_BACKUP_KEEP_COUNT || '2'), 10) || 2));
        const staleHours = Math.max(6, parseInt(String(process.env.APPSTATS_UPGRADE_CACHE_STALE_HOURS || '24'), 10) || 24);
        const staleCutoff = Date.now() - staleHours * 60 * 60 * 1000;
        const maxCacheBytes = maxCacheMb * 1024 * 1024;
        const readEntries = (dir) => {
            if (!fs.existsSync(dir))
                return [];
            return fs.readdirSync(dir, { withFileTypes: true }).map((entry) => {
                const fullPath = path.join(dir, entry.name);
                let mtimeMs = 0;
                try {
                    mtimeMs = fs.statSync(fullPath).mtimeMs || 0;
                }
                catch { }
                return {
                    path: fullPath,
                    name: entry.name,
                    isDir: entry.isDirectory(),
                    mtimeMs,
                };
            });
        };
        const removePath = (targetPath) => {
            try {
                const stat = fs.statSync(targetPath);
                if (stat.isDirectory())
                    this.removeRecursive(targetPath);
                else
                    fs.unlinkSync(targetPath);
            }
            catch { }
        };
        try {
            for (const scriptName of ['restart.bat', 'restart-service.bat', 'restart.sh']) {
                const scriptPath = path.join(installPath, scriptName);
                if (!fs.existsSync(scriptPath))
                    continue;
                try {
                    const st = fs.statSync(scriptPath);
                    if ((st.mtimeMs || 0) < staleCutoff)
                        fs.unlinkSync(scriptPath);
                }
                catch { }
            }
            const backupEntries = readEntries(backupRoot)
                .filter((entry) => entry.isDir)
                .sort((a, b) => b.mtimeMs - a.mtimeMs);
            for (const entry of backupEntries) {
                const legacyNodeModules = path.join(entry.path, 'node_modules');
                if (fs.existsSync(legacyNodeModules)) {
                    removePath(legacyNodeModules);
                }
            }
            for (let i = keepBackups; i < backupEntries.length; i++) {
                removePath(backupEntries[i].path);
            }
            const updateEntries = readEntries(updatesRoot).sort((a, b) => b.mtimeMs - a.mtimeMs);
            for (let i = 1; i < updateEntries.length; i++) {
                const entry = updateEntries[i];
                if (entry.mtimeMs < staleCutoff)
                    removePath(entry.path);
            }
            const postBackupEntries = readEntries(backupRoot)
                .filter((entry) => entry.isDir)
                .sort((a, b) => b.mtimeMs - a.mtimeMs);
            const postUpdateEntries = readEntries(updatesRoot).sort((a, b) => b.mtimeMs - a.mtimeMs);
            const protectedPaths = new Set();
            if (postBackupEntries[0])
                protectedPaths.add(postBackupEntries[0].path);
            if (postUpdateEntries[0])
                protectedPaths.add(postUpdateEntries[0].path);
            const cacheItems = [...postBackupEntries, ...postUpdateEntries]
                .map((entry) => ({
                path: entry.path,
                mtimeMs: entry.mtimeMs,
                size: this.getPathSize(entry.path),
            }))
                .filter((entry) => entry.size > 0);
            let totalCacheBytes = cacheItems.reduce((sum, entry) => sum + entry.size, 0);
            if (totalCacheBytes > maxCacheBytes) {
                const ordered = [...cacheItems].sort((a, b) => a.mtimeMs - b.mtimeMs);
                for (const item of ordered) {
                    if (totalCacheBytes <= maxCacheBytes)
                        break;
                    if (protectedPaths.has(item.path))
                        continue;
                    removePath(item.path);
                    totalCacheBytes -= item.size;
                }
            }
            logger_1.logger.info(`Upgrade cache sanitization completed (${reason})`, {
                maxCacheMb,
                keepBackups,
                staleHours,
            });
        }
        catch (error) {
            logger_1.logger.warn(`Upgrade cache sanitization skipped (${reason}): ${error?.message || String(error)}`);
        }
    }
    // ---- Called when server notifies an upgrade is available ----
    handleUpgradeAvailable(info) {
        const version = info.targetVersion || info.version;
        logger_1.logger.info(`Upgrade available: v${version_1.AGENT_VERSION} -> v${version} (mandatory=${info.mandatory})`);
        if (this.upgrading) {
            logger_1.logger.info(`Upgrade already in progress (${this.upgradingVersion || 'unknown target'}), ignoring availability notice`);
            return;
        }
        // If mandatory OR auto-update is enabled, proceed immediately
        if (info.mandatory) {
            logger_1.logger.info('Mandatory upgrade — proceeding automatically');
            this.pendingUpgrade = null;
            this.performUpgrade(info);
        }
        else if (config_1.config.upgrade.autoUpdate) {
            logger_1.logger.info('Auto-update enabled — accepting upgrade');
            this.pendingUpgrade = null;
            // Report acceptance then proceed
            this.reportUpgradeResponse('accepted', version);
            this.performUpgrade(info);
        }
        else {
            // Store as pending — server or admin can force later
            this.pendingUpgrade = info;
            logger_1.logger.info('Auto-update disabled — upgrade stored as pending. Use force to apply.');
            this.reportUpgradeResponse('deferred', version);
        }
    }
    // ---- Called when server sends a direct upgrade command (force or admin-triggered) ----
    async handleUpgradeCommand(command) {
        // Normalize to UpgradeInfo
        const info = {
            targetVersion: command.targetVersion || command.version,
            packageUrl: command.packageUrl || command.downloadUrl,
            downloadUrl: command.downloadUrl || command.packageUrl,
            checksum: command.checksum,
            mandatory: command.mandatory !== undefined ? command.mandatory : true, // commands default to mandatory
            releaseNotes: command.releaseNotes,
        };
        logger_1.logger.info(`Upgrade COMMAND received (forced): v${version_1.AGENT_VERSION} -> v${info.targetVersion}`);
        if (this.upgrading) {
            if (this.upgradingVersion && this.upgradingVersion === info.targetVersion) {
                logger_1.logger.info(`Duplicate upgrade command ignored for v${info.targetVersion} (already in progress)`);
                return;
            }
            logger_1.logger.warn(`Upgrade already in progress (${this.upgradingVersion || 'unknown target'})`);
            return;
        }
        this.pendingUpgrade = null;
        await this.performUpgrade(info);
    }
    // ---- Accept a previously deferred upgrade ----
    acceptPendingUpgrade() {
        if (!this.pendingUpgrade) {
            logger_1.logger.warn('No pending upgrade to accept');
            return false;
        }
        const info = this.pendingUpgrade;
        this.pendingUpgrade = null;
        this.reportUpgradeResponse('accepted', info.targetVersion);
        this.performUpgrade(info);
        return true;
    }
    // ---- Reject a previously deferred upgrade ----
    rejectPendingUpgrade() {
        if (!this.pendingUpgrade) {
            logger_1.logger.warn('No pending upgrade to reject');
            return false;
        }
        const version = this.pendingUpgrade.targetVersion;
        this.pendingUpgrade = null;
        logger_1.logger.info(`Upgrade to v${version} rejected by agent`);
        this.reportUpgradeResponse('rejected', version);
        return true;
    }
    getPendingUpgrade() {
        return this.pendingUpgrade;
    }
    isUpgrading() {
        return this.upgrading;
    }
    // ---- Core upgrade logic ----
    async performUpgrade(info) {
        if (this.upgrading) {
            logger_1.logger.warn(`Upgrade already in progress (${this.upgradingVersion || 'unknown target'})`);
            return;
        }
        const version = info.targetVersion;
        this.upgrading = true;
        this.upgradingVersion = version;
        this.reportStatus('downloading', version, 0);
        try {
            // 1. Build download URL candidates (packageUrl first, then fallback downloadUrl)
            const downloadCandidates = this.buildDownloadCandidates(info);
            if (downloadCandidates.length === 0) {
                throw new Error('No valid package download URL provided');
            }
            // 2. Download the package
            const downloadPath = path.join(config_1.config.upgrade.installPath, 'updates');
            if (!fs.existsSync(downloadPath))
                fs.mkdirSync(downloadPath, { recursive: true });
            const ext = downloadCandidates[0].endsWith('.zip') ? '.zip' : '.tar.gz';
            const packageFile = path.join(downloadPath, `appstats-agent-${version}${ext}`);
            const downloadErrors = [];
            let downloadedFrom = '';
            for (const candidate of downloadCandidates) {
                try {
                    logger_1.logger.info(`Downloading from: ${candidate}`);
                    await this.downloadPackage(candidate, packageFile);
                    downloadedFrom = candidate;
                    break;
                }
                catch (downloadError) {
                    const message = downloadError?.message || String(downloadError);
                    downloadErrors.push(`${candidate} (${message})`);
                    logger_1.logger.warn(`Upgrade download attempt failed: ${message}`);
                    try {
                        if (fs.existsSync(packageFile))
                            fs.unlinkSync(packageFile);
                    }
                    catch { }
                }
            }
            if (!downloadedFrom) {
                throw new Error(`All package download attempts failed: ${downloadErrors.join(' | ')}`);
            }
            this.reportStatus('downloading', version, 50);
            // 3. Verify checksum (skip if server didn't provide one)
            if (info.checksum) {
                this.reportStatus('verifying', version, 55);
                const fileChecksum = await this.calculateChecksum(packageFile);
                if (fileChecksum !== info.checksum) {
                    throw new Error(`Checksum mismatch: expected ${info.checksum}, got ${fileChecksum}`);
                }
                logger_1.logger.info('Checksum verified');
            }
            else {
                logger_1.logger.info('No checksum provided, skipping verification');
            }
            // 4. Extract
            this.reportStatus('extracting', version, 60);
            const extractDir = path.join(downloadPath, `v${version}`);
            if (fs.existsSync(extractDir)) {
                this.removeRecursive(extractDir);
            }
            await this.extractPackage(packageFile, extractDir);
            this.reportStatus('extracting', version, 75);
            // 5. Backup current version
            this.reportStatus('installing', version, 80);
            const backupDir = path.join(config_1.config.upgrade.installPath, 'backup', `v${version_1.AGENT_VERSION}`);
            await this.backupCurrent(backupDir);
            // 6. Replace files (best-effort while process is running)
            this.reportStatus('replacing', version, 85);
            try {
                await this.replaceFiles(extractDir, config_1.config.upgrade.installPath);
            }
            catch (replaceErr) {
                logger_1.logger.warn(`In-process file replace failed (will retry in restart script): ${replaceErr.message}`);
            }
            // 7. Clean up downloaded package (keep extractDir for restart script fallback)
            try {
                fs.unlinkSync(packageFile);
            }
            catch { }
            this.reportStatus('completed', version, 100);
            logger_1.logger.info(`Upgrade to v${version} completed successfully`);
            // 8. Restart — the restart script will also copy files from extractDir
            //    as a reliable fallback in case in-process replace failed
            this.reportStatus('restarting', version, 100);
            logger_1.logger.info('Restarting agent in 3 seconds...');
            setTimeout(() => {
                this.restartAgent(extractDir);
            }, 3000);
        }
        catch (error) {
            logger_1.logger.error(`Upgrade to v${version} failed:`, error);
            this.reportStatus('failed', version, 0, error.message);
            this.upgrading = false;
            this.upgradingVersion = null;
        }
    }
    normalizeBaseUrl(value) {
        const raw = String(value || '').trim();
        if (!raw)
            return '';
        try {
            const parsed = new URL(raw);
            return `${parsed.protocol}//${parsed.host}`.replace(/\/+$/, '');
        }
        catch {
            return '';
        }
    }
    deriveHttpBaseFromWsUrl(value) {
        const raw = String(value || '').trim();
        if (!raw)
            return '';
        try {
            const parsed = new URL(raw);
            if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:')
                return '';
            const protocol = parsed.protocol === 'wss:' ? 'https:' : 'http:';
            let port = parsed.port || '';
            if (port === '3001')
                port = '3000';
            const host = parsed.hostname;
            const hostPort = port ? `${host}:${port}` : host;
            return `${protocol}//${hostPort}`.replace(/\/+$/, '');
        }
        catch {
            return '';
        }
    }
    buildDownloadCandidates(info) {
        const baseCandidates = Array.from(new Set([
            this.normalizeBaseUrl(config_1.config.server.url),
            this.normalizeBaseUrl(config_1.config.server.primaryUrl),
            this.normalizeBaseUrl(config_1.config.server.secondaryUrl),
            this.deriveHttpBaseFromWsUrl(config_1.config.server.wsUrl),
            this.deriveHttpBaseFromWsUrl(config_1.config.server.primaryWsUrl),
            this.deriveHttpBaseFromWsUrl(config_1.config.server.secondaryWsUrl),
        ].filter(Boolean)));
        const values = [info.packageUrl, info.downloadUrl];
        const candidates = [];
        for (const raw of values) {
            const value = String(raw || '').trim();
            if (!value)
                continue;
            if (/^https?:\/\//i.test(value)) {
                candidates.push(value);
                try {
                    const parsed = new URL(value);
                    const rel = `${parsed.pathname || ''}${parsed.search || ''}`;
                    if (rel) {
                        for (const base of baseCandidates) {
                            candidates.push(`${base}${rel.startsWith('/') ? rel : `/${rel}`}`);
                        }
                    }
                }
                catch { }
                continue;
            }
            for (const base of baseCandidates) {
                candidates.push(`${base}${value.startsWith('/') ? value : `/${value}`}`);
            }
        }
        return Array.from(new Set(candidates));
    }
    // ---- Download using native http/https (no node-fetch dependency) ----
    downloadPackage(url, dest) {
        return new Promise((resolve, reject) => {
            const REQUEST_TIMEOUT_MS = Math.max(45000, parseInt(String(process.env.APPSTATS_UPGRADE_REQUEST_TIMEOUT_MS || '180000'), 10) || 180000);
            const IDLE_TIMEOUT_MS = Math.max(90000, parseInt(String(process.env.APPSTATS_UPGRADE_IDLE_TIMEOUT_MS || '240000'), 10) || 240000);
            const MAX_REDIRECTS = 5;
            const fileStream = fs.createWriteStream(dest, { flags: 'w' });
            let totalBytes = 0;
            let settled = false;
            let activeReq = null;
            let idleTimer = null;
            const finish = (error) => {
                if (settled)
                    return;
                settled = true;
                if (idleTimer) {
                    clearTimeout(idleTimer);
                    idleTimer = null;
                }
                if (activeReq) {
                    try {
                        activeReq.destroy();
                    }
                    catch { }
                    activeReq = null;
                }
                if (error) {
                    try {
                        fileStream.destroy();
                    }
                    catch { }
                    fs.unlink(dest, () => reject(error));
                    return;
                }
                resolve();
            };
            const refreshIdleTimeout = () => {
                if (idleTimer)
                    clearTimeout(idleTimer);
                idleTimer = setTimeout(() => {
                    finish(new Error(`Download timed out after ${IDLE_TIMEOUT_MS}ms of inactivity`));
                }, IDLE_TIMEOUT_MS);
            };
            fileStream.on('error', (err) => finish(err));
            fileStream.on('finish', () => {
                fileStream.close();
                logger_1.logger.info(`Downloaded ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
                finish();
            });
            const makeRequest = (requestUrl, redirectCount = 0) => {
                if (settled)
                    return;
                if (redirectCount > MAX_REDIRECTS) {
                    finish(new Error('Too many redirects'));
                    return;
                }
                let urlObj;
                try {
                    urlObj = new URL(requestUrl);
                }
                catch {
                    finish(new Error(`Invalid download URL: ${requestUrl}`));
                    return;
                }
                const options = {
                    hostname: urlObj.hostname,
                    port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
                    path: urlObj.pathname + urlObj.search,
                    method: 'GET',
                    headers: {
                        'X-Agent-Token': config_1.config.server.token,
                        'User-Agent': `AppStats-Agent/${version_1.AGENT_VERSION}`,
                    },
                };
                const reqProto = urlObj.protocol === 'https:' ? https : http;
                const req = reqProto.request(options, (response) => {
                    // Follow redirects
                    if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
                        response.resume();
                        const redirectUrl = new URL(response.headers.location, requestUrl).toString();
                        makeRequest(redirectUrl, redirectCount + 1);
                        return;
                    }
                    if (response.statusCode !== 200) {
                        response.resume();
                        finish(new Error(`Download failed: HTTP ${response.statusCode}`));
                        return;
                    }
                    refreshIdleTimeout();
                    response.on('data', (chunk) => {
                        totalBytes += chunk.length;
                        refreshIdleTimeout();
                    });
                    response.on('error', (err) => finish(err));
                    response.pipe(fileStream, { end: true });
                });
                activeReq = req;
                req.setTimeout(REQUEST_TIMEOUT_MS, () => {
                    req.destroy(new Error(`Request timeout after ${REQUEST_TIMEOUT_MS}ms`));
                });
                req.on('error', (err) => finish(err));
                req.end();
            };
            makeRequest(url);
        });
    }
    calculateChecksum(filePath) {
        return new Promise((resolve, reject) => {
            const hash = crypto.createHash('sha256');
            const stream = fs.createReadStream(filePath);
            stream.on('data', (data) => hash.update(data));
            stream.on('end', () => resolve(hash.digest('hex')));
            stream.on('error', reject);
        });
    }
    async extractPackage(packageFile, destDir) {
        if (!fs.existsSync(destDir))
            fs.mkdirSync(destDir, { recursive: true });
        if (packageFile.endsWith('.zip')) {
            if (process.platform === 'win32') {
                // Try multiple extraction methods on Windows for maximum compatibility
                const methods = [
                    // Method 1: tar (available on Windows 10 1803+)
                    `tar -xf "${packageFile}" -C "${destDir}"`,
                    // Method 2: .NET ZipFile (works on PowerShell 2.0+)
                    `powershell -NoProfile -Command "Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${packageFile}', '${destDir}')"`,
                    // Method 3: Expand-Archive (PowerShell 5.0+)
                    `powershell -NoProfile -Command "Expand-Archive -Path '${packageFile}' -DestinationPath '${destDir}' -Force"`,
                ];
                let lastErr = null;
                for (let i = 0; i < methods.length; i++) {
                    try {
                        logger_1.logger.info(`ZIP extraction attempt ${i + 1}/${methods.length}`);
                        await new Promise((resolve, reject) => {
                            (0, child_process_1.exec)(methods[i], { timeout: 180000 }, (err, stdout, stderr) => {
                                if (err) {
                                    logger_1.logger.warn(`Extraction method ${i + 1} failed: ${stderr || err.message}`);
                                    reject(err);
                                }
                                else {
                                    resolve();
                                }
                            });
                        });
                        logger_1.logger.info(`ZIP extraction succeeded with method ${i + 1}`);
                        return; // success
                    }
                    catch (err) {
                        lastErr = err;
                    }
                }
                throw lastErr || new Error('All ZIP extraction methods failed');
            }
            else {
                // Linux/macOS: prefer native tar module, fallback to unzip command.
                try {
                    await tar.x({
                        file: packageFile,
                        cwd: destDir,
                        preservePaths: false,
                        strict: false,
                        onentry: (entry) => {
                            // No-op hook keeps extraction resilient for odd metadata records.
                            void entry;
                        },
                    });
                    return;
                }
                catch (tarError) {
                    logger_1.logger.warn(`ZIP extraction via tar module failed, falling back to unzip: ${tarError?.message || String(tarError)}`);
                }
                await new Promise((resolve, reject) => {
                    (0, child_process_1.exec)(`unzip -o "${packageFile}" -d "${destDir}"`, { timeout: 120000 }, (err) => {
                        if (err)
                            reject(err);
                        else
                            resolve();
                    });
                });
            }
        }
        else {
            // tar.gz
            try {
                await tar.x({
                    file: packageFile,
                    cwd: destDir,
                    gzip: true,
                    preservePaths: false,
                    strict: false,
                    onentry: (entry) => {
                        void entry;
                    },
                });
            }
            catch (moduleError) {
                logger_1.logger.warn(`tar.gz extraction via tar module failed, falling back to system tar: ${moduleError?.message || String(moduleError)}`);
                await new Promise((resolve, reject) => {
                    (0, child_process_1.exec)(`tar -xzf "${packageFile}" -C "${destDir}"`, { timeout: 120000 }, (err) => {
                        if (err)
                            reject(err);
                        else
                            resolve();
                    });
                });
            }
        }
    }
    async backupCurrent(backupDir) {
        if (!fs.existsSync(backupDir))
            fs.mkdirSync(backupDir, { recursive: true });
        const installPath = config_1.config.upgrade.installPath;
        const filesToBackup = ['package.json', 'dist'];
        for (const file of filesToBackup) {
            const src = path.join(installPath, file);
            const dest = path.join(backupDir, file);
            if (fs.existsSync(src)) {
                try {
                    await this.copyRecursive(src, dest);
                }
                catch (e) {
                    logger_1.logger.warn(`Could not backup ${file}: ${e.message}`);
                }
            }
        }
        logger_1.logger.info(`Backed up to ${backupDir}`);
    }
    async replaceFiles(srcDir, destDir) {
        const entries = fs.readdirSync(srcDir, { withFileTypes: true });
        for (const entry of entries) {
            const src = path.join(srcDir, entry.name);
            const dest = path.join(destDir, entry.name);
            // Never overwrite config, .env, logs, backup, or updates.
            if (['agent-config.json', '.env', 'logs', 'backup', 'updates'].includes(entry.name))
                continue;
            if (entry.isDirectory()) {
                if (!fs.existsSync(dest))
                    fs.mkdirSync(dest, { recursive: true });
                await this.replaceFiles(src, dest);
            }
            else {
                fs.copyFileSync(src, dest);
            }
        }
    }
    async copyRecursive(src, dest) {
        const stat = fs.statSync(src);
        if (stat.isDirectory()) {
            if (!fs.existsSync(dest))
                fs.mkdirSync(dest, { recursive: true });
            const entries = fs.readdirSync(src);
            for (const entry of entries) {
                await this.copyRecursive(path.join(src, entry), path.join(dest, entry));
            }
        }
        else {
            fs.copyFileSync(src, dest);
        }
    }
    getPathSize(targetPath) {
        try {
            const stat = fs.statSync(targetPath);
            if (stat.isFile())
                return stat.size;
            if (!stat.isDirectory())
                return 0;
            let total = 0;
            for (const entry of fs.readdirSync(targetPath, { withFileTypes: true })) {
                total += this.getPathSize(path.join(targetPath, entry.name));
            }
            return total;
        }
        catch {
            return 0;
        }
    }
    removeRecursive(dir) {
        if (fs.existsSync(dir)) {
            fs.readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
                const full = path.join(dir, entry.name);
                if (entry.isDirectory())
                    this.removeRecursive(full);
                else
                    fs.unlinkSync(full);
            });
            fs.rmdirSync(dir);
        }
    }
    restartAgent(extractDir) {
        logger_1.logger.info('Restarting agent process...');
        const installPath = config_1.config.upgrade.installPath;
        // Build a file-copy command that runs AFTER the process stops
        // xcopy/robocopy on Windows, cp -r on Linux
        let copyCmd = '';
        let cleanCmd = '';
        if (extractDir) {
            if (process.platform === 'win32') {
                // robocopy copies directories; /E = recursive, /IS /IT = overwrite, /NFL /NDL /NJH /NJS = quiet
                copyCmd = `robocopy "${extractDir}" "${installPath}" /E /IS /IT /NFL /NDL /NJH /NJS /R:3 /W:2\nif errorlevel 8 echo [WARN] robocopy returned errorlevel %errorlevel%`;
                cleanCmd = `rmdir /s /q "${extractDir}" 2>nul`;
            }
            else {
                copyCmd = `cp -rf "${extractDir}"/* "${installPath}/"`;
                cleanCmd = `rm -rf "${extractDir}"`;
            }
        }
        if (process.platform === 'win32') {
            const serviceName = process.env.APPSTATS_AGENT_SERVICE_NAME || 'AppStatsAgent';
            // Single bat that probes sc to decide: service restart or direct node fallback.
            // Avoids unreliable isService env detection (ppid=1 is wrong on Windows).
            const restartScript = path.join(installPath, 'restart-agent.bat');
            const logFile = path.join(installPath, 'restart-agent.log');
            fs.writeFileSync(restartScript, `@echo off
set LOGFILE=${logFile}
echo [%date% %time%] Restart script started > "%LOGFILE%"
timeout /t 2 /nobreak >nul
${copyCmd ? copyCmd + '\n' : ''}${cleanCmd ? cleanCmd + '\n' : ''}echo [%date% %time%] Checking for Windows service: ${serviceName} >> "%LOGFILE%"
sc query "${serviceName}" >nul 2>nul
if %errorlevel% equ 0 (
  echo [%date% %time%] Service found, using net stop/start >> "%LOGFILE%"
  net stop "${serviceName}" >nul 2>nul
  timeout /t 3 /nobreak >nul
  net start "${serviceName}"
  if errorlevel 1 (
    echo [%date% %time%] First net start failed, retrying in 5s >> "%LOGFILE%"
    timeout /t 5 /nobreak >nul
    net start "${serviceName}"
    if errorlevel 1 (
      echo [%date% %time%] Second net start failed >> "%LOGFILE%"
    ) else (
      echo [%date% %time%] Service started on retry >> "%LOGFILE%"
    )
  ) else (
    echo [%date% %time%] Service started successfully >> "%LOGFILE%"
  )
  goto :done
)
echo [%date% %time%] Service not found, starting node directly >> "%LOGFILE%"
cd /d "${installPath}"
start "" /b node dist\\index.js
:done
echo [%date% %time%] Restart script complete >> "%LOGFILE%"
del "%~f0"
`);
            logger_1.logger.info(`Restarting via restart-agent.bat (service: ${serviceName})...`);
            (0, child_process_1.spawn)('cmd', ['/c', restartScript], { detached: true, stdio: 'ignore' }).unref();
        }
        else {
            const nodeExec = process.execPath.replace(/"/g, '\\"');
            const restartScript = path.join(installPath, 'restart.sh');
            fs.writeFileSync(restartScript, `#!/bin/bash
sleep 2
${copyCmd ? copyCmd + '\n' : ''}${cleanCmd ? cleanCmd + '\n' : ''}cd "${installPath}"
should_start_direct=1
restart_linux_service() {
  local svc="$1"
  if [ -z "$svc" ]; then
    return 1
  fi
  if command -v systemctl >/dev/null 2>&1; then
    if systemctl restart "$svc" >/dev/null 2>&1; then
      return 0
    fi
  fi
  if command -v service >/dev/null 2>&1; then
    if service "$svc" restart >/dev/null 2>&1; then
      return 0
    fi
  fi
  return 1
}
for svc in appstats-agent AppStatsAgent; do
  if restart_linux_service "$svc"; then
    should_start_direct=0
    break
  fi
done
if [ "$should_start_direct" = "1" ]; then
  "${nodeExec}" dist/index.js >/dev/null 2>&1 &
fi
rm -f "$0"
`, { mode: 0o755 });
            (0, child_process_1.spawn)('bash', [restartScript], { detached: true, stdio: 'ignore' }).unref();
        }
        process.exit(0);
    }
    reportStatus(status, version, progress, error) {
        this.wsClient.send({
            type: 'upgrade:status',
            payload: {
                agentId: this.wsClient.getAgentId(),
                fromVersion: version_1.AGENT_VERSION,
                toVersion: version,
                status,
                progress,
                startedAt: new Date().toISOString(),
                error,
            },
        });
    }
    reportUpgradeResponse(response, version) {
        this.wsClient.send({
            type: 'upgrade:response',
            payload: {
                agentId: this.wsClient.getAgentId(),
                currentVersion: version_1.AGENT_VERSION,
                targetVersion: version,
                response,
                autoUpdate: config_1.config.upgrade.autoUpdate,
                timestamp: new Date().toISOString(),
            },
        });
    }
    getCurrentVersion() {
        return version_1.AGENT_VERSION;
    }
}
exports.UpgradeService = UpgradeService;
//# sourceMappingURL=upgrade.js.map