/**
 * Low-level macOS desktop executor using cliclick, osascript, and screencapture.
 * Used by computer_use MCP tools to perform mouse, keyboard, and screenshot actions.
 */
import { exec } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { takeScreenshot } from "./screenshot.js";
import { logger } from "../logger.js";
import { execWithTimeout, HEAVY_EXEC_TIMEOUT_MS, QUICK_EXEC_TIMEOUT_MS } from "../utils/exec.js";
/** Capture the screen. Returns base64 PNG and the saved file path. */
export async function captureScreen() {
    return takeScreenshot();
}
/** Cached display size, set once at agent startup. */
let cachedDisplaySize = null;
/**
 * Initialize display size cache. Call once at agent startup.
 *
 * Throws if the system display size cannot be determined. We intentionally
 * don't silently fall back to a guessed resolution: every coordinate the
 * computer_use tools compute (click targets, drag endpoints, vision-
 * resolver scaling) depends on the real display size, and a wrong value
 * makes the agent click the wrong pixels silently. Callers at startup
 * should catch this, disable computer_use, and notify the owner.
 */
export async function initDisplaySize() {
    cachedDisplaySize = await fetchDisplaySize();
}
/** Whether `initDisplaySize()` has successfully populated the cache. */
export function isDisplaySizeInitialized() {
    return cachedDisplaySize !== null;
}
/**
 * Return cached display size. Throws if `initDisplaySize()` has not run
 * successfully — callers must gate computer_use entry points on
 * `isDisplaySizeInitialized()` first.
 */
export function getDisplaySize() {
    if (!cachedDisplaySize) {
        throw new Error("Display size not initialized — call initDisplaySize() first, or gate computer_use on isDisplaySizeInitialized()");
    }
    return cachedDisplaySize;
}
/**
 * Fetch display dimensions via system_profiler SPDisplaysDataType (JSON).
 * Structure: SPDisplaysDataType[] -> spdisplays_ndrvs[] -> _spdisplays_resolution or _spdisplays_pixels
 *
 * Uses the heavy timeout because `system_profiler` can take 10–30s on
 * macOS — it does a full display enumeration including EDID reads and
 * talks to WindowServer, and can be particularly slow right after boot
 * or resume-from-sleep. The previous 15s default was causing spurious
 * failures on otherwise-healthy machines.
 *
 * Throws on any failure; we deliberately do NOT return a fallback size
 * because wrong coordinates are worse than no computer_use at all.
 */
async function fetchDisplaySizeViaScreencapture() {
    const tmpFile = path.join(os.tmpdir(), `display-size-probe-${Date.now()}.png`);
    try {
        await execWithTimeout(`screencapture -x ${tmpFile}`, {
            timeout: HEAVY_EXEC_TIMEOUT_MS,
        });
        const { stdout } = await execWithTimeout(`sips -g pixelWidth -g pixelHeight ${tmpFile}`, { timeout: QUICK_EXEC_TIMEOUT_MS });
        const wMatch = /pixelWidth:\s*(\d+)/.exec(stdout);
        const hMatch = /pixelHeight:\s*(\d+)/.exec(stdout);
        if (!wMatch?.[1] || !hMatch?.[1]) {
            throw new Error(`sips returned unparseable output: ${stdout}`);
        }
        const width = parseInt(wMatch[1], 10);
        const height = parseInt(hMatch[1], 10);
        logger.debug(`getDisplaySize (screencapture fallback): ${width}x${height}`, { width, height });
        return { width, height };
    }
    finally {
        try {
            fs.unlinkSync(tmpFile);
        }
        catch { /* noop */ }
    }
}
async function fetchDisplaySize() {
    const { stdout } = await execWithTimeout("system_profiler SPDisplaysDataType -json 2>/dev/null", { timeout: HEAVY_EXEC_TIMEOUT_MS });
    const json = JSON.parse(stdout);
    const gpus = json.SPDisplaysDataType;
    if (!Array.isArray(gpus)) {
        logger.warn(`system_profiler returned no SPDisplaysDataType (keys: ${Object.keys(json).join(", ")}), trying screencapture fallback`);
        return fetchDisplaySizeViaScreencapture();
    }
    const candidates = [];
    for (const gpu of gpus) {
        const ndrvs = gpu.spdisplays_ndrvs;
        if (!Array.isArray(ndrvs))
            continue;
        for (const ndrv of ndrvs) {
            const res = ndrv._spdisplays_resolution ?? ndrv._spdisplays_pixels;
            if (!res)
                continue;
            const re = /(\d+)\s*x\s*(\d+)/i;
            const m = re.exec(res);
            if (m?.[1] && m[2]) {
                candidates.push({
                    width: parseInt(m[1], 10),
                    height: parseInt(m[2], 10),
                    isMain: ndrv.spdisplays_main === "spdisplays_yes",
                });
            }
        }
    }
    if (candidates.length === 0) {
        logger.warn(`system_profiler returned no parseable resolution (${gpus.length} GPU(s)), trying screencapture fallback`);
        return fetchDisplaySizeViaScreencapture();
    }
    const chosen = candidates.find((c) => c.isMain) ?? candidates[0];
    logger.debug(`getDisplaySize: ${chosen.width}x${chosen.height} (main=${String(chosen.isMain)}, ${candidates.length} displays)`, {
        rawCount: candidates.length,
        mainCount: candidates.filter((c) => c.isMain).length,
        width: chosen.width,
        height: chosen.height,
        isMain: chosen.isMain,
    });
    return { width: chosen.width, height: chosen.height };
}
/**
 * Run cliclick. Throws if cliclick is not installed.
 *
 * Each space-separated cliclick command (e.g. "kd:cmd t:v ku:cmd") is
 * individually shell-quoted so the shell doesn't interpret special
 * characters inside the argument values while cliclick still sees
 * them as separate commands.
 */
async function cliclick(args) {
    const quoted = args.split(" ").map(shellQuote).join(" ");
    // cliclick is a near-instant CLI; a hung call is a stuck TCC prompt or
    // the Accessibility server wedged. Keep a tight budget so we fail fast
    // rather than accumulating orphan cliclick processes.
    await execWithTimeout(`cliclick ${quoted}`, { timeout: QUICK_EXEC_TIMEOUT_MS });
}
function shellQuote(value) {
    // POSIX-safe single-quote escaping: ' -> '\''
    return `'${value.replace(/'/g, `'\\''`)}'`;
}
/**
 * Left click at (x, y).
 */
export async function click(x, y) {
    await cliclick(`c:${Math.round(x)},${Math.round(y)}`);
}
/**
 * Double click at (x, y).
 */
export async function doubleClick(x, y) {
    await cliclick(`dc:${Math.round(x)},${Math.round(y)}`);
}
/**
 * Right click at (x, y).
 */
export async function rightClick(x, y) {
    await cliclick(`rc:${Math.round(x)},${Math.round(y)}`);
}
/**
 * Move cursor to (x, y).
 */
export async function moveTo(x, y) {
    await cliclick(`m:${Math.round(x)},${Math.round(y)}`);
}
/**
 * Drag from (x1, y1) to (x2, y2).
 *
 * Uses cliclick dd: (drag down) + dm: (drag move) + du: (drag up) with a
 * small wait (-w) between commands so macOS registers the press-hold-move
 * gesture rather than treating it as a quick click.
 */
export async function drag(x1, y1, x2, y2) {
    const rx1 = Math.round(x1);
    const ry1 = Math.round(y1);
    const rx2 = Math.round(x2);
    const ry2 = Math.round(y2);
    // -w 150  → wait 150ms between each cliclick command
    // dd → press at start, dm → move (keeps button held), du → release at end
    // 3 cliclick sub-commands with -w 1000 → up to ~3s of built-in waits plus
    // motion, so use the default timeout budget rather than the quick one.
    await execWithTimeout(`cliclick -w 1000 'dd:${rx1},${ry1}' 'dm:${rx2},${ry2}' 'du:${rx2},${ry2}'`);
}
/**
 * Type text into the currently focused element.
 *
 * Uses the clipboard (pbcopy + Cmd-V) for speed and full Unicode support
 * (including emoji). The previous clipboard content is saved and restored.
 */
export async function typeText(text) {
    // Fast path: single ASCII character — cliclick is fine.
    if (text.length === 1 && text.charCodeAt(0) < 128) {
        const escaped = text.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
        await cliclick(`t:${escaped}`);
        return;
    }
    // Save current clipboard, set our text, paste, then restore.
    const { stdout: prevClip } = await execWithTimeout("pbpaste", { timeout: QUICK_EXEC_TIMEOUT_MS }).catch(() => ({
        stdout: "",
    }));
    // pbcopy reads stdin — pipe via the child's stdin; exec's `timeout` +
    // SIGKILL guarantees we don't deadlock if pbcopy hangs (rare, but the
    // pasteboard server can wedge during user sessions).
    await new Promise((resolve, reject) => {
        const proc = exec("pbcopy", { timeout: QUICK_EXEC_TIMEOUT_MS, killSignal: "SIGKILL" }, (err) => (err ? reject(err) : resolve()));
        proc.stdin?.end(text);
    });
    // Cmd-V to paste
    await cliclick("kd:cmd t:v ku:cmd");
    // Small delay to let the paste complete before restoring clipboard.
    await new Promise((r) => setTimeout(r, 50));
    // Restore previous clipboard content.
    await new Promise((resolve, reject) => {
        const proc = exec("pbcopy", { timeout: QUICK_EXEC_TIMEOUT_MS, killSignal: "SIGKILL" }, (err) => (err ? reject(err) : resolve()));
        proc.stdin?.end(prevClip);
    });
}
/**
 * macOS key codes for System Events `key code` command.
 * More reliable than cliclick kp: for special keys in many apps.
 */
const MAC_KEY_CODES = {
    enter: 36,
    return: 36,
    tab: 48,
    escape: 53,
    esc: 53,
    space: 49,
    backspace: 51,
    delete: 117,
    up: 126,
    down: 125,
    left: 123,
    right: 124,
    home: 115,
    end: 119,
    pageup: 116,
    pagedown: 121,
    f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97,
    f7: 98, f8: 100, f9: 101, f10: 109, f11: 103, f12: 111,
};
/** osascript modifier keyword for System Events `using` clause */
const OSA_MODS = {
    cmd: "command down",
    meta: "command down",
    ctrl: "control down",
    alt: "option down",
    option: "option down",
    shift: "shift down",
};
/**
 * Press a key or key combination (e.g. "enter", "escape", "cmd+s", "ctrl+shift+t").
 *
 * Uses osascript System Events `key code` for special keys (more reliable across
 * apps than cliclick kp:), and cliclick `t:` for single character keys.
 * Modifiers are handled via System Events `using` clause or cliclick `kd:/ku:`.
 */
export async function pressKey(key) {
    const parts = key.toLowerCase().split("+");
    const keyName = parts[parts.length - 1] ?? key;
    const modParts = parts.slice(0, -1);
    // Small delay so the frontmost app has time to accept focus before the key event.
    await new Promise((r) => setTimeout(r, 100));
    const keyCode = MAC_KEY_CODES[keyName];
    if (keyCode !== undefined) {
        // Use osascript System Events for special keys — more reliable than cliclick kp:
        const using = modParts.length > 0
            ? ` using {${modParts.map((m) => OSA_MODS[m] ?? `${m} down`).join(", ")}}`
            : "";
        await execWithTimeout(`osascript -e 'tell application "System Events" to key code ${keyCode}${using}'`);
    }
    else if (keyName.length === 1) {
        // Single character key — use cliclick t: (with modifier support via kd:/ku:)
        const cliclickMods = modParts
            .map((m) => CLICLICK_MODS[m])
            .filter(Boolean);
        if (cliclickMods.length > 0) {
            const mod = cliclickMods.join(",");
            await cliclick(`kd:${mod} t:${keyName} ku:${mod}`);
        }
        else {
            await cliclick(`t:${keyName}`);
        }
    }
    else {
        // Unknown key name — try osascript keystroke as a last resort
        const using = modParts.length > 0
            ? ` using {${modParts.map((m) => OSA_MODS[m] ?? `${m} down`).join(", ")}}`
            : "";
        await execWithTimeout(`osascript -e 'tell application "System Events" to keystroke "${keyName}"${using}'`);
    }
}
/** cliclick modifier names for kd:/ku: */
const CLICLICK_MODS = {
    cmd: "cmd",
    meta: "cmd",
    ctrl: "ctrl",
    alt: "alt",
    option: "alt",
    shift: "shift",
};
let _scrollHelperPath = null;
async function getScrollHelper() {
    if (_scrollHelperPath && fs.existsSync(_scrollHelperPath))
        return _scrollHelperPath;
    const binPath = path.join(os.tmpdir(), "visionclaw-scroll-helper");
    if (fs.existsSync(binPath)) {
        _scrollHelperPath = binPath;
        return binPath;
    }
    const src = path.join(os.tmpdir(), "visionclaw-scroll-helper.swift");
    const code = [
        "import CoreGraphics",
        "import Foundation",
        "let v = Int32(CommandLine.arguments[1]) ?? 0",
        "let h = Int32(CommandLine.arguments[2]) ?? 0",
        "let e = CGEvent(scrollWheelEvent2Source: nil, units: .line, wheelCount: 2, wheel1: v, wheel2: h, wheel3: 0)!",
        "e.post(tap: .cghidEventTap)",
    ].join("\n");
    fs.writeFileSync(src, code);
    await execWithTimeout(`swiftc -O -o "${binPath}" "${src}"`, { timeout: HEAVY_EXEC_TIMEOUT_MS });
    try {
        fs.unlinkSync(src);
    }
    catch { /* noop */ }
    _scrollHelperPath = binPath;
    return binPath;
}
/**
 * Scroll at (x, y) in direction. Amount is number of scroll line-steps (default 3).
 *
 * Uses CGEvent scroll wheel via a compiled Swift helper for all directions.
 * The helper is compiled once on first use, then cached for subsequent calls.
 */
export async function scroll(x, y, direction, amount = 3) {
    const vertical = direction === "up" ? amount : direction === "down" ? -amount : 0;
    const horizontal = direction === "left" ? amount : direction === "right" ? -amount : 0;
    await scrollBy(x, y, vertical, horizontal);
}
/**
 * Scroll by explicit vertical/horizontal deltas at a given cursor position.
 *
 * Positive vertical values scroll up; negative values scroll down.
 * Positive horizontal values scroll left; negative values scroll right.
 */
export async function scrollBy(x, y, vertical, horizontal) {
    await cliclick(`m:${Math.round(x)},${Math.round(y)}`);
    await new Promise((r) => setTimeout(r, 100));
    const bin = await getScrollHelper();
    await execWithTimeout(`"${bin}" ${vertical} ${horizontal}`, { timeout: QUICK_EXEC_TIMEOUT_MS });
}
//# sourceMappingURL=desktop-executor.js.map