/**
 * Android executor — ADB-based counterpart to the desktop executor.
 *
 * All interactions go through `adb shell` commands.
 * Screenshot pipeline mirrors the desktop implementation:
 *   capture → resize for vision → compress if needed → persist to ~/screenshots/
 */
import { exec } from "node:child_process";
import { promisify } from "node:util";
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { logger } from "../logger.js";
import { getScreenshotsDir } from "../config/index.js";
import { resizeForVision } from "./screenshot.js";
import { execWithTimeout, HEAVY_EXEC_TIMEOUT_MS } from "../utils/exec.js";
// Preserved for the screencap path where we need Buffer output — the shared
// `execWithTimeout` helper returns strings, which would corrupt binary PNG
// data. All other adb calls in this file go through `execWithTimeout`.
const execAsyncBuffer = promisify(exec);
const MAX_SCREENSHOT_SIZE = 1 * 1024 * 1024; // 1 MB
const ADB_KEYBOARD_PACKAGE = "com.android.adbkeyboard";
const ADB_KEYBOARD_IME = "com.android.adbkeyboard/.AdbIME";
const ADB_KEYBOARD_CACHE_APK = path.join(os.tmpdir(), "visionclaw-adbkeyboard.apk");
const ADB_KEYBOARD_DOWNLOAD_URLS = [
    "https://github.com/senzhk/ADBKeyBoard/releases/download/v2.4-dev/keyboardservice-debug.apk",
    "https://github.com/senzhk/ADBKeyBoard/raw/master/ADBKeyboard.apk",
];
const ADB_KEYBOARD_LOCAL_APK_CANDIDATES = [
    process.env.ADBKEYBOARD_APK_PATH,
    path.join(process.cwd(), "ADBKeyboard.apk"),
    path.join(process.cwd(), "scripts", "ADBKeyboard.apk"),
].filter((p) => Boolean(p?.trim()));
// ── ADB device detection ──────────────────────────────────────────────
let _cachedSerial = null;
/** Cache whether ADBKeyboard is the active IME (set lazily in typeText). */
let _adbKeyboardActive = null;
let _adbKeyboardInstallAttempted = false;
/**
 * Find a connected ADB device serial.
 * Caches after first successful detection; reset with `resetDevice()`.
 */
async function getDeviceSerial() {
    if (_cachedSerial)
        return _cachedSerial;
    const { stdout } = await execWithTimeout("adb devices");
    const lines = stdout.trim().split("\n").slice(1); // skip header
    for (const line of lines) {
        const [serial, state] = line.trim().split(/\s+/);
        if (serial && state === "device") {
            _cachedSerial = serial;
            return serial;
        }
    }
    throw new Error("No ADB device connected. Please connect a device and enable USB debugging.");
}
/** Clear the cached device serial (useful when device reconnects). */
export function resetDevice() {
    _cachedSerial = null;
    _cachedDisplaySize = null;
    _adbKeyboardActive = null;
}
function adbCmd(shellCmd) {
    // We intentionally don't use -s serial here; getDeviceSerial validates
    // a device is present, and omitting -s works fine for single-device setups.
    // For multi-device, callers can set ANDROID_SERIAL env var.
    return `adb shell ${shellCmd}`;
}
// ── Display size ──────────────────────────────────────────────────────
let _cachedDisplaySize = null;
export async function initDisplaySize() {
    try {
        await getDeviceSerial(); // ensure device is connected
        const { stdout } = await execWithTimeout(adbCmd("wm size"));
        // Output: "Physical size: 1080x2400" or "Override size: ..."
        // Prefer physical size
        const physMatch = /Physical size:\s*(\d+)x(\d+)/.exec(stdout);
        const overMatch = /Override size:\s*(\d+)x(\d+)/.exec(stdout);
        const match = overMatch ?? physMatch;
        if (match) {
            _cachedDisplaySize = {
                width: parseInt(match[1], 10),
                height: parseInt(match[2], 10),
            };
            logger.info(`Android display size: ${_cachedDisplaySize.width}x${_cachedDisplaySize.height}`);
        }
    }
    catch (e) {
        logger.warn(`Failed to detect Android display size: ${e instanceof Error ? e.message : String(e)}`);
    }
}
export function getDisplaySize() {
    return _cachedDisplaySize ?? { width: 1080, height: 2400 };
}
/** Returns true if display size has been detected from the device. */
export function isDisplaySizeInitialized() {
    return _cachedDisplaySize !== null;
}
// ── Screenshot ────────────────────────────────────────────────────────
/**
 * Capture the Android screen via ADB, resize for Claude's vision, compress,
 * and persist to ~/screenshots/.
 *
 * Uses `adb exec-out screencap -p` which streams raw PNG bytes to stdout,
 * avoiding the need for an intermediate file on the device.
 */
export async function captureScreen() {
    const screenshotsDir = getScreenshotsDir();
    await fs.mkdir(screenshotsDir, { recursive: true });
    const tmpFile = path.join(os.tmpdir(), `android-screenshot-${Date.now()}.png`);
    try {
        await getDeviceSerial(); // validate device is connected
        // exec-out streams binary data to stdout without \r\n corruption on Windows.
        // Uses the raw buffer-returning variant + manual timeout since the shared
        // helper stringifies stdout which would corrupt PNG bytes.
        const { stdout } = await execAsyncBuffer("adb exec-out screencap -p", {
            encoding: "buffer",
            maxBuffer: 20 * 1024 * 1024, // 20 MB for high-res screens
            timeout: HEAVY_EXEC_TIMEOUT_MS,
            killSignal: "SIGKILL",
        });
        const rawBuffer = Buffer.isBuffer(stdout)
            ? stdout
            : Buffer.from(stdout, "binary");
        if (rawBuffer.length < 100) {
            throw new Error(`screencap returned only ${rawBuffer.length} bytes — device may be in a bad state`);
        }
        // Pipeline: resize for vision → compress if needed
        const resized = await resizeForVision(rawBuffer);
        const compressed = await compressIfNeeded(resized);
        const filePath = path.join(screenshotsDir, `android-screenshot-${Date.now()}.png`);
        await fs.writeFile(filePath, compressed);
        return { base64: compressed.toString("base64"), filePath };
    }
    finally {
        try {
            await fs.unlink(tmpFile);
        }
        catch { /* noop */ }
    }
}
/**
 * Compress PNG if it exceeds size threshold.
 * Uses ffmpeg palette quantization (same approach as desktop screenshot.ts).
 */
async function compressIfNeeded(buffer, maxSize = MAX_SCREENSHOT_SIZE) {
    if (buffer.length <= maxSize)
        return buffer;
    const tmpIn = path.join(os.tmpdir(), `android-compress-in-${Date.now()}.png`);
    const tmpOut = path.join(os.tmpdir(), `android-compress-out-${Date.now()}.png`);
    try {
        await fs.writeFile(tmpIn, buffer);
        for (const maxColors of [128, 64]) {
            try {
                const palettePath = tmpIn.replace(/\.png$/, "-palette.png");
                const ffmpeg = "ffmpeg";
                await execWithTimeout(`${ffmpeg} -y -i "${tmpIn}" -vf "palettegen=max_colors=${maxColors}" "${palettePath}"`, { timeout: HEAVY_EXEC_TIMEOUT_MS });
                await execWithTimeout(`${ffmpeg} -y -i "${tmpIn}" -i "${palettePath}" -lavfi "paletteuse=dither=bayer" -compression_level 100 "${tmpOut}"`, { timeout: HEAVY_EXEC_TIMEOUT_MS });
                try {
                    await fs.unlink(palettePath);
                }
                catch { /* noop */ }
                const compressed = await fs.readFile(tmpOut);
                logger.debug(`Android screenshot compressed (${maxColors} colours): ${buffer.length} → ${compressed.length} bytes`);
                if (compressed.length <= maxSize)
                    return compressed;
                return compressed; // still return even if over limit
            }
            catch {
                // ffmpeg not available or failed — return original
            }
        }
    }
    finally {
        try {
            await fs.unlink(tmpIn);
        }
        catch { /* noop */ }
        try {
            await fs.unlink(tmpOut);
        }
        catch { /* noop */ }
    }
    return buffer;
}
// ── Input actions ─────────────────────────────────────────────────────
export async function tap(x, y) {
    await execWithTimeout(adbCmd(`input tap ${Math.round(x)} ${Math.round(y)}`));
}
export async function doubleTap(x, y) {
    // ADB has no native double-tap; simulate with two quick taps
    await execWithTimeout(adbCmd(`input tap ${Math.round(x)} ${Math.round(y)}`));
    await new Promise((r) => setTimeout(r, 100));
    await execWithTimeout(adbCmd(`input tap ${Math.round(x)} ${Math.round(y)}`));
}
export async function longPress(x, y, durationMs = 1000) {
    // Long press = swipe to same point with duration
    const rx = Math.round(x);
    const ry = Math.round(y);
    await execWithTimeout(adbCmd(`input swipe ${rx} ${ry} ${rx} ${ry} ${durationMs}`));
}
export async function swipe(x1, y1, x2, y2, durationMs = 300) {
    await execWithTimeout(adbCmd(`input swipe ${Math.round(x1)} ${Math.round(y1)} ${Math.round(x2)} ${Math.round(y2)} ${durationMs}`));
}
/**
 * ADBKeyboard — enables Unicode input (CJK, emoji, etc.) via ADB.
 * Install: adb install ADBKeyboard.apk
 * Enable:  adb shell ime enable com.android.adbkeyboard/.AdbIME
 * Set:     adb shell ime set com.android.adbkeyboard/.AdbIME
 */
/**
 * Check if ADBKeyboard is the currently active input method.
 * The broadcast-based input (`ADB_INPUT_B64`) only works when
 * ADBKeyboard is the active IME, not merely enabled.
 */
async function isAdbKeyboardActive() {
    if (_adbKeyboardActive !== null)
        return _adbKeyboardActive;
    try {
        const { stdout } = await execWithTimeout(adbCmd("settings get secure default_input_method"));
        _adbKeyboardActive = stdout.trim().includes("com.android.adbkeyboard");
        return _adbKeyboardActive;
    }
    catch {
        _adbKeyboardActive = false;
        return false;
    }
}
async function isAdbKeyboardAvailable() {
    try {
        const { stdout } = await execWithTimeout(adbCmd("ime list -s"));
        return stdout
            .split("\n")
            .map((line) => line.trim())
            .includes(ADB_KEYBOARD_IME);
    }
    catch {
        return false;
    }
}
async function isAdbKeyboardInstalled() {
    try {
        const { stdout } = await execWithTimeout(adbCmd(`pm path ${ADB_KEYBOARD_PACKAGE}`));
        return stdout.includes(ADB_KEYBOARD_PACKAGE);
    }
    catch {
        return false;
    }
}
async function pickAdbKeyboardApkPath() {
    for (const candidate of ADB_KEYBOARD_LOCAL_APK_CANDIDATES) {
        try {
            await fs.access(candidate);
            logger.info(`Using local ADBKeyboard APK: ${candidate}`);
            return candidate;
        }
        catch {
            // try next
        }
    }
    try {
        await fs.access(ADB_KEYBOARD_CACHE_APK);
        logger.info(`Using cached ADBKeyboard APK: ${ADB_KEYBOARD_CACHE_APK}`);
        return ADB_KEYBOARD_CACHE_APK;
    }
    catch {
        // continue and download
    }
    for (const url of ADB_KEYBOARD_DOWNLOAD_URLS) {
        try {
            logger.info(`Downloading ADBKeyboard APK from ${url}`);
            const response = await fetch(url);
            if (!response.ok) {
                logger.warn(`ADBKeyboard download failed (${response.status}) from ${url}`);
                continue;
            }
            const arrayBuffer = await response.arrayBuffer();
            const apkBuffer = Buffer.from(arrayBuffer);
            if (apkBuffer.length < 100_000) {
                logger.warn(`ADBKeyboard download too small (${apkBuffer.length} bytes) from ${url}`);
                continue;
            }
            await fs.writeFile(ADB_KEYBOARD_CACHE_APK, apkBuffer);
            logger.info(`ADBKeyboard APK downloaded to ${ADB_KEYBOARD_CACHE_APK}`);
            return ADB_KEYBOARD_CACHE_APK;
        }
        catch (e) {
            logger.warn(`ADBKeyboard download error from ${url}: ${e instanceof Error ? e.message : String(e)}`);
        }
    }
    return null;
}
async function ensureAdbKeyboardInstalled() {
    if (await isAdbKeyboardInstalled())
        return true;
    if (_adbKeyboardInstallAttempted) {
        return await isAdbKeyboardInstalled();
    }
    _adbKeyboardInstallAttempted = true;
    const apkPath = await pickAdbKeyboardApkPath();
    if (!apkPath) {
        logger.warn("ADBKeyboard APK not found locally and download failed. " +
            "Set ADBKEYBOARD_APK_PATH or install manually.");
        return false;
    }
    try {
        await execWithTimeout(`adb install -r "${apkPath}"`, { timeout: HEAVY_EXEC_TIMEOUT_MS });
    }
    catch (e) {
        const message = e instanceof Error ? e.message : String(e);
        // Treat already-installed variants as success.
        if (!/INSTALL_FAILED_ALREADY_EXISTS|Already installed/i.test(message)) {
            logger.warn(`ADBKeyboard install failed: ${message}`);
            return false;
        }
    }
    try {
        await execWithTimeout(adbCmd(`ime enable ${ADB_KEYBOARD_IME}`));
    }
    catch {
        // best effort
    }
    const installed = await isAdbKeyboardInstalled();
    if (installed) {
        logger.info("ADBKeyboard installed successfully.");
    }
    return installed;
}
/**
 * Ensure ADBKeyboard is active when we need robust Unicode input.
 * This uses the activation flow from ADBKeyboard docs:
 *   ime enable ... && ime set ...
 */
async function ensureAdbKeyboardActive() {
    if (await isAdbKeyboardActive())
        return true;
    if (!(await isAdbKeyboardAvailable())) {
        const installed = await ensureAdbKeyboardInstalled();
        if (!installed)
            return false;
    }
    try {
        await execWithTimeout(adbCmd(`ime enable ${ADB_KEYBOARD_IME}`));
        await execWithTimeout(adbCmd(`ime set ${ADB_KEYBOARD_IME}`));
        _adbKeyboardActive = null; // refresh cache after switching IME
        return await isAdbKeyboardActive();
    }
    catch (e) {
        logger.warn(`Failed to switch to ADBKeyboard IME: ${e instanceof Error ? e.message : String(e)}`);
        _adbKeyboardActive = false;
        return false;
    }
}
/**
 * Type text via ADBKeyboard broadcast intent.
 * This supports full Unicode including CJK characters.
 */
async function typeViaAdbKeyboard(text) {
    // ADBKeyboard accepts text via am broadcast with base64 encoding
    const b64 = Buffer.from(text, "utf-8").toString("base64");
    await execWithTimeout(adbCmd(`am broadcast -a ADB_INPUT_B64 --es msg '${b64}'`));
}
async function clearViaAdbKeyboard() {
    await execWithTimeout(adbCmd("am broadcast -a ADB_CLEAR_TEXT"));
}
export async function typeText(text, options) {
    const replace = options?.replace ?? false;
    // Check if text contains non-ASCII characters (CJK, emoji, etc.)
    const hasNonAscii = /[^\x20-\x7E]/.test(text);
    const preferAdbKeyboard = hasNonAscii || replace;
    if (preferAdbKeyboard) {
        // Use ADBKeyboard whenever replace/unicode reliability matters.
        if (await ensureAdbKeyboardActive()) {
            if (replace) {
                await clearViaAdbKeyboard();
                await new Promise((r) => setTimeout(r, 120));
            }
            await typeViaAdbKeyboard(text);
            return;
        }
        // ADBKeyboard unavailable — fall back, but still honour `replace` by
        // clearing the current field via the generic selectAll + delete path.
        // Without this, `replace: true` would silently append instead of
        // replacing when ADBKeyboard can't be installed/activated.
        if (replace) {
            try {
                await selectAll();
                await new Promise((r) => setTimeout(r, 200));
                await pressKey("KEYCODE_DEL");
                await new Promise((r) => setTimeout(r, 200));
            }
            catch (e) {
                logger.warn(`replace fallback (selectAll + delete) failed: ${e instanceof Error ? e.message : String(e)}`);
            }
        }
        if (hasNonAscii) {
            logger.warn("ADBKeyboard is unavailable. Falling back to adb input text (non-ASCII chars may be lost).");
        }
    }
    // ASCII-only text: use standard adb shell input text
    // Escape shell-special characters for adb shell
    const escaped = text.replace(/([ '"\\&|;`$!<>(){}[\]#*?~^])/g, "\\$1");
    await execWithTimeout(adbCmd(`input text "${escaped}"`));
}
export async function pressKey(key) {
    // Map common key names to Android KEYCODE values
    const keyMap = {
        back: "KEYCODE_BACK",
        home: "KEYCODE_HOME",
        recents: "KEYCODE_APP_SWITCH",
        enter: "KEYCODE_ENTER",
        return: "KEYCODE_ENTER",
        tab: "KEYCODE_TAB",
        escape: "KEYCODE_ESCAPE",
        delete: "KEYCODE_DEL",
        backspace: "KEYCODE_DEL",
        space: "KEYCODE_SPACE",
        up: "KEYCODE_DPAD_UP",
        down: "KEYCODE_DPAD_DOWN",
        left: "KEYCODE_DPAD_LEFT",
        right: "KEYCODE_DPAD_RIGHT",
        power: "KEYCODE_POWER",
        volume_up: "KEYCODE_VOLUME_UP",
        volume_down: "KEYCODE_VOLUME_DOWN",
        camera: "KEYCODE_CAMERA",
        menu: "KEYCODE_MENU",
        search: "KEYCODE_SEARCH",
        wakeup: "KEYCODE_WAKEUP",
        sleep: "KEYCODE_SLEEP",
        // Media keys
        play: "KEYCODE_MEDIA_PLAY",
        pause: "KEYCODE_MEDIA_PAUSE",
        play_pause: "KEYCODE_MEDIA_PLAY_PAUSE",
        next: "KEYCODE_MEDIA_NEXT",
        previous: "KEYCODE_MEDIA_PREVIOUS",
    };
    const normalized = key.toLowerCase().trim();
    // Check if it's already a KEYCODE_* value
    if (normalized.startsWith("keycode_")) {
        await execWithTimeout(adbCmd(`input keyevent ${key.toUpperCase()}`));
        return;
    }
    // Check our map
    const mapped = keyMap[normalized];
    if (mapped) {
        await execWithTimeout(adbCmd(`input keyevent ${mapped}`));
        return;
    }
    // Try as a numeric keycode
    if (/^\d+$/.test(normalized)) {
        await execWithTimeout(adbCmd(`input keyevent ${normalized}`));
        return;
    }
    // Fallback: try the key name as-is with KEYCODE_ prefix
    await execWithTimeout(adbCmd(`input keyevent KEYCODE_${key.toUpperCase()}`));
}
/**
 * Select all text in the currently focused input field.
 * Tries `input keycombination` (Android 12+) first, then falls back
 * to Home → Shift+End selection for older versions.
 */
export async function selectAll() {
    try {
        // Android 12+ (API 31): native key combination support
        // KEYCODE_CTRL_LEFT=113, KEYCODE_A=29
        await execWithTimeout(adbCmd("input keycombination 113 29"));
    }
    catch {
        // Fallback for older Android versions:
        // Move cursor to start, then Shift+End to select all text.
        // KEYCODE_SHIFT_LEFT=59, KEYCODE_MOVE_END=123
        await execWithTimeout(adbCmd("input keyevent KEYCODE_MOVE_HOME"));
        try {
            await execWithTimeout(adbCmd("input keycombination 59 123"));
        }
        catch {
            // keycombination not available at all (pre-Android 12):
            // Move to end, then delete backwards to clear the field.
            await execWithTimeout(adbCmd("input keyevent KEYCODE_MOVE_END"));
            for (let i = 0; i < 100; i++) {
                await execWithTimeout(adbCmd("input keyevent KEYCODE_DEL"));
            }
        }
    }
}
export async function scroll(x, y, direction, amount = 3) {
    const distance = amount * 300; // 300px per scroll unit
    const rx = Math.round(x);
    const ry = Math.round(y);
    let x2 = rx;
    let y2 = ry;
    switch (direction) {
        case "up":
            y2 = ry + distance; // swipe up = finger moves down→up, so start below
            break;
        case "down":
            y2 = ry - distance;
            break;
        case "left":
            x2 = rx + distance;
            break;
        case "right":
            x2 = rx - distance;
            break;
    }
    // Clamp to display bounds
    const size = getDisplaySize();
    x2 = Math.max(0, Math.min(size.width, x2));
    y2 = Math.max(0, Math.min(size.height, y2));
    await swipe(rx, ry, x2, y2, 300);
}
/**
 * Wake the device screen if it's off, and unlock if on lock screen.
 */
export async function wakeDevice() {
    try {
        // Check if screen is on
        const { stdout } = await execWithTimeout(adbCmd("dumpsys power | grep 'Display Power'"));
        const isOn = stdout.includes("state=ON");
        if (!isOn) {
            await execWithTimeout(adbCmd("input keyevent KEYCODE_WAKEUP"));
            await new Promise((r) => setTimeout(r, 500));
        }
        // Swipe up to dismiss lock screen (if present)
        const size = getDisplaySize();
        await swipe(size.width / 2, size.height * 0.75, size.width / 2, size.height * 0.25, 300);
        await new Promise((r) => setTimeout(r, 500));
    }
    catch (e) {
        logger.warn(`Failed to wake device: ${e instanceof Error ? e.message : String(e)}`);
    }
}
/**
 * Launch an app by package name.
 */
export async function launchApp(packageName) {
    await execWithTimeout(adbCmd(`monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`));
}
/**
 * Get the currently focused activity.
 */
export async function getCurrentActivity() {
    const { stdout } = await execWithTimeout(adbCmd("dumpsys activity activities | grep -E '(mResumedActivity|ResumedActivity)'"));
    return stdout.trim();
}
//# sourceMappingURL=android-executor.js.map