import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { logger } from "../logger.js";
import { getBaseDir, getScreenshotsDir } from "../config/index.js";
import { getPackageRoot } from "../utils/version-check.js";
import { execWithTimeout, withTimeout, QUICK_EXEC_TIMEOUT_MS, HEAVY_EXEC_TIMEOUT_MS, } from "../utils/exec.js";
const WINDOWS_EXEC_OPTIONS = process.platform === "win32" ? { windowsHide: true } : {};
const MAX_SCREENSHOT_SIZE = 1 * 1024 * 1024; // 1MB
/** Overall wall-clock ceiling for `takeScreenshot()`. Must be larger than the
 *  sum of wake + SCK + fallback screencapture to avoid spurious failures. */
const TAKE_SCREENSHOT_OVERALL_TIMEOUT_MS = 30_000;
// ── macOS display wake ──────────────────────────────────────────────────
// `screencapture` and ScreenCaptureKit both return a full-resolution but
// all-black PNG when the display is asleep.  We detect this condition and
// wake the display before capture so the image contains real content.
/** Time to wait after waking the display for it to fully light up. */
const DISPLAY_WAKE_SETTLE_MS = 1500;
/**
 * Check whether the macOS display is currently asleep.
 *
 * Uses `pmset -g assertions` and checks the `PreventUserIdleDisplaySleep`
 * counter combined with the display-sleep flag.  This works on both Intel
 * and Apple Silicon Macs regardless of IOKit class names.
 *
 * Returns `true` if the display appears to be asleep, `false` if awake or
 * if the state cannot be determined (fail-open so we don't add unnecessary
 * latency on detection errors).
 */
async function isDisplayAsleep() {
    try {
        // Primary check: `pmset -g assertions` reports the system-wide
        // assertion summary.  When the display is on, UserIsActive is 1.
        const { stdout } = await execWithTimeout("pmset -g assertions", { timeout: QUICK_EXEC_TIMEOUT_MS });
        const userActiveMatch = /UserIsActive\s+(\d+)/i.exec(stdout);
        if (userActiveMatch && Number(userActiveMatch[1]) === 1)
            return false;
        // Secondary check: `pmset -g systemstate` reports the actual power
        // state.  State 4 = fully awake (CPU + GPU + display).
        // States < 4 indicate some form of sleep / dim.
        try {
            const { stdout: stateOut } = await execWithTimeout("pmset -g systemstate", { timeout: QUICK_EXEC_TIMEOUT_MS });
            const stateMatch = /Current Power State:\s*(\d+)/i.exec(stateOut);
            if (stateMatch && Number(stateMatch[1]) >= 4)
                return false;
        }
        catch {
            // pmset -g systemstate may not be available on all macOS versions
        }
        // If UserIsActive is 0 and power state < 4, the display is likely asleep.
        return true;
    }
    catch {
        // Can't determine — assume awake to avoid unnecessary delay.
        return false;
    }
}
/**
 * Wake the macOS display if it is currently asleep.
 *
 * The wake sequence mirrors what a human would do:
 *   1. `caffeinate -u -t 5` — sends a user-activity assertion to wake the display
 *   2. Move mouse to screen centre — ensures the cursor is visible
 *   3. Click — triggers the lock screen to appear (if locked)
 *   4. Settle delay — gives the panel time to fully light up
 *
 * When the display is already awake the function returns immediately with
 * negligible overhead (~16ms for the `pmset` assertion check).
 *
 * No-op on non-macOS platforms.
 */
async function ensureDisplayAwake() {
    if (process.platform !== "darwin")
        return;
    if (!(await isDisplayAsleep()))
        return;
    try {
        // Step 1: Send user-activity assertion to wake the display hardware.
        // -u = declare user activity (wakes display)
        // -t 5 = hold the assertion for 5 seconds (longer hold is more reliable)
        await execWithTimeout("caffeinate -u -t 5 &", { timeout: QUICK_EXEC_TIMEOUT_MS });
        // Step 2: Give the display a moment to power on before mouse events.
        await new Promise((r) => setTimeout(r, 500));
        // Step 3: Move mouse to screen centre and click to activate lock screen.
        // cliclick may not be available on all setups, so we try but don't fail.
        try {
            await execWithTimeout("cliclick m:960,540 c:960,540", { timeout: QUICK_EXEC_TIMEOUT_MS });
        }
        catch {
            // cliclick not available — caffeinate alone should still wake the display
        }
        // Step 4: Final settle delay for the panel to fully light up.
        await new Promise((r) => setTimeout(r, DISPLAY_WAKE_SETTLE_MS));
        logger.debug("Display woken via caffeinate + click for screenshot capture");
    }
    catch (err) {
        logger.warn(`Failed to wake display: ${err instanceof Error ? err.message : String(err)}`);
    }
}
// ── macOS ScreenCaptureKit helper ───────────────────────────────────────
// `screencapture -C` is unreliable for cursor capture on macOS 14+.
// We compile a small Swift CLI that uses ScreenCaptureKit with
// `showsCursor = true` and cache the binary across runs.
//
// In unbundled mode the .swift source sits next to the compiled .js:
//   dist/tools/helpers/macos-screenshot.swift
// In bundled mode (dist-agent/bundle.cjs) we resolve from the package root:
//   <root>/dist/tools/helpers/macos-screenshot.swift
const SCK_HELPER_SRC = path.join(getPackageRoot(), "dist", "tools", "helpers", "macos-screenshot.swift");
const SCK_HELPER_BIN = path.join(getBaseDir(), "bin", "macos-screenshot");
let _sckBinaryReady;
/**
 * Ensure the ScreenCaptureKit helper binary is compiled and up-to-date.
 * Returns the binary path on success, or null if compilation fails.
 */
async function ensureSckBinary() {
    if (_sckBinaryReady === true)
        return SCK_HELPER_BIN;
    if (_sckBinaryReady === false)
        return null;
    try {
        const [srcStat, binStat] = await Promise.all([
            fsp.stat(SCK_HELPER_SRC).catch(() => null),
            fsp.stat(SCK_HELPER_BIN).catch(() => null),
        ]);
        if (!srcStat) {
            _sckBinaryReady = false;
            return null;
        }
        if (binStat && binStat.mtimeMs >= srcStat.mtimeMs) {
            _sckBinaryReady = true;
            return SCK_HELPER_BIN;
        }
        await fsp.mkdir(path.dirname(SCK_HELPER_BIN), { recursive: true });
        await execWithTimeout(`swiftc -O -framework ScreenCaptureKit -framework CoreGraphics -framework ImageIO -o "${SCK_HELPER_BIN}" "${SCK_HELPER_SRC}"`, { timeout: HEAVY_EXEC_TIMEOUT_MS });
        _sckBinaryReady = true;
        logger.debug(`Compiled ScreenCaptureKit helper → ${SCK_HELPER_BIN}`);
        return SCK_HELPER_BIN;
    }
    catch (e) {
        logger.warn(`Failed to compile ScreenCaptureKit helper: ${e instanceof Error ? e.message : String(e)}`);
        _sckBinaryReady = false;
        return null;
    }
}
// ── Claude vision dimension constraints ────────────────────────────────
// Reference: https://platform.claude.com/docs/en/build-with-claude/vision
// Images larger than these limits are resized server-side, increasing
// time-to-first-token latency without any model performance benefit.
export const CLAUDE_MAX_LONG_EDGE_PX = 1568;
export const CLAUDE_MAX_PIXELS = 1_150_000;
/**
 * Compute how much to scale image dimensions to fit Claude's vision constraints.
 * Returns a factor <= 1.0.
 */
export function getVisionScaleFactor(width, height) {
    const longEdge = Math.max(width, height);
    const totalPixels = width * height;
    const longEdgeScale = CLAUDE_MAX_LONG_EDGE_PX / longEdge;
    const totalPixelsScale = Math.sqrt(CLAUDE_MAX_PIXELS / totalPixels);
    return Math.min(1.0, longEdgeScale, totalPixelsScale);
}
/** Resolve the absolute path to ffmpeg, checking common system locations. */
let _ffmpegPath;
async function findFfmpeg() {
    if (_ffmpegPath !== undefined)
        return _ffmpegPath;
    // Try PATH first
    const whichCmd = process.platform === "win32" ? "where" : "which";
    try {
        const { stdout } = await execWithTimeout(`${whichCmd} ffmpeg`, { ...WINDOWS_EXEC_OPTIONS, timeout: QUICK_EXEC_TIMEOUT_MS });
        const p = stdout.trim().split(/\r?\n/)[0];
        if (p) {
            _ffmpegPath = p;
            return p;
        }
    }
    catch { /* not on PATH */ }
    // Check well-known locations
    const candidates = process.platform === "win32"
        ? [
            "C:\\ProgramData\\chocolatey\\bin\\ffmpeg.exe",
            "C:\\ffmpeg\\bin\\ffmpeg.exe",
        ]
        : [
            "/opt/homebrew/bin/ffmpeg",
            "/usr/local/bin/ffmpeg",
            "/usr/bin/ffmpeg",
        ];
    for (const candidate of candidates) {
        try {
            fs.accessSync(candidate, fs.constants.X_OK);
            _ffmpegPath = candidate;
            return candidate;
        }
        catch { /* next */ }
    }
    _ffmpegPath = null;
    return null;
}
/**
 * Compress a PNG file on disk using ffmpeg two-pass palette quantization.
 * Pass 1: generate an optimal palette with up to `maxColors` colours.
 * Pass 2: re-encode the image using that palette with Bayer dithering.
 *
 * Returns true if the output file was written successfully.
 */
async function compressWithFfmpeg(inputPath, outputPath, maxColors = 128) {
    const ffmpeg = await findFfmpeg();
    if (!ffmpeg) {
        logger.warn("ffmpeg not found on this system");
        return false;
    }
    const palettePath = inputPath.replace(/\.png$/, "-palette.png");
    try {
        // Pass 1: palette generation
        await execWithTimeout(`"${ffmpeg}" -y -i "${inputPath}" -vf "palettegen=max_colors=${maxColors}" "${palettePath}"`, { ...WINDOWS_EXEC_OPTIONS, timeout: HEAVY_EXEC_TIMEOUT_MS });
        // Pass 2: apply palette
        await execWithTimeout(`"${ffmpeg}" -y -i "${inputPath}" -i "${palettePath}" -lavfi "paletteuse=dither=bayer" -compression_level 100 "${outputPath}"`, { ...WINDOWS_EXEC_OPTIONS, timeout: HEAVY_EXEC_TIMEOUT_MS });
        try {
            await fsp.access(outputPath);
            return true;
        }
        catch {
            return false;
        }
    }
    catch (e) {
        logger.warn(`ffmpeg compression failed: ${e instanceof Error ? e.message : String(e)}`);
        return false;
    }
    finally {
        try {
            await fsp.unlink(palettePath);
        }
        catch { /* noop */ }
    }
}
/**
 * Resize a buffer using system tools (sips on macOS, ImageMagick on Linux/Windows,
 * PowerShell GDI+ on Windows as fallback).
 * Returns the resized buffer, or null on failure.
 */
async function resizeWithSystemTools(buffer, targetW, targetH) {
    const tmpIn = path.join(os.tmpdir(), `visionclaw-resize-in-${Date.now()}.png`);
    const tmpOut = path.join(os.tmpdir(), `visionclaw-resize-out-${Date.now()}.png`);
    try {
        await fsp.writeFile(tmpIn, buffer);
        if (process.platform === "darwin") {
            await execWithTimeout(`sips -z ${targetH} ${targetW} "${tmpIn}" --out "${tmpOut}"`);
        }
        else if (process.platform === "win32") {
            // Try ImageMagick first (uses 'magick' prefix on Windows)
            try {
                await execWithTimeout(`magick convert "${tmpIn}" -resize ${targetW}x${targetH}! "${tmpOut}"`, WINDOWS_EXEC_OPTIONS);
            }
            catch {
                // Fallback to PowerShell System.Drawing GDI+
                await execWithTimeout(`powershell -NoProfile -Command "` +
                    `Add-Type -AssemblyName System.Drawing; ` +
                    `$img = [System.Drawing.Image]::FromFile('${tmpIn}'); ` +
                    `$bmp = New-Object System.Drawing.Bitmap(${targetW}, ${targetH}); ` +
                    `$g = [System.Drawing.Graphics]::FromImage($bmp); ` +
                    `$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic; ` +
                    `$g.DrawImage($img, 0, 0, ${targetW}, ${targetH}); ` +
                    `$bmp.Save('${tmpOut}', [System.Drawing.Imaging.ImageFormat]::Png); ` +
                    `$g.Dispose(); $bmp.Dispose(); $img.Dispose()"`, WINDOWS_EXEC_OPTIONS);
            }
        }
        else {
            await execWithTimeout(`convert "${tmpIn}" -resize ${targetW}x${targetH}! "${tmpOut}"`);
        }
        try {
            return await fsp.readFile(tmpOut);
        }
        catch {
            return null;
        }
    }
    catch (e) {
        logger.warn(`System tool resize failed: ${e instanceof Error ? e.message : String(e)}`);
        return null;
    }
    finally {
        try {
            await fsp.unlink(tmpIn);
        }
        catch { /* noop */ }
        try {
            await fsp.unlink(tmpOut);
        }
        catch { /* noop */ }
    }
}
/**
 * Get image dimensions using system tools (sips on macOS, identify on Linux).
 * Returns { width, height } or null on failure.
 */
export async function getImageDimensions(buffer) {
    const tmpFile = path.join(os.tmpdir(), `visionclaw-dims-${Date.now()}.png`);
    try {
        await fsp.writeFile(tmpFile, buffer);
        return await getImageDimensionsFromFile(tmpFile);
    }
    catch {
        return null;
    }
    finally {
        try {
            await fsp.unlink(tmpFile);
        }
        catch { /* noop */ }
    }
}
/**
 * Get image dimensions from a file path directly (avoids base64 decode + temp file).
 */
export async function getImageDimensionsFromFile(filePath) {
    try {
        if (process.platform === "darwin") {
            const { stdout } = await execWithTimeout(`sips -g pixelWidth -g pixelHeight "${filePath}"`);
            const wMatch = /pixelWidth:\s*(\d+)/.exec(stdout);
            const hMatch = /pixelHeight:\s*(\d+)/.exec(stdout);
            if (wMatch && hMatch) {
                return { width: parseInt(wMatch[1], 10), height: parseInt(hMatch[1], 10) };
            }
        }
        else if (process.platform === "win32") {
            // Try ImageMagick first
            try {
                const { stdout } = await execWithTimeout(`magick identify -format "%w %h" "${filePath}"`, WINDOWS_EXEC_OPTIONS);
                const parts = stdout.trim().split(/\s+/);
                if (parts.length >= 2) {
                    return { width: parseInt(parts[0], 10), height: parseInt(parts[1], 10) };
                }
            }
            catch {
                // Fallback to PowerShell System.Drawing
                const { stdout } = await execWithTimeout(`powershell -NoProfile -Command "` +
                    `Add-Type -AssemblyName System.Drawing; ` +
                    `$img = [System.Drawing.Image]::FromFile('${filePath}'); ` +
                    `Write-Output \\"$($img.Width) $($img.Height)\\"; ` +
                    `$img.Dispose()"`, WINDOWS_EXEC_OPTIONS);
                const parts = stdout.trim().split(/\s+/);
                if (parts.length >= 2) {
                    return { width: parseInt(parts[0], 10), height: parseInt(parts[1], 10) };
                }
            }
        }
        else {
            const { stdout } = await execWithTimeout(`identify -format "%w %h" "${filePath}"`);
            const parts = stdout.trim().split(/\s+/);
            if (parts.length >= 2) {
                return { width: parseInt(parts[0], 10), height: parseInt(parts[1], 10) };
            }
        }
        return null;
    }
    catch {
        return null;
    }
}
/**
 * Resize a PNG buffer to fit within Claude's vision constraints.
 * Uses system tools (sips on macOS, ImageMagick on Linux).
 * Returns the (possibly resized) buffer.
 */
export async function resizeForVision(buffer) {
    const dims = await getImageDimensions(buffer);
    if (!dims) {
        logger.warn("Could not read image dimensions, keeping original");
        return buffer;
    }
    const scale = getVisionScaleFactor(dims.width, dims.height);
    if (scale >= 1.0)
        return buffer; // already within limits
    const targetW = Math.max(1, Math.round(dims.width * scale));
    const targetH = Math.max(1, Math.round(dims.height * scale));
    const resized = await resizeWithSystemTools(buffer, targetW, targetH);
    if (resized) {
        logger.debug(`Image resized for vision: ${dims.width}x${dims.height} → ${targetW}x${targetH} (scale=${scale.toFixed(3)}, ${buffer.length} → ${resized.length} bytes)`);
        return resized;
    }
    logger.warn("Image vision resize failed, keeping original");
    return buffer;
}
/**
 * Resize a base64-encoded image to fit within Claude's vision constraints.
 * Returns the (possibly resized) image as base64.
 */
export async function resizeBase64ForVision(base64) {
    const buffer = Buffer.from(base64, "base64");
    const resized = await resizeForVision(buffer);
    return resized === buffer ? base64 : resized.toString("base64");
}
/**
 * Detect MIME type from buffer magic bytes.
 * Returns a MIME type string, defaulting to "image/png" if unknown.
 */
function detectMimeType(buffer) {
    if (buffer[0] === 0xFF && buffer[1] === 0xD8)
        return "image/jpeg";
    if (buffer[0] === 0x89 && buffer[1] === 0x50)
        return "image/png";
    if (buffer[0] === 0x47 && buffer[1] === 0x49)
        return "image/gif";
    if (buffer[0] === 0x52 && buffer[1] === 0x49)
        return "image/webp";
    return "image/png";
}
/**
 * Download an image from a URL and resize it for Claude's vision constraints.
 * Returns `{ base64, mimeType }` if successful, or `null` on failure.
 * Used to replace URL-referenced images with properly-sized base64 data.
 */
export async function downloadAndResizeForVision(url, retries = 2, delayMs = 1000) {
    for (let attempt = 0; attempt <= retries; attempt++) {
        try {
            const response = await fetch(url);
            if (!response.ok) {
                logger.warn(`Failed to download image from ${url}: ${response.status} (attempt ${attempt + 1}/${retries + 1})`);
                if (attempt < retries) {
                    await new Promise((r) => setTimeout(r, delayMs));
                    continue;
                }
                return null;
            }
            const buffer = Buffer.from(await response.arrayBuffer());
            const mimeType = detectMimeType(buffer);
            const resized = await resizeForVision(buffer);
            return { base64: resized.toString("base64"), mimeType };
        }
        catch (e) {
            logger.warn(`Failed to download/resize image from ${url}: ${e instanceof Error ? e.message : String(e)} (attempt ${attempt + 1}/${retries + 1})`);
            if (attempt < retries) {
                await new Promise((r) => setTimeout(r, delayMs));
                continue;
            }
            return null;
        }
    }
    return null;
}
/**
 * Compress a PNG buffer if it exceeds the size threshold.
 * Uses ffmpeg palette quantization.
 */
async function compressIfNeeded(buffer, maxSize = MAX_SCREENSHOT_SIZE) {
    if (buffer.length <= maxSize)
        return buffer;
    const tmpIn = path.join(os.tmpdir(), `visionclaw-compress-in-${Date.now()}.png`);
    const tmpOut = path.join(os.tmpdir(), `visionclaw-compress-out-${Date.now()}.png`);
    try {
        await fsp.writeFile(tmpIn, buffer);
        for (const maxColors of [128, 64]) {
            const ok = await compressWithFfmpeg(tmpIn, tmpOut, maxColors);
            if (ok) {
                const compressedBuffer = await fsp.readFile(tmpOut);
                logger.debug(`Screenshot ffmpeg compress (${maxColors} colours): ${buffer.length} → ${compressedBuffer.length} bytes`);
                if (compressedBuffer.length <= maxSize)
                    return compressedBuffer;
                return compressedBuffer;
            }
        }
    }
    finally {
        try {
            await fsp.unlink(tmpIn);
        }
        catch { /* noop */ }
        try {
            await fsp.unlink(tmpOut);
        }
        catch { /* noop */ }
    }
    return buffer;
}
const SCREENSHOT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 60 minutes
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg"]);
let lastCleanupMs = 0;
/**
 * Remove image files older than 24 hours from the screenshots directory.
 * Cleans up both our own screenshots and Playwright output.
 * Throttled to run at most once every 10 minutes.
 */
export async function cleanOldScreenshots() {
    const now = Date.now();
    if (now - lastCleanupMs < CLEANUP_INTERVAL_MS)
        return;
    lastCleanupMs = now;
    try {
        const dir = getScreenshotsDir();
        try {
            await fsp.access(dir);
        }
        catch {
            return;
        }
        const cutoff = now - SCREENSHOT_MAX_AGE_MS;
        let deleted = 0;
        const entries = await fsp.readdir(dir);
        for (const entry of entries) {
            if (!IMAGE_EXTENSIONS.has(path.extname(entry).toLowerCase()))
                continue;
            const filePath = path.join(dir, entry);
            try {
                const stat = await fsp.stat(filePath);
                if (stat.isFile() && stat.mtimeMs < cutoff) {
                    await fsp.unlink(filePath);
                    deleted++;
                }
            }
            catch { /* ignore per-file errors */ }
        }
        if (deleted > 0) {
            logger.debug(`[screenshots] cleaned ${deleted} file(s) older than 24h from ${dir}`);
        }
    }
    catch (err) {
        logger.warn(`[screenshots] cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
    }
}
/**
 * Kill any stale `macos-screenshot` helper processes that are not children
 * of the current agent. When the ScreenCaptureKit helper hangs (see the
 * Swift source for context) and the agent is then restarted, macOS
 * reparents the stuck helper to launchd (PPID=1) and it continues to
 * spin at 100% CPU until someone kills it. We've observed instances
 * running for hours accumulating >4h of CPU time.
 *
 * Runs once at agent startup, best-effort. On a healthy machine this
 * finds nothing and returns in ~10ms.
 *
 * Notes on safety:
 *   - We only match our own helper binary path under
 *     `<visionclaw-base-dir>/bin/macos-screenshot`, so other tools are
 *     untouched.
 *   - We skip processes whose PPID is the current agent PID, in case an
 *     in-flight capture is still running (only relevant for re-entry
 *     edge cases; the single-instance PID lock usually prevents this).
 *   - Errors are logged at debug level and never propagated.
 */
export async function killStaleScreenshotHelpers() {
    if (process.platform !== "darwin")
        return;
    try {
        const { stdout } = await execWithTimeout(
        // pgrep -l prints "pid command", `-f` matches against full command
        // line, `-U $(id -u)` scopes to current user.
        `pgrep -lf -U $(id -u) "${SCK_HELPER_BIN}"`, { timeout: QUICK_EXEC_TIMEOUT_MS }).catch(() => ({ stdout: "", stderr: "" }));
        const pids = [];
        for (const line of stdout.split("\n")) {
            const m = /^\s*(\d+)\b/.exec(line);
            if (!m)
                continue;
            const pid = Number(m[1]);
            if (!Number.isFinite(pid) || pid <= 1)
                continue;
            if (pid === process.pid)
                continue;
            pids.push(pid);
        }
        if (pids.length === 0)
            return;
        // Only kill PPID=1 orphans; leave alive any helper whose parent is still
        // a running VisionClaw (could be a sibling profile).
        const kept = [];
        const killed = [];
        for (const pid of pids) {
            let ppid = 0;
            try {
                const { stdout: ppidOut } = await execWithTimeout(`ps -o ppid= -p ${pid}`, { timeout: QUICK_EXEC_TIMEOUT_MS });
                ppid = Number(ppidOut.trim());
            }
            catch {
                // ps failed — process may have already exited; skip.
                continue;
            }
            if (ppid !== 1) {
                kept.push(pid);
                continue;
            }
            try {
                process.kill(pid, "SIGKILL");
                killed.push(pid);
            }
            catch {
                // Already gone or permission denied; ignore.
            }
        }
        if (killed.length > 0) {
            logger.system(`[screenshot] Killed ${killed.length} stale macos-screenshot orphan(s): ${killed.join(", ")}`);
        }
        if (kept.length > 0) {
            logger.debug(`[screenshot] Left ${kept.length} macos-screenshot helper(s) alive (non-orphan): ${kept.join(", ")}`);
        }
    }
    catch (err) {
        logger.debug(`[screenshot] killStaleScreenshotHelpers failed: ${err instanceof Error ? err.message : String(err)}`);
    }
}
/**
 * Take a screenshot of the current desktop.
 * Returns the base64-encoded PNG image and the path to the persisted file.
 * The file is saved to ~/screenshots/ for later use (e.g. sending to user).
 *
 * Wrapped in an overall wall-clock timeout as a last-resort guard — the
 * per-subprocess `execWithTimeout` calls are the primary defence, but a
 * rogue sequence (e.g. display-wake → stuck cliclick → stuck SCK helper)
 * could in theory still eat the full sum of those budgets. The overall
 * ceiling guarantees the agent's wake cycle never deadlocks on screenshot.
 */
export async function takeScreenshot() {
    return withTimeout(takeScreenshotInner(), TAKE_SCREENSHOT_OVERALL_TIMEOUT_MS, "takeScreenshot");
}
async function takeScreenshotInner() {
    const screenshotsDir = getScreenshotsDir();
    await fsp.mkdir(screenshotsDir, { recursive: true });
    const tmpFile = path.join(os.tmpdir(), `visionclaw-screenshot-${Date.now()}.png`);
    try {
        if (process.platform === "darwin") {
            await ensureDisplayAwake();
            const sckBin = await ensureSckBinary();
            if (sckBin) {
                try {
                    await execWithTimeout(`"${sckBin}" "${tmpFile}"`);
                }
                catch (e) {
                    logger.warn(`ScreenCaptureKit capture failed, falling back to screencapture: ${e instanceof Error ? e.message : String(e)}`);
                    await execWithTimeout(`screencapture -x -C "${tmpFile}"`);
                }
            }
            else {
                await execWithTimeout(`screencapture -x -C "${tmpFile}"`);
            }
        }
        else if (process.platform === "win32") {
            // Use DPI-aware capture + cursor overlay for complete screenshots on scaled displays
            const ps1 = [
                `Add-Type -AssemblyName System.Windows.Forms`,
                `Add-Type -AssemblyName System.Drawing`,
                `$pinvoke = @'`,
                `using System;`,
                `using System.Runtime.InteropServices;`,
                `public class ScreenCapHelper {`,
                `  [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();`,
                `  [DllImport("user32.dll")] public static extern int GetSystemMetrics(int nIndex);`,
                `  [DllImport("user32.dll")] public static extern bool GetCursorInfo(ref CURSORINFO pci);`,
                `  [DllImport("user32.dll")] public static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyWidth, uint istepIfAniCur, IntPtr hbrFlickerFreeDraw, uint diFlags);`,
                `  [StructLayout(LayoutKind.Sequential)] public struct POINT { public int x; public int y; }`,
                `  [StructLayout(LayoutKind.Sequential)] public struct CURSORINFO { public int cbSize; public int flags; public IntPtr hCursor; public POINT ptScreenPos; }`,
                `}`,
                `'@`,
                `Add-Type -TypeDefinition $pinvoke -Language CSharp`,
                `[ScreenCapHelper]::SetProcessDPIAware() | Out-Null`,
                `$w = [ScreenCapHelper]::GetSystemMetrics(0)`,
                `$h = [ScreenCapHelper]::GetSystemMetrics(1)`,
                `$bmp = New-Object System.Drawing.Bitmap($w, $h)`,
                `$g = [System.Drawing.Graphics]::FromImage($bmp)`,
                `$g.CopyFromScreen(0, 0, 0, 0, (New-Object System.Drawing.Size($w, $h)))`,
                `$ci = New-Object ScreenCapHelper+CURSORINFO`,
                `$ci.cbSize = [System.Runtime.InteropServices.Marshal]::SizeOf($ci)`,
                `if ([ScreenCapHelper]::GetCursorInfo([ref]$ci) -and ($ci.flags -eq 1)) {`,
                `  $hdc = $g.GetHdc()`,
                `  [ScreenCapHelper]::DrawIconEx($hdc, $ci.ptScreenPos.x, $ci.ptScreenPos.y, $ci.hCursor, 0, 0, 0, [IntPtr]::Zero, 3) | Out-Null`,
                `  $g.ReleaseHdc($hdc)`,
                `}`,
                `$bmp.Save('${tmpFile}', [System.Drawing.Imaging.ImageFormat]::Png)`,
                `$g.Dispose(); $bmp.Dispose()`,
            ].join("\n");
            const ps1File = path.join(os.tmpdir(), `visionclaw-screenshot-${Date.now()}.ps1`);
            await fsp.writeFile(ps1File, ps1, "utf-8");
            try {
                await execWithTimeout(`powershell -NoProfile -ExecutionPolicy Bypass -File "${ps1File}"`, WINDOWS_EXEC_OPTIONS);
            }
            finally {
                try {
                    await fsp.unlink(ps1File);
                }
                catch { /* noop */ }
            }
        }
        else {
            try {
                await execWithTimeout(`scrot "${tmpFile}"`);
            }
            catch {
                await execWithTimeout(`gnome-screenshot -f "${tmpFile}"`);
            }
        }
        const rawBuffer = await fsp.readFile(tmpFile);
        // 1. Resize to fit Claude's vision constraints (reduces TTFT)
        const resized = await resizeForVision(rawBuffer);
        // 2. Compress if over file size threshold (ffmpeg)
        const compressed = await compressIfNeeded(resized);
        // Persist to ~/screenshots/ so the agent can reference the file later
        const filePath = path.join(screenshotsDir, `screenshot-${Date.now()}.png`);
        await fsp.writeFile(filePath, compressed);
        return { base64: compressed.toString("base64"), filePath };
    }
    finally {
        try {
            await fsp.unlink(tmpFile);
        }
        catch {
            // Ignore cleanup errors
        }
    }
}
//# sourceMappingURL=screenshot.js.map