/**
 * Swift child-process utilities: compilation (ensureSwiftBinary) and
 * lifecycle management for screen-streamer and input-injector.
 */
import { spawn, execFile } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { logger } from "../../logger.js";
/** Directory containing the Swift source files (same as this module). */
export const SWIFT_SRC_DIR = new URL(".", import.meta.url).pathname;
/** Temporary directory prefix for compiled Swift binaries. */
const CACHE_PREFIX = path.join(os.tmpdir(), "visionclaw-rdp-");
/**
 * Ensure a compiled Swift binary is up to date.
 * Compiles the .swift source if the binary is missing or stale.
 * Returns the path to the compiled binary.
 */
export async function ensureSwiftBinary(name, frameworks = []) {
    const srcPath = path.join(SWIFT_SRC_DIR, `${name}.swift`);
    const binPath = `${CACHE_PREFIX}${name}`;
    const srcStat = fs.statSync(srcPath, { throwIfNoEntry: false });
    const binStat = fs.statSync(binPath, { throwIfNoEntry: false });
    if (binStat && srcStat && binStat.mtimeMs > srcStat.mtimeMs) {
        logger.debug(`Swift binary up to date: ${binPath}`);
        return binPath;
    }
    logger.debug(`Compiling ${name}.swift...`);
    const args = ["-O", "-o", binPath, srcPath];
    for (const fw of frameworks) {
        args.push("-framework", fw);
    }
    const startedMs = Date.now();
    return new Promise((resolve, reject) => {
        execFile("swiftc", args, { timeout: 120_000 }, (err, _stdout, stderr) => {
            if (err) {
                logger.err(`Failed to compile ${name}.swift: ${stderr}`);
                reject(new Error(`Swift compilation failed: ${stderr}`));
                return;
            }
            // Single consolidated line instead of separate "Compiling…" / "Compiled"
            // pair. The path is uninteresting for normal users — debug has the full
            // path if needed.
            logger.system(`Compiled ${name}.swift (${Date.now() - startedMs}ms)`);
            logger.debug(`Compiled binary path: ${binPath}`);
            resolve(binPath);
        });
    });
}
/**
 * Lines from screen-streamer stderr that are purely internal progress noise.
 * Promote them to `debug` so the default console only shows lifecycle events
 * that actually matter to an operator watching the terminal.
 *
 * Kept at `system` (visible by default): `Capture started …` and errors.
 */
export const STREAMER_DEBUG_PATTERNS = [
    /^Frame #\d+ received$/,
    /^FPS changed /,
    /^Bitrate changed /,
    /^Keyframe requested$/,
    /^Capture config: /, // superseded by "Capture started" on same cycle
    /^Encoder created: /, // superseded by "Capture started" on same cycle
    /^Resolution changed: /, // mid-stream resize: debug-only
    /^Displays: \d+$/, // streamer-parser already logs screen count
    /^Switch to display \d+ requested$/, // caller logs "[rdp] Switch to display N"
    /^Stopping old stream for display /, // implementation detail of display switch
];
/**
 * Build a line-buffered emitter for a child process stream. Chunks from
 * `stream.on("data")` have no line-boundary guarantees — they may bundle
 * several lines together (which we saw leak out as "bare" terminal output)
 * or split a single line across chunks. Buffer the tail until a newline is
 * observed, then flush each complete line through `onLine`.
 */
export function createLineBuffer(onLine) {
    let carry = "";
    return (chunk) => {
        const text = carry + chunk.toString();
        const lines = text.split(/\r?\n/);
        // The last element is whatever comes after the final `\n` — possibly empty,
        // possibly a partial line. Stash it for the next chunk.
        carry = lines.pop() ?? "";
        for (const raw of lines) {
            const line = raw.trim();
            if (line)
                onLine(line);
        }
    };
}
/**
 * Start the screen-streamer Swift process using the `swift` interpreter
 * (not a pre-compiled binary) so it inherits the terminal's Screen Recording
 * entitlement from macOS System Preferences.
 *
 * @returns The spawned ChildProcess.
 */
export function startStreamerProcess(onData, onExit) {
    const srcPath = path.join(SWIFT_SRC_DIR, "screen-streamer.swift");
    logger.debug(`Starting screen-streamer (swift ${srcPath})...`);
    const proc = spawn("swift", [srcPath], {
        stdio: ["pipe", "pipe", "pipe"],
    });
    // If the child crashes between when we decide to write and when the write
    // flushes, stdin surfaces an async EPIPE. Swallow it here — the exit
    // handler below already logs the real cause.
    proc.stdin.on("error", (err) => {
        logger.debug(`[screen-streamer] stdin error: ${err.message}`);
    });
    proc.stderr.on("data", createLineBuffer((line) => {
        const tagged = `[screen-streamer] ${line}`;
        if (STREAMER_DEBUG_PATTERNS.some((re) => re.test(line))) {
            logger.debug(tagged);
        }
        else {
            logger.system(tagged);
        }
    }));
    // stdout carries the length-prefixed binary video protocol — pass raw
    // chunks straight to the parser.
    proc.stdout.on("data", onData);
    proc.on("exit", (code) => {
        // Normal shutdown paths all pass code=null (SIGTERM) — don't spam a
        // system-level line for those; the caller logs the higher-level reason.
        if (code === null || code === 0) {
            logger.debug(`screen-streamer exited with code ${String(code)}`);
        }
        else {
            logger.warn(`screen-streamer exited with code ${String(code)}`);
        }
        onExit(code);
    });
    return proc;
}
/**
 * Start the input-injector compiled binary.
 *
 * @param binPath - Path to the compiled input-injector binary.
 * @returns The spawned ChildProcess.
 */
export function startInjectorProcess(binPath, onExit) {
    logger.debug("Starting input-injector...");
    const proc = spawn(binPath, [], {
        stdio: ["pipe", "pipe", "pipe"],
    });
    proc.stdin.on("error", (err) => {
        logger.debug(`[input-injector] stdin error: ${err.message}`);
    });
    // input-injector is currently write-only from our side (we send action
    // commands on stdin). Both stderr and stdout are only used for textual
    // status output ("ok"/"ready"/error messages). If a binary stdout protocol
    // is ever added, give it its own dedicated handler instead of funnelling
    // stdout through the text log path here.
    proc.stderr.on("data", createLineBuffer((line) => {
        logger.debug(`[input-injector] ${line}`);
    }));
    proc.stdout.on("data", createLineBuffer((line) => {
        if (line === "ok" || line === "ready")
            return;
        logger.debug(`[input-injector] ${line}`);
    }));
    proc.on("exit", (code) => {
        if (code === null || code === 0) {
            logger.debug(`input-injector exited with code ${String(code)}`);
        }
        else {
            logger.warn(`input-injector exited with code ${String(code)}`);
        }
        onExit(code);
    });
    return proc;
}
/** Terminate a child process gracefully with SIGTERM. */
export function killProcess(proc, label) {
    if (!proc)
        return;
    // Debug-level: the callers that matter (idle timeout, explicit stop) log
    // a single higher-level line already.
    logger.debug(`Stopping ${label}...`);
    proc.stdin?.end();
    proc.kill("SIGTERM");
}
//# sourceMappingURL=swift-process.js.map