"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.ProductivityCollector = void 0;
const logger_1 = require("../logger");
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const linux_desktop_1 = require("../utils/linux-desktop");
// Productivity scoring rules (can be overridden by server config)
const DEFAULT_PRODUCTIVITY_RULES = {
    // Productive
    'code': 'productive', 'visual studio': 'productive', 'terminal': 'productive',
    'powershell': 'productive', 'cmd': 'productive', 'git': 'productive',
    'node': 'productive', 'python': 'productive', 'docker': 'productive',
    'idea': 'productive', 'webstorm': 'productive', 'pycharm': 'productive',
    'sublime': 'productive', 'notepad++': 'productive', 'vim': 'productive',
    'winword': 'productive', 'excel': 'productive', 'powerpnt': 'productive',
    'figma': 'productive', 'photoshop': 'productive', 'illustrator': 'productive',
    // Neutral
    'teams': 'neutral', 'slack': 'neutral', 'zoom': 'neutral',
    'outlook': 'neutral', 'thunderbird': 'neutral', 'discord': 'neutral',
    'explorer': 'neutral', 'finder': 'neutral', 'chrome': 'neutral',
    'firefox': 'neutral', 'edge': 'neutral', 'msedge': 'neutral',
    // Unproductive
    'spotify': 'unproductive', 'netflix': 'unproductive', 'youtube': 'unproductive',
    'steam': 'unproductive', 'epic': 'unproductive',
};
class ProductivityCollector {
    constructor() {
        this.currentApp = null;
        this.currentTitle = null;
        this.currentPath = null;
        this.appStartTime = Date.now();
        this.pendingSessionSwitchCount = 0;
        this.lastCpuSecByPid = new Map();
        this.records = [];
        this.events = [];
        this.lastActivityTime = Date.now();
        this.lastIdleState = false;
        this.idleThreshold = 300000; // 5 minutes
        this.segmentDurationMs = 30000; // Emit records every 30s even without app switches
        this.productivityRules = { ...DEFAULT_PRODUCTIVITY_RULES };
        // Granular tracking config — all on by default
        this.trackingMode = 'full';
        this.trackActiveWindow = true;
        this.trackKeystrokesActivity = true;
        this.trackKeystrokesLog = false; // opt-in only — logs actual keystrokes
        this.trackMouseActivity = true;
        this.trackBrowserUrls = true;
        this.trackBrowserTitles = true;
        this.trackNetworkTraffic = true;
        this.trackScreenshots = false;
        this.screenshotIntervalSeconds = 60;
        this.lastScreenshotAt = 0;
        this.lastScreenshotFailureLogAt = 0;
        this.lastScreenshotFailureEventAt = 0;
        this.lastScreenshotFailureReason = '';
        this.screenshots = [];
        this.windowsScreenshotHelperExe = null;
        this.windowsScreenshotHelperBuildAttemptAt = 0;
        this.windowsScreenshotHelperVersion = 'v2';
        this.privacyAnonUsers = false;
        this.privacyBlurTitles = false;
        this.privacyExcludeUrls = false;
    }
    /** Called when server pushes a config update with tenant tracking settings */
    applyTrackingConfig(cfg) {
        const bool = (v, def) => {
            if (v === undefined || v === null)
                return def;
            if (typeof v === 'boolean')
                return v;
            if (typeof v === 'number')
                return v === 1;
            const s = String(v).trim().toLowerCase();
            if (s === '1' || s === 'true' || s === 'yes' || s === 'on')
                return true;
            if (s === '0' || s === 'false' || s === 'no' || s === 'off')
                return false;
            return def;
        };
        if (cfg.tracking_mode !== undefined) {
            this.trackingMode = cfg.tracking_mode || 'off';
        }
        this.trackActiveWindow = bool(cfg.track_active_window, true);
        this.trackKeystrokesActivity = bool(cfg.track_keystrokes_activity, true);
        this.trackKeystrokesLog = bool(cfg.track_keystrokes_log, false);
        this.trackMouseActivity = bool(cfg.track_mouse_activity, true);
        this.trackBrowserUrls = bool(cfg.track_browser_urls, true);
        this.trackBrowserTitles = bool(cfg.track_browser_titles, true);
        this.trackNetworkTraffic = bool(cfg.track_network_traffic, true);
        this.trackScreenshots = bool(cfg.track_screenshots, false);
        const intervalSeconds = Number(cfg.screenshot_interval_seconds);
        if (Number.isFinite(intervalSeconds) && intervalSeconds > 0) {
            this.screenshotIntervalSeconds = Math.floor(intervalSeconds);
        }
        this.privacyAnonUsers = bool(cfg.privacy_anon_users, false);
        this.privacyBlurTitles = bool(cfg.privacy_blur_titles, false);
        this.privacyExcludeUrls = bool(cfg.privacy_exclude_urls, false);
        logger_1.logger.info(`Tracking config applied: mode=${this.trackingMode}, activeWindow=${this.trackActiveWindow}, ksActivity=${this.trackKeystrokesActivity}, ksLog=${this.trackKeystrokesLog}, screenshots=${this.trackScreenshots}`);
    }
    getTrackingMode() { return this.trackingMode; }
    isTrackNetworkTraffic() { return this.trackNetworkTraffic && this.trackingMode !== 'off'; }
    isTrackBrowserUrls() { return this.trackBrowserUrls && this.trackingMode !== 'off'; }
    isTrackBrowserTitles() { return this.trackBrowserTitles && this.trackingMode !== 'off'; }
    async collectActiveWindow() {
        // Respect tracking mode
        if (this.trackingMode === 'off')
            return;
        try {
            const window = this.trackActiveWindow ? await this.getActiveWindow() : null;
            const now = Date.now();
            const idleMs = await this.getIdleTime();
            const isIdle = idleMs > this.idleThreshold;
            // Track idle state transitions
            if (isIdle !== this.lastIdleState) {
                this.lastIdleState = isIdle;
                this.recordIdleEvent(isIdle);
                if (!isIdle) {
                    this.lastActivityTime = now;
                }
            }
            if (!isIdle) {
                this.lastActivityTime = now;
            }
            if (!window) {
                this.flushCurrentAppSlice(now, isIdle);
                if (this.trackScreenshots) {
                    const nowTs = now;
                    const minIntervalMs = Math.max(30, this.screenshotIntervalSeconds) * 1000;
                    if (nowTs - this.lastScreenshotAt >= minIntervalMs) {
                        const shot = await this.captureScreenshot(null);
                        if (shot) {
                            this.lastScreenshotAt = nowTs;
                            this.screenshots.push(shot);
                            if (this.screenshots.length > 50)
                                this.screenshots = this.screenshots.slice(-50);
                        }
                        else {
                            // Retry failed captures sooner than the full screenshot interval.
                            this.lastScreenshotAt = nowTs - Math.max(0, minIntervalMs - 30000);
                            this.recordScreenshotFailureEvent();
                        }
                    }
                }
                return;
            }
            const nextOwner = String(window.owner || '').trim()
                || (window.path ? path.basename(window.path).replace(/\.[^.]+$/i, '') : '')
                || this.currentApp
                || 'unknown';
            const nextTitle = String(window.title || '').trim() || nextOwner;
            const nextPath = String(window.path || '').trim();
            // Detect app switch
            if (!this.currentApp) {
                this.currentApp = nextOwner;
                this.currentTitle = nextTitle;
                this.currentPath = nextPath || null;
                this.appStartTime = now;
                this.pendingSessionSwitchCount = 1;
            }
            else {
                this.flushCurrentAppSlice(now, isIdle);
                if (nextOwner !== this.currentApp || nextTitle !== this.currentTitle) {
                    this.events.push({
                        timestamp: new Date().toISOString(),
                        type: 'app_switch',
                        details: {
                            from: this.currentApp,
                            fromTitle: this.currentTitle,
                            to: nextOwner,
                            toTitle: nextTitle,
                        },
                    });
                    this.currentApp = nextOwner;
                    this.currentTitle = nextTitle;
                    this.currentPath = nextPath || null;
                    this.pendingSessionSwitchCount = 1;
                }
                else if (nextPath) {
                    this.currentPath = nextPath;
                }
            }
            if (this.trackScreenshots) {
                const nowTs = now;
                const minIntervalMs = Math.max(30, this.screenshotIntervalSeconds) * 1000;
                if (nowTs - this.lastScreenshotAt >= minIntervalMs) {
                    const shot = await this.captureScreenshot(window);
                    if (shot) {
                        this.lastScreenshotAt = nowTs;
                        this.screenshots.push(shot);
                        if (this.screenshots.length > 50)
                            this.screenshots = this.screenshots.slice(-50);
                    }
                    else {
                        // Retry failed captures sooner than the full screenshot interval.
                        this.lastScreenshotAt = nowTs - Math.max(0, minIntervalMs - 30000);
                        this.recordScreenshotFailureEvent();
                    }
                }
            }
        }
        catch (e) {
            // Active window detection can fail for various reasons
            logger_1.logger.debug('Active window detection error');
        }
    }
    flushCurrentAppSlice(now, isIdle) {
        if (!this.currentApp)
            return;
        const duration = now - this.appStartTime;
        if (duration <= 1000)
            return;
        this.records.push({
            timestamp: new Date(this.appStartTime).toISOString(),
            application: this.currentApp,
            windowTitle: this.currentTitle || '',
            path: this.currentPath || '',
            category: this.categorize(this.currentApp),
            productivityScore: this.score(this.currentApp, this.currentTitle || undefined),
            duration,
            idle: isIdle,
            switches: this.pendingSessionSwitchCount,
        });
        this.pendingSessionSwitchCount = 0;
        this.appStartTime = now;
    }
    recordScreenshotFailureEvent() {
        const nowTs = Date.now();
        if ((nowTs - this.lastScreenshotFailureEventAt) < 5 * 60 * 1000)
            return;
        this.lastScreenshotFailureEventAt = nowTs;
        this.events.push({
            timestamp: new Date().toISOString(),
            type: 'screenshot_capture_failed',
            details: {
                reason: this.lastScreenshotFailureReason || 'capture_returned_null',
            },
        });
    }
    async getActiveWindow() {
        if (process.platform === 'win32') {
            return this.getActiveWindowWindows();
        }
        else if (process.platform === 'darwin') {
            return this.getActiveWindowMac();
        }
        else {
            return this.getActiveWindowLinux();
        }
    }
    getActiveWindowWindows() {
        return new Promise((resolve) => {
            // Prefer tasklist heuristics first to avoid frequent PowerShell launches.
            (0, child_process_1.exec)('tasklist /V /FO CSV /NH', { timeout: 8000, windowsHide: true }, (err, stdout) => {
                if (!err && stdout.trim()) {
                    const result = this.parseTasklistOutput(stdout);
                    if (result) {
                        resolve(result);
                        return;
                    }
                }
                const allowPowerShellFallback = String(process.env.APPSTATS_ACTIVE_WINDOW_POWERSHELL || '').trim() === '1';
                if (!allowPowerShellFallback) {
                    if (this.currentApp) {
                        resolve({
                            title: this.currentTitle || this.currentApp,
                            owner: this.currentApp,
                        });
                        return;
                    }
                    resolve(null);
                    return;
                }
                // Last fallback: Get-Process window title scan.
                const script = [
                    '$p = Get-Process |',
                    '  Where-Object { $_.MainWindowTitle -ne \'\' -and $_.SessionId -ne 0 } |',
                    '  Sort-Object CPU -Descending |',
                    '  Select-Object -First 1;',
                    'if ($p) {',
                    '  "$($p.MainWindowTitle)|$($p.Name)|$(if($p.Path){$p.Path}else{""})|$($p.Id)"',
                    '} else {',
                    '  $p2 = Get-Process |',
                    '    Where-Object { $_.MainWindowTitle -ne \'\' } |',
                    '    Sort-Object CPU -Descending |',
                    '    Select-Object -First 1;',
                    '  if ($p2) {',
                    '    "$($p2.MainWindowTitle)|$($p2.Name)|$(if($p2.Path){$p2.Path}else{""})|$($p2.Id)"',
                    '  }',
                    '}',
                ].join(' ');
                (0, child_process_1.execFile)('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], { timeout: 5000, windowsHide: true }, (err2, stdout2) => {
                    if (err2 || !stdout2.trim()) {
                        resolve(null);
                        return;
                    }
                    const parts = stdout2.trim().split('|');
                    if (parts.length >= 2 && (parts[0] || parts[1])) {
                        resolve({
                            title: parts[0] || '',
                            owner: parts[1] || '',
                            path: parts[2] || undefined,
                            pid: parts[3] ? parseInt(parts[3]) : undefined,
                        });
                    }
                    else {
                        resolve(null);
                    }
                });
            });
        });
    }
    /**
     * Parse `tasklist /V /FO CSV /NH` output.
     * Columns: "Image Name","PID","Session Name","Session#","Mem Usage","Status","User Name","CPU Time","Window Title"
     * We pick the row with the highest CPU time among those with a non-N/A window title
     * in an interactive session (Session# > 0).
     */
    parseTasklistOutput(raw) {
        const lines = raw.split('\n').filter(l => l.trim());
        let bestWithTitle = null;
        let bestWithTitleRank = -Infinity;
        let bestLikelyInteractive = null;
        let bestLikelyInteractiveRank = -Infinity;
        let bestLikelyGlobal = null;
        let bestLikelyGlobalRank = -Infinity;
        let bestAnyInteractive = null;
        let bestAnyInteractiveRank = -Infinity;
        let bestAnyGlobal = null;
        let bestAnyGlobalRank = -Infinity;
        const nextCpuSecByPid = new Map();
        for (const line of lines) {
            // Parse CSV fields (handle quoted values with commas)
            const fields = [];
            let inQuote = false;
            let field = '';
            for (const ch of line) {
                if (ch === '"') {
                    inQuote = !inQuote;
                }
                else if (ch === ',' && !inQuote) {
                    fields.push(field);
                    field = '';
                }
                else {
                    field += ch;
                }
            }
            fields.push(field);
            if (fields.length < 9)
                continue;
            const [imageName, pidStr, , sessionNum, , , , cpuTime, windowTitle] = fields;
            const title = (windowTitle || '').trim();
            const session = parseInt(sessionNum || '0');
            // Parse CPU time "H:MM:SS" to seconds
            const cpuParts = (cpuTime || '0:00:00').split(':').map(Number);
            const cpuSec = (cpuParts[0] || 0) * 3600 + (cpuParts[1] || 0) * 60 + (cpuParts[2] || 0);
            const owner = (imageName || '').replace(/\.exe$/i, '');
            const normalizedOwner = owner.toLowerCase() === 'olk' ? 'outlook' :
                owner.toLowerCase() === 'winword' ? 'word' :
                    owner.toLowerCase() === 'powerpnt' ? 'powerpoint' :
                        owner;
            const pid = parseInt(pidStr || '0') || undefined;
            if (pid)
                nextCpuSecByPid.set(pid, cpuSec);
            const prevCpuSec = pid ? this.lastCpuSecByPid.get(pid) : undefined;
            const cpuDeltaSec = prevCpuSec === undefined ? 0 : Math.max(0, cpuSec - prevCpuSec);
            const ownerLower = owner.toLowerCase();
            const likelySystemProc = ownerLower === 'system idle process' ||
                ownerLower === 'idle' ||
                ownerLower === 'system' ||
                ownerLower === 'registry' ||
                ownerLower === 'memcompression' ||
                ownerLower === 'services' ||
                ownerLower === 'lsass' ||
                ownerLower === 'wininit' ||
                ownerLower === 'winlogon' ||
                ownerLower === 'csrss' ||
                ownerLower === 'smss' ||
                ownerLower === 'dwm' ||
                ownerLower === 'fontdrvhost' ||
                ownerLower === 'ctfmon' ||
                ownerLower === 'sihost' ||
                ownerLower === 'taskhostw' ||
                ownerLower === 'textinputhost' ||
                ownerLower === 'startmenuexperiencehost' ||
                ownerLower === 'shellexperiencehost' ||
                ownerLower === 'applicationframehost' ||
                ownerLower === 'searchhost' ||
                ownerLower === 'searchprotocolhost' ||
                ownerLower === 'searchfilterhost' ||
                ownerLower === 'runtimebroker' ||
                ownerLower === 'audiodg' ||
                ownerLower === 'wudfhost' ||
                ownerLower === 'spoolsv' ||
                ownerLower.startsWith('svchost');
            if (likelySystemProc)
                continue;
            const likelyBackgroundProc = ownerLower.includes('service') ||
                ownerLower.includes('daemon') ||
                ownerLower.includes('updater') ||
                ownerLower.includes('helper') ||
                ownerLower.includes('agent') ||
                ownerLower.includes('vpn') ||
                ownerLower.includes('wireguard') ||
                ownerLower.startsWith('forti') ||
                ownerLower.startsWith('wmi');
            const likelyUserApp = /(chrome|msedge|edge|firefox|brave|opera|vivaldi|explorer|code|devenv|idea|pycharm|webstorm|notepad|winword|excel|powerpnt|outlook|olk|teams|slack|zoom|discord|whatsapp|chatgpt|mstsc|powershell|cmd|terminal)/.test(ownerLower);
            const fallbackRank = (cpuDeltaSec * 10000) +
                (session > 0 ? 5000 : 0) +
                ((title && title !== 'N/A') ? 2000 : 0) +
                (likelyUserApp ? 1000 : 0) -
                (likelyBackgroundProc ? 1000 : 0) +
                ((this.currentApp && normalizedOwner.toLowerCase() === String(this.currentApp).toLowerCase()) ? 25 : 0);
            if (likelyUserApp && fallbackRank > bestLikelyGlobalRank) {
                bestLikelyGlobalRank = fallbackRank;
                bestLikelyGlobal = {
                    title: (!title || title === 'N/A') ? normalizedOwner : title,
                    owner: normalizedOwner,
                    pid,
                };
            }
            if (fallbackRank > bestAnyGlobalRank) {
                bestAnyGlobalRank = fallbackRank;
                bestAnyGlobal = {
                    title: (!title || title === 'N/A') ? normalizedOwner : title,
                    owner: normalizedOwner,
                    pid,
                };
            }
            if (session === 0)
                continue; // Prefer interactive sessions when available.
            if (likelyUserApp && fallbackRank > bestLikelyInteractiveRank) {
                bestLikelyInteractiveRank = fallbackRank;
                bestLikelyInteractive = {
                    title: (!title || title === 'N/A') ? normalizedOwner : title,
                    owner: normalizedOwner,
                    pid,
                };
            }
            if (fallbackRank > bestAnyInteractiveRank) {
                bestAnyInteractiveRank = fallbackRank;
                bestAnyInteractive = {
                    // Some Windows service contexts return N/A for all titles; fall back to process name.
                    title: (!title || title === 'N/A') ? normalizedOwner : title,
                    owner: normalizedOwner,
                    pid,
                };
            }
            if (title && title !== 'N/A' && session > 0 && fallbackRank > bestWithTitleRank) {
                bestWithTitleRank = fallbackRank;
                bestWithTitle = {
                    title,
                    owner: normalizedOwner,
                    pid,
                };
            }
        }
        this.lastCpuSecByPid = nextCpuSecByPid;
        return bestWithTitle || bestLikelyInteractive || bestAnyInteractive || bestLikelyGlobal || bestAnyGlobal;
    }
    getActiveWindowMac() {
        return new Promise((resolve) => {
            const script = `osascript -e 'tell application "System Events" to get {name, unix id} of first process whose frontmost is true' -e 'tell application "System Events" to get name of front window of first process whose frontmost is true'`;
            (0, child_process_1.exec)(script, { timeout: 3000 }, (err, stdout) => {
                if (err) {
                    resolve(null);
                    return;
                }
                const lines = stdout.trim().split('\n');
                resolve({
                    title: lines[1] || '',
                    owner: lines[0]?.split(',')[0]?.trim() || '',
                });
            });
        });
    }
    getActiveWindowLinux() {
        const readProcessMeta = (pid) => {
            if (!Number.isFinite(Number(pid)) || Number(pid) <= 0)
                return { owner: '' };
            const numericPid = Number(pid);
            let owner = '';
            let procPath = '';
            try {
                owner = String(fs.readFileSync(`/proc/${numericPid}/comm`, 'utf8') || '').trim();
            }
            catch { }
            try {
                procPath = fs.readlinkSync(`/proc/${numericPid}/exe`);
            }
            catch { }
            if (!owner && procPath)
                owner = path.basename(procPath).replace(/\.[^.]+$/i, '');
            return { owner, path: procPath || undefined };
        };
        const parseXdotoolWindow = (stdout) => {
            const lines = String(stdout || '').trim().split('\n').filter(Boolean);
            if (!lines.length)
                return null;
            const pidRaw = lines[lines.length - 1];
            const pid = /^\d+$/.test(String(pidRaw || '').trim()) ? parseInt(String(pidRaw).trim(), 10) : undefined;
            const title = (pid ? lines.slice(0, -1) : lines).join(' ').trim();
            const meta = readProcessMeta(pid);
            const owner = String(meta.owner || '').trim() || 'unknown';
            if (!title && !owner)
                return null;
            return { title: title || owner, owner, path: meta.path, pid };
        };
        const parseProcessFallback = (stdout) => {
            const lines = String(stdout || '').trim().split('\n').map((line) => line.trim()).filter(Boolean);
            let best = null;
            for (const line of lines) {
                const match = line.match(/^(\d+)\|([^|]+)\|([^|]+)\|(.*)$/);
                if (!match)
                    continue;
                const pid = parseInt(match[1], 10);
                const owner = String(match[2] || '').trim();
                const cpu = Number(match[3] || 0) || 0;
                if (!owner)
                    continue;
                const lower = owner.toLowerCase();
                if (/^(systemd|dbus|gnome-shell|Xorg|Xwayland|pulseaudio|pipewire|wireplumber|networkmanager|login|bash|sh|zsh|sudo|sshd|cron|polkit|cupsd|agetty|containerd|dockerd)$/.test(lower))
                    continue;
                const likelyUserApp = /(chrome|chromium|edge|msedge|firefox|brave|opera|code|cursor|idea|pycharm|webstorm|slack|teams|zoom|discord|thunderbird|libreoffice|soffice|obsidian|notion|figma|inkscape|gimp|spotify|vlc|steam|telegram)/.test(lower);
                const continuityBonus = this.currentApp && lower === String(this.currentApp).toLowerCase() ? 1.5 : 0;
                const rank = cpu + (likelyUserApp ? 2 : 0) + continuityBonus;
                if (!best || rank > best.rank) {
                    const meta = readProcessMeta(pid);
                    best = {
                        rank,
                        row: {
                            title: String(match[4] || '').trim() || meta.owner || owner,
                            owner: meta.owner || owner,
                            path: meta.path,
                            pid,
                        },
                    };
                }
            }
            return best?.row || null;
        };
        return (async () => {
            try {
                const xdotoolOutput = await (0, linux_desktop_1.runLinuxDesktopCommand)('xdotool getactivewindow getwindowname getwindowpid 2>/dev/null', 3000);
                const parsed = parseXdotoolWindow(xdotoolOutput);
                if (parsed)
                    return parsed;
            }
            catch { }
            try {
                const psOutput = await (0, linux_desktop_1.runLinuxDesktopCommand)("ps -eo pid=,comm=,%cpu=,args= --sort=-%cpu | head -n 80 | awk '{ pid=$1; comm=$2; cpu=$3; $1=\"\"; $2=\"\"; $3=\"\"; sub(/^   */, \"\"); print pid \"|\" comm \"|\" cpu \"|\" $0 }'", 4000);
                const parsed = parseProcessFallback(psOutput);
                if (parsed)
                    return parsed;
            }
            catch { }
            return null;
        })();
    }
    async captureScreenshot(activeWindow) {
        if (!this.trackScreenshots)
            return null;
        if (process.platform === 'win32') {
            return this.captureScreenshotWindows(activeWindow);
        }
        if (process.platform === 'linux') {
            return this.captureScreenshotLinux(activeWindow);
        }
        return null;
    }
    inferImageMimeType(buffer) {
        if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
            return 'image/png';
        }
        if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xd8) {
            return 'image/jpeg';
        }
        return 'application/octet-stream';
    }
    readImageDimensions(buffer, mimeType) {
        if (mimeType === 'image/png' && buffer.length >= 24) {
            return {
                width: buffer.readUInt32BE(16),
                height: buffer.readUInt32BE(20),
            };
        }
        if (mimeType === 'image/jpeg') {
            let offset = 2;
            while (offset + 9 < buffer.length) {
                if (buffer[offset] !== 0xff) {
                    offset += 1;
                    continue;
                }
                const marker = buffer[offset + 1];
                const segmentLength = buffer.readUInt16BE(offset + 2);
                if (marker >= 0xc0
                    && marker <= 0xcf
                    && marker !== 0xc4
                    && marker !== 0xc8
                    && marker !== 0xcc
                    && offset + 8 < buffer.length) {
                    return {
                        height: buffer.readUInt16BE(offset + 5),
                        width: buffer.readUInt16BE(offset + 7),
                    };
                }
                if (!segmentLength || offset + 2 + segmentLength > buffer.length)
                    break;
                offset += 2 + segmentLength;
            }
        }
        return {};
    }
    async captureScreenshotLinux(activeWindow) {
        const screenshotPath = path.join(os.tmpdir(), `appstats-linux-shot-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.png`);
        const quotedPath = (0, linux_desktop_1.quoteShellArg)(screenshotPath);
        const commands = [
            `if command -v gnome-screenshot >/dev/null 2>&1; then gnome-screenshot -f ${quotedPath} >/dev/null 2>&1; else exit 127; fi`,
            `if command -v gdbus >/dev/null 2>&1; then gdbus call --session --dest org.gnome.Shell.Screenshot --object-path /org/gnome/Shell/Screenshot --method org.gnome.Shell.Screenshot.Screenshot false false ${quotedPath} >/dev/null 2>&1; else exit 127; fi`,
            `if command -v grim >/dev/null 2>&1; then grim ${quotedPath} >/dev/null 2>&1; else exit 127; fi`,
            `if command -v spectacle >/dev/null 2>&1; then spectacle -b -n -o ${quotedPath} >/dev/null 2>&1; else exit 127; fi`,
            `if command -v scrot >/dev/null 2>&1; then scrot -z ${quotedPath} >/dev/null 2>&1; else exit 127; fi`,
            `if command -v maim >/dev/null 2>&1; then maim ${quotedPath} >/dev/null 2>&1; else exit 127; fi`,
            `if command -v ffmpeg >/dev/null 2>&1 && [ -n "\${DISPLAY:-}" ]; then ffmpeg -hide_banner -loglevel error -y -f x11grab -framerate 1 -i "$DISPLAY" -frames:v 1 ${quotedPath} >/dev/null 2>&1; else exit 127; fi`,
            `if command -v import >/dev/null 2>&1; then import -window root ${quotedPath} >/dev/null 2>&1; else exit 127; fi`,
        ];
        let captureFailure = 'linux_screenshot_backend_unavailable';
        try {
            for (const command of commands) {
                try {
                    await (0, linux_desktop_1.runLinuxDesktopCommand)(command, 15000);
                    if (fs.existsSync(screenshotPath) && fs.statSync(screenshotPath).size > 0) {
                        const imageBuffer = fs.readFileSync(screenshotPath);
                        const mimeType = this.inferImageMimeType(imageBuffer);
                        const dimensions = this.readImageDimensions(imageBuffer, mimeType);
                        this.lastScreenshotFailureReason = '';
                        return {
                            timestamp: new Date().toISOString(),
                            mimeType,
                            imageBase64: imageBuffer.toString('base64'),
                            width: dimensions.width,
                            height: dimensions.height,
                            application: activeWindow?.owner || undefined,
                            windowTitle: activeWindow?.title || undefined,
                            isForeground: true,
                        };
                    }
                }
                catch (error) {
                    captureFailure = String(error?.message || error || 'linux_screenshot_failed').trim().slice(0, 450);
                }
            }
            this.lastScreenshotFailureReason = captureFailure;
            return null;
        }
        finally {
            try {
                if (fs.existsSync(screenshotPath))
                    fs.unlinkSync(screenshotPath);
            }
            catch { }
        }
    }
    ensureWindowsScreenshotHelper() {
        if (process.platform !== 'win32')
            return null;
        const helperDirCandidates = Array.from(new Set([
            path.join(process.cwd(), 'bin'),
            path.join(String(process.env.PROGRAMDATA || '').trim(), 'AppStats', 'bin'),
            path.join(os.tmpdir(), 'appstats-bin'),
        ].map((value) => String(value || '').trim()).filter(Boolean)));
        const helperExeCandidates = helperDirCandidates.map((dir) => {
            try {
                fs.mkdirSync(dir, { recursive: true });
            }
            catch { }
            return path.join(dir, `appstats-screenshot-helper-${this.windowsScreenshotHelperVersion}.exe`);
        });
        if (this.windowsScreenshotHelperExe && fs.existsSync(this.windowsScreenshotHelperExe)) {
            return this.windowsScreenshotHelperExe;
        }
        for (const candidate of helperExeCandidates) {
            if (fs.existsSync(candidate)) {
                this.windowsScreenshotHelperExe = candidate;
                return candidate;
            }
        }
        const nowTs = Date.now();
        if ((nowTs - this.windowsScreenshotHelperBuildAttemptAt) < 2 * 60 * 1000)
            return null;
        this.windowsScreenshotHelperBuildAttemptAt = nowTs;
        if (!helperExeCandidates.length)
            return null;
        const helperSourcePath = path.join(os.tmpdir(), 'appstats-screenshot-helper.cs');
        const helperSource = [
            'using System;',
            'using System.Drawing;',
            'using System.Drawing.Drawing2D;',
            'using System.Drawing.Imaging;',
            'using System.IO;',
            'using System.Windows.Forms;',
            'public static class AppStatsScreenshotHelper {',
            '  private static ImageCodecInfo GetJpegCodec() {',
            '    foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders()) {',
            '      if (string.Equals(codec.MimeType, "image/jpeg", StringComparison.OrdinalIgnoreCase)) return codec;',
            '    }',
            '    return null;',
            '  }',
            '  public static int Main(string[] args) {',
            '    if (args == null || args.Length < 3) return 2;',
            '    string outputPath = args[0];',
            '    string metaPath = args[1];',
            '    string errorPath = args[2];',
            '    try {',
            '      Rectangle bounds = SystemInformation.VirtualScreen;',
            '      if (bounds.Width <= 0 || bounds.Height <= 0) bounds = Screen.PrimaryScreen.Bounds;',
            '      if (bounds.Width <= 0 || bounds.Height <= 0) return 3;',
            '      using (Bitmap srcBmp = new Bitmap(bounds.Width, bounds.Height)) {',
            '        using (Graphics gfx = Graphics.FromImage(srcBmp)) {',
            '          gfx.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);',
            '        }',
            '        int maxW = 3840;',
            '        int maxH = 2160;',
            '        double scaleW = (double)maxW / (double)bounds.Width;',
            '        double scaleH = (double)maxH / (double)bounds.Height;',
            '        double scale = Math.Min(1.0d, Math.Min(scaleW, scaleH));',
            '        int outW = Math.Max(1, (int)Math.Round(bounds.Width * scale));',
            '        int outH = Math.Max(1, (int)Math.Round(bounds.Height * scale));',
            '        Bitmap finalBmp = srcBmp;',
            '        bool disposeFinal = false;',
            '        try {',
            '          if (scale < 1.0d) {',
            '            Bitmap scaled = new Bitmap(outW, outH);',
            '            using (Graphics sg = Graphics.FromImage(scaled)) {',
            '              sg.CompositingQuality = CompositingQuality.HighQuality;',
            '              sg.InterpolationMode = InterpolationMode.HighQualityBicubic;',
            '              sg.SmoothingMode = SmoothingMode.HighQuality;',
            '              sg.DrawImage(srcBmp, 0, 0, outW, outH);',
            '            }',
            '            finalBmp = scaled;',
            '            disposeFinal = true;',
            '          }',
            '          string mime = "image/jpeg";',
            '          ImageCodecInfo codec = GetJpegCodec();',
            '          if (codec != null) {',
            '            using (EncoderParameters ep = new EncoderParameters(1)) {',
            '              ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 55L);',
            '              finalBmp.Save(outputPath, codec, ep);',
            '            }',
            '          } else {',
            '            mime = "image/png";',
            '            finalBmp.Save(outputPath, ImageFormat.Png);',
            '          }',
            '          File.WriteAllText(metaPath, outW.ToString() + "|" + outH.ToString() + "|" + mime);',
            '          Console.WriteLine(outW.ToString() + "|" + outH.ToString() + "|" + mime);',
            '        } finally {',
            '          if (disposeFinal && finalBmp != null) finalBmp.Dispose();',
            '        }',
            '      }',
            '      return 0;',
            '    } catch (Exception ex) {',
            '      try { if (!string.IsNullOrEmpty(errorPath)) File.WriteAllText(errorPath, ex.ToString()); } catch {}',
            '      return 1;',
            '    }',
            '  }',
            '}',
            '',
        ].join('\n');
        try {
            fs.writeFileSync(helperSourcePath, helperSource, 'utf8');
        }
        catch {
            return null;
        }
        const winDir = process.env.WINDIR || 'C:\\Windows';
        const compilerCandidates = [
            path.join(winDir, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe'),
            path.join(winDir, 'Microsoft.NET', 'Framework', 'v4.0.30319', 'csc.exe'),
            path.join(winDir, 'Microsoft.NET', 'Framework64', 'v3.5', 'csc.exe'),
            path.join(winDir, 'Microsoft.NET', 'Framework', 'v3.5', 'csc.exe'),
            path.join(winDir, 'Microsoft.NET', 'Framework64', 'v2.0.50727', 'csc.exe'),
            path.join(winDir, 'Microsoft.NET', 'Framework', 'v2.0.50727', 'csc.exe'),
        ];
        try {
            const whereOutput = String((0, child_process_1.execFileSync)('where.exe', ['csc.exe'], {
                timeout: 5000,
                windowsHide: true,
            }) || '');
            for (const line of whereOutput.split(/\r?\n/)) {
                const candidate = String(line || '').trim();
                if (candidate)
                    compilerCandidates.push(candidate);
            }
        }
        catch { }
        const uniqueCompilers = Array.from(new Set(compilerCandidates.map((value) => String(value || '').trim()).filter(Boolean)));
        for (const compilerPath of uniqueCompilers) {
            if (!fs.existsSync(compilerPath))
                continue;
            for (const helperExe of helperExeCandidates) {
                try {
                    (0, child_process_1.execFileSync)(compilerPath, [
                        '/nologo',
                        '/target:winexe',
                        `/out:${helperExe}`,
                        '/r:System.Drawing.dll',
                        '/r:System.Windows.Forms.dll',
                        helperSourcePath,
                    ], { timeout: 30000, windowsHide: true });
                    if (fs.existsSync(helperExe)) {
                        this.windowsScreenshotHelperExe = helperExe;
                        return helperExe;
                    }
                }
                catch { }
            }
        }
        return null;
    }
    buildWindowsScreenshotHelperCommand(tempFile, metaFile, errorFile) {
        const helperExe = this.ensureWindowsScreenshotHelper();
        if (!helperExe)
            return null;
        const quote = (value) => `"${String(value || '').replace(/"/g, '""')}"`;
        return `${quote(helperExe)} ${quote(tempFile)} ${quote(metaFile)} ${quote(errorFile)}`;
    }
    buildWindowsScreenshotInlineCommand(tempFile, metaFile) {
        const safePath = tempFile.replace(/'/g, "''");
        const safeMetaPath = metaFile.replace(/'/g, "''");
        const ps = [
            'Add-Type -AssemblyName System.Windows.Forms;',
            'Add-Type -AssemblyName System.Drawing;',
            '$b=[System.Windows.Forms.SystemInformation]::VirtualScreen;',
            'if($b.Width -le 0 -or $b.Height -le 0){$b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds};',
            'if($b.Width -le 0 -or $b.Height -le 0){exit 2};',
            '$bmp=New-Object System.Drawing.Bitmap $b.Width,$b.Height;',
            '$g=[System.Drawing.Graphics]::FromImage($bmp);',
            '$g.CopyFromScreen($b.Location,[System.Drawing.Point]::Empty,$b.Size);',
            '$maxW=3840;$maxH=2160;',
            '$scale=[Math]::Min(1.0,[Math]::Min($maxW/[double]$b.Width,$maxH/[double]$b.Height));',
            '$outW=[Math]::Max(1,[int][Math]::Round($b.Width*$scale));',
            '$outH=[Math]::Max(1,[int][Math]::Round($b.Height*$scale));',
            '$final=$bmp;',
            'if($scale -lt 1.0){',
            '  $scaled=New-Object System.Drawing.Bitmap $outW,$outH;',
            '  $sg=[System.Drawing.Graphics]::FromImage($scaled);',
            '  $sg.CompositingQuality=[System.Drawing.Drawing2D.CompositingQuality]::HighQuality;',
            '  $sg.InterpolationMode=[System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic;',
            '  $sg.SmoothingMode=[System.Drawing.Drawing2D.SmoothingMode]::HighQuality;',
            '  $sg.DrawImage($bmp,0,0,$outW,$outH);',
            '  $sg.Dispose();',
            '  $final=$scaled;',
            '};',
            "$codec=[System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders()|Where-Object { $_.MimeType -eq 'image/jpeg' };",
            "$mime='image/jpeg';",
            'if($codec){',
            '  $ep=New-Object System.Drawing.Imaging.EncoderParameters 1;',
            '  $ep.Param[0]=New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality,55L);',
            `  $final.Save('${safePath}',$codec,$ep);`,
            '  $ep.Dispose();',
            '}else{',
            `  $final.Save('${safePath}',[System.Drawing.Imaging.ImageFormat]::Png);`,
            "  $mime='image/png';",
            '};',
            'if($final -ne $bmp){$final.Dispose()};',
            '$g.Dispose();',
            '$bmp.Dispose();',
            ` [System.IO.File]::WriteAllText('${safeMetaPath}',($outW.ToString() + '|' + $outH.ToString() + '|' + $mime));`,
        ].join('');
        const encoded = Buffer.from(ps, 'utf16le').toString('base64');
        return `powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -STA -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
    }
    captureScreenshotWindows(activeWindow) {
        return new Promise((resolve) => {
            const tempFile = path.join(os.tmpdir(), `appstats-shot-${Date.now()}-${Math.random().toString(16).slice(2)}.jpg`);
            const scriptFile = path.join(os.tmpdir(), `appstats-shot-${Date.now()}-${Math.random().toString(16).slice(2)}.ps1`);
            const errorFile = path.join(os.tmpdir(), `appstats-shot-${Date.now()}-${Math.random().toString(16).slice(2)}.err.txt`);
            const metaFile = path.join(os.tmpdir(), `appstats-shot-${Date.now()}-${Math.random().toString(16).slice(2)}.meta.txt`);
            const helperTaskCommand = this.buildWindowsScreenshotHelperCommand(tempFile, metaFile, errorFile);
            if (helperTaskCommand) {
                this.captureScreenshotWindowsViaTask(scriptFile, tempFile, errorFile, metaFile, activeWindow, helperTaskCommand).then(resolve).catch(() => resolve(null));
                return;
            }
            const preferTaskFallback = String(process.env.APPSTATS_SCREENSHOT_DIRECT_PS || '').trim() !== '1';
            if (preferTaskFallback) {
                this.captureScreenshotWindowsViaTask(scriptFile, tempFile, errorFile, metaFile, activeWindow, this.buildWindowsScreenshotInlineCommand(tempFile, metaFile)).then(resolve).catch(() => resolve(null));
                return;
            }
            const allowPowerShellFallback = String(process.env.APPSTATS_SCREENSHOT_ALLOW_POWERSHELL || '').trim() === '1';
            if (!allowPowerShellFallback) {
                this.lastScreenshotFailureReason = 'screenshot_direct_powershell_disabled';
                resolve(null);
                return;
            }
            const safePath = tempFile.replace(/'/g, "''");
            const safeErrPath = errorFile.replace(/'/g, "''");
            const safeMetaPath = metaFile.replace(/'/g, "''");
            const script = [
                'try {',
                '  Add-Type -AssemblyName System.Windows.Forms;',
                '  Add-Type -AssemblyName System.Drawing;',
                '  [System.Windows.Forms.SystemInformation]::VirtualScreen | Out-Null;',
                '  $bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen;',
                '  if ($bounds.Width -le 0 -or $bounds.Height -le 0) { $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds }',
                '  if ($bounds.Width -le 0 -or $bounds.Height -le 0) { exit 2 }',
                '  $srcBmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height;',
                '  try {',
                '    $gfx = [System.Drawing.Graphics]::FromImage($srcBmp);',
                '    $gfx.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size);',
                '  } catch {',
                '    try { $gfx.Dispose(); } catch {}',
                '    try { $srcBmp.Dispose(); } catch {}',
                '    exit 3',
                '  }',
                '  $maxW = 3840;',
                '  $maxH = 2160;',
                '  $scaleW = $maxW / [double]$bounds.Width;',
                '  $scaleH = $maxH / [double]$bounds.Height;',
                '  $scale = [Math]::Min(1.0, [Math]::Min($scaleW, $scaleH));',
                '  $outW = [Math]::Max(1, [int][Math]::Round($bounds.Width * $scale));',
                '  $outH = [Math]::Max(1, [int][Math]::Round($bounds.Height * $scale));',
                '  $finalBmp = $srcBmp;',
                '  if ($scale -lt 1.0) {',
                '    $scaled = New-Object System.Drawing.Bitmap $outW, $outH;',
                '    $sg = [System.Drawing.Graphics]::FromImage($scaled);',
                '    $sg.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality;',
                '    $sg.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic;',
                '    $sg.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality;',
                '    $sg.DrawImage($srcBmp, 0, 0, $outW, $outH);',
                '    $sg.Dispose();',
                '    $finalBmp = $scaled;',
                '  }',
                "  $codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' };",
                "  $mime = 'image/jpeg';",
                '  if ($codec) {',
                '    $ep = New-Object System.Drawing.Imaging.EncoderParameters 1;',
                '    $ep.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, 55L);',
                `    $finalBmp.Save('${safePath}', $codec, $ep);`,
                '    $ep.Dispose();',
                '  } else {',
                `    $finalBmp.Save('${safePath}', [System.Drawing.Imaging.ImageFormat]::Png);`,
                "    $mime = 'image/png';",
                '  }',
                '  if ($finalBmp -ne $srcBmp) { $finalBmp.Dispose(); }',
                '  $gfx.Dispose();',
                '  $srcBmp.Dispose();',
                `  try { [System.IO.File]::WriteAllText('${safeMetaPath}', ($outW.ToString() + '|' + $outH.ToString() + '|' + $mime)); } catch {}`,
                "  Write-Output ($outW.ToString() + '|' + $outH.ToString() + '|' + $mime);",
                '} catch {',
                '  try { if ($gfx) { $gfx.Dispose(); } } catch {}',
                '  try { if ($srcBmp) { $srcBmp.Dispose(); } } catch {}',
                '  try { if ($finalBmp -and ($finalBmp -ne $srcBmp)) { $finalBmp.Dispose(); } } catch {}',
                '  try { if ($ep) { $ep.Dispose(); } } catch {}',
                `  try { [System.IO.File]::WriteAllText('${safeErrPath}', ($_.Exception | Out-String)); } catch {}`,
                '  exit 1',
                '  }',
            ].join('\n');
            try {
                fs.writeFileSync(scriptFile, script, 'utf8');
            }
            catch (writeErr) {
                const nowTs = Date.now();
                if ((nowTs - this.lastScreenshotFailureLogAt) > 5 * 60 * 1000) {
                    this.lastScreenshotFailureLogAt = nowTs;
                    logger_1.logger.warn('Screenshot script write failed', { error: String(writeErr?.message || writeErr || '') });
                }
                this.lastScreenshotFailureReason = String(writeErr?.message || 'screenshot_script_write_failed').slice(0, 450);
                resolve(null);
                return;
            }
            (0, child_process_1.execFile)('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-STA', '-ExecutionPolicy', 'Bypass', '-File', scriptFile], { timeout: 15000, maxBuffer: 4 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
                try {
                    if (err || !fs.existsSync(tempFile)) {
                        const nowTs = Date.now();
                        const stdoutMsg = String(stdout || '').trim().slice(0, 400);
                        const stderrMsg = String(stderr || '').trim().slice(0, 400);
                        const errMsg = String(err?.message || '').trim();
                        let scriptErrMsg = '';
                        try {
                            if (fs.existsSync(errorFile)) {
                                scriptErrMsg = String(fs.readFileSync(errorFile, 'utf8') || '').trim().slice(0, 400);
                            }
                        }
                        catch { }
                        const reason = [errMsg, stderrMsg, stdoutMsg, scriptErrMsg].filter(Boolean).join(' | ').slice(0, 450);
                        const likelyScriptBlocked = (/blocked by your antivirus software|malicious content|scriptcont/i.test(reason)
                            || (/command failed:\s*powershell\.exe/i.test(errMsg) && /appstats-shot-[^\\\s]+\.ps1/i.test(errMsg)));
                        if (likelyScriptBlocked) {
                            this.lastScreenshotFailureReason = 'screenshot_script_blocked_by_antivirus';
                            const helperTaskCommand = this.buildWindowsScreenshotHelperCommand(tempFile, metaFile, errorFile);
                            const taskCommand = helperTaskCommand || this.buildWindowsScreenshotInlineCommand(tempFile, metaFile);
                            if (taskCommand) {
                                if ((nowTs - this.lastScreenshotFailureLogAt) > 10 * 60 * 1000) {
                                    this.lastScreenshotFailureLogAt = nowTs;
                                    logger_1.logger.warn('Screenshot PowerShell capture blocked; attempting task fallback', {
                                        reason: reason.slice(0, 300),
                                        mode: helperTaskCommand ? 'compiled-helper' : 'inline-encoded-command',
                                    });
                                }
                                this.captureScreenshotWindowsViaTask(scriptFile, tempFile, errorFile, metaFile, activeWindow, taskCommand)
                                    .then(resolve)
                                    .catch(() => resolve(null));
                                return;
                            }
                            else if ((nowTs - this.lastScreenshotFailureLogAt) > 10 * 60 * 1000) {
                                this.lastScreenshotFailureLogAt = nowTs;
                                logger_1.logger.warn('Screenshot helper fallback unavailable after script block', {
                                    reason: reason.slice(0, 300),
                                });
                            }
                        }
                        if (/copyfromscreen|handle is invalid|service context|session 0/i.test(reason)) {
                            this.lastScreenshotFailureReason = 'service_context_no_desktop_access';
                            if ((nowTs - this.lastScreenshotFailureLogAt) > 10 * 60 * 1000) {
                                this.lastScreenshotFailureLogAt = nowTs;
                                logger_1.logger.warn('Screenshot capture unavailable in service context; attempting scheduled-task fallback', {
                                    reason: reason.slice(0, 300),
                                });
                            }
                            this.captureScreenshotWindowsViaTask(scriptFile, tempFile, errorFile, metaFile, activeWindow)
                                .then(resolve)
                                .catch(() => resolve(null));
                            return;
                        }
                        else {
                            this.lastScreenshotFailureReason = reason;
                            if ((nowTs - this.lastScreenshotFailureLogAt) > 5 * 60 * 1000) {
                                this.lastScreenshotFailureLogAt = nowTs;
                                logger_1.logger.warn('Screenshot capture failed', {
                                    error: errMsg,
                                    stderr: stderrMsg,
                                    stdout: stdoutMsg,
                                    scriptError: scriptErrMsg,
                                });
                            }
                        }
                        try {
                            if (fs.existsSync(tempFile))
                                fs.unlinkSync(tempFile);
                        }
                        catch { }
                        try {
                            if (fs.existsSync(scriptFile))
                                fs.unlinkSync(scriptFile);
                        }
                        catch { }
                        try {
                            if (fs.existsSync(errorFile))
                                fs.unlinkSync(errorFile);
                        }
                        catch { }
                        try {
                            if (fs.existsSync(metaFile))
                                fs.unlinkSync(metaFile);
                        }
                        catch { }
                        resolve(null);
                        return;
                    }
                    const raw = fs.readFileSync(tempFile);
                    try {
                        fs.unlinkSync(tempFile);
                    }
                    catch { }
                    try {
                        if (fs.existsSync(scriptFile))
                            fs.unlinkSync(scriptFile);
                    }
                    catch { }
                    try {
                        if (fs.existsSync(errorFile))
                            fs.unlinkSync(errorFile);
                    }
                    catch { }
                    try {
                        if (fs.existsSync(metaFile))
                            fs.unlinkSync(metaFile);
                    }
                    catch { }
                    if (!raw.length) {
                        resolve(null);
                        return;
                    }
                    const dims = String(stdout || '').trim().split('|');
                    const width = dims[0] ? parseInt(dims[0], 10) : undefined;
                    const height = dims[1] ? parseInt(dims[1], 10) : undefined;
                    const mimeType = String(dims[2] || '').trim().toLowerCase() === 'image/png'
                        ? 'image/png'
                        : 'image/jpeg';
                    const safeWidth = typeof width === 'number' && !Number.isNaN(width) ? width : undefined;
                    const safeHeight = typeof height === 'number' && !Number.isNaN(height) ? height : undefined;
                    resolve({
                        timestamp: new Date().toISOString(),
                        mimeType,
                        imageBase64: raw.toString('base64'),
                        width: safeWidth,
                        height: safeHeight,
                        application: activeWindow?.owner || '',
                        windowTitle: activeWindow?.title || '',
                        isForeground: true,
                    });
                }
                catch {
                    try {
                        if (fs.existsSync(tempFile))
                            fs.unlinkSync(tempFile);
                    }
                    catch { }
                    try {
                        if (fs.existsSync(scriptFile))
                            fs.unlinkSync(scriptFile);
                    }
                    catch { }
                    try {
                        if (fs.existsSync(errorFile))
                            fs.unlinkSync(errorFile);
                    }
                    catch { }
                    try {
                        if (fs.existsSync(metaFile))
                            fs.unlinkSync(metaFile);
                    }
                    catch { }
                    resolve(null);
                }
            });
        });
    }
    captureScreenshotWindowsViaTask(scriptFile, tempFile, errorFile, metaFile, activeWindow, taskCommandOverride) {
        return new Promise((resolve) => {
            const taskName = `AppStatsScreenshot-${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')}`;
            const taskCommand = String(taskCommandOverride || '').trim()
                || this.buildWindowsScreenshotHelperCommand(tempFile, metaFile, errorFile)
                || this.buildWindowsScreenshotInlineCommand(tempFile, metaFile);
            const runTaskCommand = (args, timeoutMs = 20000) => new Promise((done) => {
                (0, child_process_1.execFile)('schtasks.exe', args, { timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
                    done({
                        ok: !err,
                        stdout: String(stdout || '').trim(),
                        stderr: String(stderr || '').trim(),
                    });
                });
            });
            const wait = (ms) => new Promise((done) => setTimeout(done, ms));
            const cleanupFiles = () => {
                try {
                    if (fs.existsSync(tempFile))
                        fs.unlinkSync(tempFile);
                }
                catch { }
                try {
                    if (fs.existsSync(scriptFile))
                        fs.unlinkSync(scriptFile);
                }
                catch { }
                try {
                    if (fs.existsSync(errorFile))
                        fs.unlinkSync(errorFile);
                }
                catch { }
                try {
                    if (fs.existsSync(metaFile))
                        fs.unlinkSync(metaFile);
                }
                catch { }
            };
            const buildScreenshotRecord = () => {
                if (!fs.existsSync(tempFile))
                    return null;
                const raw = fs.readFileSync(tempFile);
                if (!raw.length)
                    return null;
                let width;
                let height;
                let mimeType = String(path.extname(tempFile || '')).toLowerCase() === '.png' ? 'image/png' : 'image/jpeg';
                try {
                    const meta = fs.existsSync(metaFile) ? String(fs.readFileSync(metaFile, 'utf8') || '').trim() : '';
                    const parts = meta.split('|');
                    const parsedWidth = Number(parts[0] || 0);
                    const parsedHeight = Number(parts[1] || 0);
                    const parsedMime = String(parts[2] || '').trim().toLowerCase();
                    if (Number.isFinite(parsedWidth) && parsedWidth > 0)
                        width = Math.floor(parsedWidth);
                    if (Number.isFinite(parsedHeight) && parsedHeight > 0)
                        height = Math.floor(parsedHeight);
                    if (parsedMime === 'image/png')
                        mimeType = 'image/png';
                }
                catch { }
                return {
                    timestamp: new Date().toISOString(),
                    mimeType,
                    imageBase64: raw.toString('base64'),
                    width,
                    height,
                    application: activeWindow?.owner || '',
                    windowTitle: activeWindow?.title || '',
                    isForeground: true,
                };
            };
            void (async () => {
                try {
                    const created = await runTaskCommand([
                        '/Create',
                        '/F',
                        '/TN',
                        taskName,
                        '/TR',
                        taskCommand,
                        '/SC',
                        'ONCE',
                        '/ST',
                        startTime,
                        '/RU',
                        'INTERACTIVE',
                        '/IT',
                        '/RL',
                        'HIGHEST',
                    ], 25000);
                    if (!created.ok) {
                        this.lastScreenshotFailureReason = `scheduled_task_create_failed: ${(created.stderr || created.stdout || 'unknown').slice(0, 200)}`;
                        cleanupFiles();
                        resolve(null);
                        return;
                    }
                    const started = await runTaskCommand(['/Run', '/TN', taskName], 15000);
                    if (!started.ok) {
                        this.lastScreenshotFailureReason = `scheduled_task_run_failed: ${(started.stderr || started.stdout || 'unknown').slice(0, 200)}`;
                        await runTaskCommand(['/Delete', '/F', '/TN', taskName], 10000);
                        cleanupFiles();
                        resolve(null);
                        return;
                    }
                    for (let attempt = 0; attempt < 25; attempt += 1) {
                        if (fs.existsSync(tempFile) || fs.existsSync(errorFile))
                            break;
                        await wait(1000);
                    }
                    if (!fs.existsSync(tempFile) && !fs.existsSync(errorFile)) {
                        await runTaskCommand(['/Run', '/TN', taskName], 10000);
                        for (let attempt = 0; attempt < 10; attempt += 1) {
                            if (fs.existsSync(tempFile) || fs.existsSync(errorFile))
                                break;
                            await wait(1000);
                        }
                    }
                    const screenshot = buildScreenshotRecord();
                    let taskError = '';
                    try {
                        if (fs.existsSync(errorFile)) {
                            taskError = String(fs.readFileSync(errorFile, 'utf8') || '').trim().slice(0, 220);
                        }
                    }
                    catch { }
                    await runTaskCommand(['/Delete', '/F', '/TN', taskName], 10000);
                    cleanupFiles();
                    if (screenshot) {
                        this.lastScreenshotFailureReason = '';
                        resolve(screenshot);
                        return;
                    }
                    this.lastScreenshotFailureReason = taskError
                        ? `scheduled_task_fallback_no_image: ${taskError}`
                        : 'scheduled_task_fallback_no_image';
                    resolve(null);
                }
                catch (error) {
                    this.lastScreenshotFailureReason = `scheduled_task_fallback_error: ${String(error?.message || error || '').slice(0, 220)}`;
                    await runTaskCommand(['/Delete', '/F', '/TN', taskName], 10000);
                    cleanupFiles();
                    resolve(null);
                }
            })();
        });
    }
    /**
     * Get user idle time in milliseconds using OS-level APIs.
     * On Windows, uses query user/quser idle-time output.
     * This detects mouse/keyboard/touch inactivity at the OS level.
     */
    getIdleTime() {
        if (process.platform === 'win32') {
            return this.getIdleTimeWindows();
        }
        else if (process.platform === 'darwin') {
            return this.getIdleTimeMac();
        }
        else {
            return this.getIdleTimeLinux();
        }
    }
    getIdleTimeWindows() {
        return new Promise((resolve) => {
            // Use GetLastInputInfo for second-level idle accuracy instead of minute-level query user
            const psScript = `
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class IdleTime {
  [StructLayout(LayoutKind.Sequential)]
  struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }
  [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
  public static int GetIdleMs() {
    LASTINPUTINFO lii = new LASTINPUTINFO();
    lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
    if (!GetLastInputInfo(ref lii)) return -1;
    return (int)((uint)Environment.TickCount - lii.dwTime);
  }
}
'@
[IdleTime]::GetIdleMs()
`.trim();
            (0, child_process_1.exec)(`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { timeout: 8000, windowsHide: true }, (err, stdout) => {
                if (err) {
                    // Fallback to query user if PowerShell fails
                    this.getIdleTimeWindowsFallback().then(resolve);
                    return;
                }
                const ms = parseInt(String(stdout || '').trim(), 10);
                if (isNaN(ms) || ms < 0) {
                    this.getIdleTimeWindowsFallback().then(resolve);
                    return;
                }
                resolve(ms);
            });
        });
    }
    getIdleTimeWindowsFallback() {
        return new Promise((resolve) => {
            (0, child_process_1.exec)('query user 2>nul || quser 2>nul', { timeout: 5000, windowsHide: true }, (err, stdout) => {
                if (err) {
                    resolve(0);
                    return;
                }
                const lines = String(stdout || '')
                    .split(/\r?\n/)
                    .map((line) => line.trim())
                    .filter(Boolean);
                if (!lines.length) {
                    resolve(0);
                    return;
                }
                const statePattern = /^(active|disc|disconnected|listen|down|conn)$/i;
                const parseIdleTokenMs = (token) => {
                    const raw = String(token || '').trim().toLowerCase();
                    if (!raw || raw === 'none' || raw === '.' || raw === '0' || raw === '0:00' || raw === '00:00')
                        return 0;
                    let match = raw.match(/^(\d+)\+(\d+):(\d+)$/);
                    if (match) {
                        const d = parseInt(match[1], 10) || 0;
                        const h = parseInt(match[2], 10) || 0;
                        const m = parseInt(match[3], 10) || 0;
                        return ((d * 24 + h) * 60 + m) * 60000;
                    }
                    match = raw.match(/^(\d+):(\d+)$/);
                    if (match)
                        return ((parseInt(match[1], 10) || 0) * 60 + (parseInt(match[2], 10) || 0)) * 60000;
                    match = raw.match(/^(\d+)$/);
                    if (match)
                        return (parseInt(match[1], 10) || 0) * 60000;
                    return 0;
                };
                let minActiveIdleMs = null;
                for (const line of lines) {
                    const cleaned = line.replace(/^>\s*/, '').trim();
                    if (!cleaned)
                        continue;
                    const tokens = cleaned.split(/\s+/);
                    const stateIdx = tokens.findIndex((t) => statePattern.test(t));
                    if (stateIdx < 0 || stateIdx + 1 >= tokens.length)
                        continue;
                    if (String(tokens[stateIdx] || '').toLowerCase() !== 'active')
                        continue;
                    const idleMs = parseIdleTokenMs(tokens[stateIdx + 1] || '');
                    if (minActiveIdleMs === null || idleMs < minActiveIdleMs)
                        minActiveIdleMs = idleMs;
                }
                resolve(minActiveIdleMs !== null ? Math.max(0, minActiveIdleMs) : Math.max(this.idleThreshold, 300000));
            });
        });
    }
    getIdleTimeMac() {
        return new Promise((resolve) => {
            (0, child_process_1.exec)('ioreg -c IOHIDSystem | awk \'/HIDIdleTime/ {print $NF/1000000000; exit}\'', { timeout: 3000 }, (err, stdout) => {
                if (err) {
                    resolve(0);
                    return;
                }
                const sec = parseFloat(stdout.trim());
                resolve(isNaN(sec) ? 0 : sec * 1000);
            });
        });
    }
    getIdleTimeLinux() {
        const parseIdleMs = (raw) => {
            const match = String(raw || '').match(/(\d{1,12})/);
            if (!match)
                return null;
            const ms = Number(match[1]);
            return Number.isFinite(ms) && ms >= 0 ? ms : null;
        };
        const commands = [
            'xprintidle 2>/dev/null',
            'gdbus call --session --dest org.gnome.Mutter.IdleMonitor --object-path /org/gnome/Mutter/IdleMonitor/Core --method org.gnome.Mutter.IdleMonitor.GetIdletime 2>/dev/null',
            'gdbus call --session --dest org.freedesktop.ScreenSaver --object-path /org/freedesktop/ScreenSaver --method org.freedesktop.ScreenSaver.GetSessionIdleTime 2>/dev/null',
            'gdbus call --session --dest org.gnome.ScreenSaver --object-path /org/gnome/ScreenSaver --method org.gnome.ScreenSaver.GetSessionIdleTime 2>/dev/null',
            'qdbus org.freedesktop.ScreenSaver /ScreenSaver GetSessionIdleTime 2>/dev/null',
        ];
        return (async () => {
            for (const command of commands) {
                try {
                    const output = await (0, linux_desktop_1.runLinuxDesktopCommand)(command, 3000);
                    const parsed = parseIdleMs(output);
                    if (parsed !== null)
                        return parsed;
                }
                catch { }
            }
            return 0;
        })();
    }
    categorize(appName) {
        const lower = appName.toLowerCase().replace('.exe', '');
        const categories = {
            'development': ['code', 'visual studio', 'terminal', 'powershell', 'cmd', 'git', 'node', 'python', 'docker', 'idea', 'webstorm', 'pycharm', 'sublime', 'notepad++', 'vim'],
            'communication': ['teams', 'slack', 'zoom', 'outlook', 'thunderbird', 'discord', 'skype', 'telegram'],
            'office': ['winword', 'excel', 'powerpnt', 'onenote', 'libreoffice', 'word'],
            'browser': ['chrome', 'firefox', 'edge', 'msedge', 'safari', 'opera', 'brave'],
            'design': ['figma', 'sketch', 'photoshop', 'illustrator', 'gimp', 'inkscape'],
            'media': ['spotify', 'vlc', 'itunes', 'netflix', 'youtube'],
        };
        for (const [cat, apps] of Object.entries(categories)) {
            if (apps.some(a => lower.includes(a)))
                return cat;
        }
        return 'other';
    }
    score(appName, windowTitle) {
        const lower = appName.toLowerCase().replace('.exe', '');
        // First check window title for more specific scoring (browser titles especially)
        if (windowTitle) {
            const titleLower = windowTitle.toLowerCase();
            // Productive title keywords (dev/work tools in browser)
            const productiveTitleKeywords = [
                'github', 'gitlab', 'bitbucket', 'jira', 'confluence', 'asana',
                'trello', 'notion', 'linear', 'azure devops', 'stack overflow',
                'stackoverflow', 'mdn', 'docs.microsoft', 'developer.mozilla',
                'google docs', 'google sheets', 'google slides', 'sharepoint',
                'salesforce', 'hubspot', 'zendesk', 'servicenow', 'grafana',
                'kibana', 'datadog', 'new relic', 'aws console', 'portal.azure',
                'console.cloud.google', 'terraform', 'ansible', 'jenkins',
                'circleci', 'travis', 'vercel', 'netlify', 'heroku',
                'chatgpt', 'copilot', 'claude', 'gemini',
            ];
            if (productiveTitleKeywords.some(k => titleLower.includes(k)))
                return 'productive';
            // Unproductive title keywords
            const unproductiveTitleKeywords = [
                'youtube', 'netflix', 'twitch', 'reddit', 'facebook', 'instagram',
                'tiktok', 'twitter', 'x.com', 'hulu', 'disney+', 'amazon prime',
                'spotify', 'soundcloud', '9gag', 'imgur', 'pinterest',
            ];
            if (unproductiveTitleKeywords.some(k => titleLower.includes(k)))
                return 'unproductive';
        }
        // Fall back to app name matching
        for (const [key, score] of Object.entries(this.productivityRules)) {
            if (lower.includes(key))
                return score;
        }
        return 'neutral';
    }
    async captureScreenshotForRemote(activeWindow = null) {
        if (process.platform === 'win32') {
            return this.captureScreenshotWindows(activeWindow);
        }
        if (process.platform === 'linux') {
            return this.captureScreenshotLinux(activeWindow);
        }
        return null;
    }
    getLastScreenshotFailureReason() {
        return String(this.lastScreenshotFailureReason || '').trim();
    }
    updateRules(rules) {
        this.productivityRules = { ...DEFAULT_PRODUCTIVITY_RULES, ...rules };
        logger_1.logger.info('Productivity rules updated');
    }
    flushRecords() {
        const records = [...this.records];
        this.records = [];
        return records;
    }
    flushEvents() {
        const events = [...this.events];
        this.events = [];
        return events;
    }
    flushScreenshots() {
        const shots = [...this.screenshots];
        this.screenshots = [];
        return shots;
    }
    recordIdleEvent(isIdle) {
        this.events.push({
            timestamp: new Date().toISOString(),
            type: isIdle ? 'idle_start' : 'idle_end',
            details: { duration: isIdle ? 0 : Date.now() - this.lastActivityTime, idleThresholdMs: this.idleThreshold },
        });
    }
    getLastInputState() {
        return {
            lastActivityTime: this.lastActivityTime,
            isIdle: this.lastIdleState,
            idleSinceMs: this.lastIdleState ? (Date.now() - this.lastActivityTime) : 0
        };
    }
    recordURLVisit(url, title, browser) {
        this.events.push({
            timestamp: new Date().toISOString(),
            type: 'url_visit',
            details: { url, title, browser },
        });
    }
}
exports.ProductivityCollector = ProductivityCollector;
//# sourceMappingURL=productivity.js.map