/**
 * Terminal injection — Gap 2 of the Terminal Engine.
 *
 * Where the rest of the engine OPENS a surface (a tab / split running a
 * command), injection types into an ALREADY-running surface. It's the primitive
 * a native watchdog (RUSH-1415) needs to nudge a stalled agent with "continue"
 * delivered into the exact terminal that agent lives in — the gap flagged in
 * src/lib/session/provenance.ts:20:
 *
 *   > Actually delivering the keystrokes is Gap 2 (pty/tmux send-keys).
 *
 * It mirrors the engine's shape exactly: pure per-backend spec builders (like
 * `tmuxTabArgv` / `itermTabScript`) produce a `LaunchSpec` (argv), and the same
 * `runSpec` transport runs it — so injection inherits the engine's local/remote
 * (`--device` over SSH) execution for free. A `LaunchSpec` never opens anything;
 * building one is side-effect-free and unit-testable without a display.
 *
 * Backends (each addresses the EXACT split, not the frontmost surface):
 *   - tmux  → `tmux send-keys -t <pane>` — the send-keys primitive addressed by
 *             pane id + socket (the exact rail provenance.ts identifies).
 *   - iterm (macOS) → AppleScript `tell session id "<uuid>" to write text` —
 *             addresses the precise iTerm2 split by its session UUID WITHOUT
 *             `activate`, so it never steals focus or types into the wrong split.
 *             Uses the engine's `appleScriptStr` escaper; guarded by the iterm
 *             backend's `isAvailable` (platform + app installed).
 *   - vscodium (VSCodium / Cursor / VS Code) → the editor CLI's `--open-url`
 *             into the swarmify `swarm-ext` extension's `/inject` verb, targeting
 *             a live-terminals.json terminal by id. Focus-independent, exact
 *             terminal, and (like the launch backend, src/lib/terminal/backends/
 *             vscodium-agent.ts) works over `--device` SSH and on Linux.
 *   - ghostty (macOS) → COARSE only: Ghostty has no scripting dictionary, so
 *             there is no per-split addressing — this raises a window and types
 *             via System Events keystrokes, stealing focus. The resolver refuses
 *             to route here by default (see resolve.ts); it stays behind an
 *             explicit opt-in.
 *   - pty   → the `agents pty write` sidecar path (ptyRequest). Local-only —
 *             the sidecar is not an engine transport surface.
 *
 * Ink-TUI Enter semantics: Claude's Ink TUI swallows an Enter fused to the text,
 * so the default path delivers the text and the Enter as TWO SEPARATE writes
 * (swarmify's `sendText(text,false)` then `sendText('\r',false)`). `combined`
 * opts into one fused write for plain shells / REPLs.
 */
import { ptyRequest } from '../pty-client.js';
import { appleScriptStr } from './quote.js';
import { runSpec } from './transport.js';
import { itermBackend, ghosttyBackend } from './backends/index.js';
import { currentContext } from './types.js';
/**
 * Whether a successful transport on this backend CONFIRMS delivery to the agent.
 * Everything but vscodium self-confirms (see InjectResult.confirmed); vscodium's
 * `--open-url` is fire-and-forget until the swarm-ext extension acks the verb.
 */
export function backendConfirmsDelivery(backend) {
    return backend !== 'vscodium';
}
/** Carriage return — what Enter delivers into a raw PTY/tmux byte stream. */
const CR = '\r';
// --- tmux -------------------------------------------------------------------
/**
 * argv for `tmux send-keys` targeting a pane by id. Starts with `tmux` (like the
 * engine's `tmuxTabArgv`) so the same transport runs it locally or over SSH. The
 * socket, when set, is positioned before the subcommand (`-S` must lead). `-l`
 * sends the keys literally; without it tmux interprets a key name like `Enter`.
 *
 * NOTE: distinct from `sendKeys()` in src/lib/tmux/session.ts — that one
 * addresses by session *name* on the default socket (the `agents tmux` surface)
 * and shells out directly; this one addresses by *pane id* + arbitrary socket and
 * returns an SSH-capable `LaunchSpec` for the engine transport. Don't collapse them.
 */
export function tmuxSendKeysArgv(pane, keys, opts = {}) {
    const argv = ['tmux'];
    if (opts.socket)
        argv.push('-S', opts.socket);
    argv.push('send-keys', '-t', pane);
    if (opts.literal)
        argv.push('-l');
    argv.push(keys);
    return argv;
}
/** The one-or-two send-keys specs for a tmux injection (Ink-safe split by default). */
export function tmuxInjectSpecs(target, text, o) {
    const socket = o.socket ?? target.socket;
    if (o.enter && o.combined) {
        return [{ argv: tmuxSendKeysArgv(target.pane, text + CR, { literal: true, socket }) }];
    }
    const specs = [{ argv: tmuxSendKeysArgv(target.pane, text, { literal: true, socket }) }];
    // A separate Enter keypress — its own write, so the Ink TUI sees Enter alone.
    if (o.enter)
        specs.push({ argv: tmuxSendKeysArgv(target.pane, 'Enter', { socket }) });
    return specs;
}
// --- macOS (iterm / ghostty) ------------------------------------------------
/**
 * AppleScript that types into a SPECIFIC iTerm2 split via `tell session id
 * "<uuid>" to write text` — the exact split provenance's iterm rail identifies,
 * addressed by its session UUID. Crucially there is NO `activate`: `write text`
 * delivers to that session directly, so injection never brings iTerm forward or
 * types into whatever split happens to be focused. Omitting `session` targets
 * the current session (a direct-call convenience; the resolver always supplies
 * one).
 *
 * Enter semantics — iTerm's `write text` appends a trailing newline, which fuses
 * text+Enter into ONE write and Claude's Ink TUI swallows it. So the Ink-safe
 * default (enter, not combined) suppresses that newline (`write text … newline
 * no`) and sends the Return as a SEPARATE `write text` of a lone CR (`character
 * id 13`) — two distinct pty writes, Enter seen on its own. `combined` opts into
 * the single fused `write text` (auto-newline) for plain shells / REPLs.
 *
 * NOTE (macOS-only verification pending): the two-write CR path is the safe
 * choice because it mirrors the tmux/pty Ink-safe split that IS verified here on
 * Linux; whether a single `write text T` would also submit under Ink can only be
 * confirmed on a Mac running iTerm (see the PR body).
 */
export function itermInjectScript(text, opts) {
    // Two writes for the Ink-safe default; one fused write for combined / enter=false.
    const body = opts.enter && opts.combined
        ? [`write text ${appleScriptStr(text)}`]
        : opts.enter
            ? [`write text ${appleScriptStr(text)} newline no`, 'write text (character id 13) newline no']
            : [`write text ${appleScriptStr(text)} newline no`];
    const target = opts.session
        ? `session id ${appleScriptStr(opts.session)}`
        : 'current session of current window';
    return [
        'tell application "iTerm2"',
        `  tell ${target}`,
        ...body.map((l) => `    ${l}`),
        '  end tell',
        'end tell',
    ].join('\n');
}
/**
 * AppleScript for Ghostty (no scripting dictionary): raise the target window via
 * System Events, then keystroke the text and a separate Return. `window` matches
 * a window whose title contains the string; omitted → the frontmost Ghostty window.
 */
export function ghosttyInjectScript(text, opts) {
    const lines = ['tell application "System Events"', '  tell process "ghostty"', '    set frontmost to true'];
    if (opts.window) {
        lines.push(`    perform action "AXRaise" of (first window whose title contains ${appleScriptStr(opts.window)})`);
    }
    lines.push('  end tell');
    lines.push(`  keystroke ${appleScriptStr(text)}`);
    if (opts.enter)
        lines.push('  key code 36');
    lines.push('end tell');
    return lines.join('\n');
}
/** The osascript spec for a macOS injection. One invocation delivers the text + Return. */
export function appleScriptInjectSpec(target, text, enter, combined = false) {
    const script = target.backend === 'iterm'
        ? itermInjectScript(text, { session: target.session, enter, combined })
        : ghosttyInjectScript(text, { window: target.window, enter });
    return { argv: ['osascript', '-e', script] };
}
// --- vscodium (VSCodium / Cursor / VS Code integrated terminal) -------------
/** The swarmify extension identifier that owns the swarm-ext URI verbs (matches the launch backend). */
const EXTENSION_AUTHORITY = 'swarmify.swarm-ext';
/**
 * The `<scheme>://swarmify.swarm-ext/inject?p=<payload>` URL the extension
 * handles to type into an ALREADY-open integrated terminal (the inject sibling
 * of the launch backend's `/spawn`, src/lib/terminal/backends/vscodium-agent.ts).
 *
 * Payload is base64url-encoded JSON in a single `p` param — identical encoding
 * to `spawnUri`, and for the same reason: VS Code percent-decodes `uri.query`
 * once before the extension parses it, so a naive multi-param query would
 * mis-split a text containing `&`/`=`; base64url (`[A-Za-z0-9_-]`) survives that
 * decode untouched. `terminalId` is the id the extension keys live-terminals.json
 * on (the session UUID); `enter` tells the extension to submit; `combined` asks
 * it to fuse text+Enter into one write (default is the Ink-safe two-write split,
 * mirroring the tmux/iterm paths — the extension owns that split on its side).
 *
 * DEPENDENCY: the `/inject` verb ships in the swarm-ext extension via PR #608's
 * successor (PR #608 introduces `/spawn`; the inject verb + `write`/`enter`
 * handling is its follow-up). Until that lands, this routes correctly on the CLI
 * side but the extension will no-op the unknown verb. See the PR body.
 */
export function vscodiumInjectUri(scheme, terminalId, text, opts) {
    const payload = { terminalId, text, enter: opts.enter, combined: opts.combined };
    const p = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
    return `${scheme}://${EXTENSION_AUTHORITY}/inject?p=${p}`;
}
/** The editor-CLI spec for a VSCodium/Cursor/VS Code injection (one `--open-url` invocation). */
export function vscodiumInjectSpec(target, text, opts) {
    return { argv: [target.cli, '--open-url', vscodiumInjectUri(target.scheme, target.terminalId, text, opts)] };
}
// --- pty --------------------------------------------------------------------
async function injectPty(target, text, o) {
    if (o.host && o.host !== 'local') {
        return { ok: false, confirmed: false, backend: 'pty', writes: 0, error: 'pty injection is local-only (the sidecar is not a remote surface)' };
    }
    // Same Ink-safe split as tmux: text write, then a separate CR write.
    const writes = o.enter && o.combined ? [text + CR] : o.enter ? [text, CR] : [text];
    if (o.dryRun)
        return { ok: true, confirmed: true, backend: 'pty', writes: writes.length };
    try {
        for (const input of writes) {
            const res = await ptyRequest('write', target.id, { input });
            if (!res.ok)
                return { ok: false, confirmed: false, backend: 'pty', writes: 0, error: res.error ?? 'pty write failed' };
        }
        // A successful sidecar write reached the fd — real delivery.
        return { ok: true, confirmed: true, backend: 'pty', writes: writes.length };
    }
    catch (err) {
        return { ok: false, confirmed: false, backend: 'pty', writes: 0, error: err instanceof Error ? err.message : String(err) };
    }
}
// --- public entry -----------------------------------------------------------
/**
 * Deliver `text` (+ Enter) into an existing terminal surface. Resolves the
 * target's backend and routes to the matching primitive. Never throws — launch
 * failures come back in the result, like the engine's `openSurface`.
 */
export async function injectIntoTerminal(target, text, opts = {}) {
    const enter = opts.enter !== false;
    const combined = opts.combined === true;
    if (target.backend === 'pty') {
        return injectPty(target, text, { enter, combined, host: opts.host, dryRun: opts.dryRun });
    }
    // tmux + AppleScript + editor-CLI backends all run through the engine transport.
    const specs = target.backend === 'tmux'
        ? tmuxInjectSpecs(target, text, { enter, combined, socket: opts.socket })
        : target.backend === 'vscodium'
            ? [vscodiumInjectSpec(target, text, { enter, combined })]
            : [appleScriptInjectSpec(target, text, enter, combined)];
    // Discrete writes delivered on the far side. tmux counts its send-keys calls.
    // iterm/vscodium honor `combined` (text+Enter fused into one write). ghostty's
    // coarse keystroke path ignores `combined` (it always emits keystroke + a
    // separate Return), so its count tracks `enter` alone.
    const writes = target.backend === 'tmux'
        ? specs.length
        : target.backend === 'ghostty'
            ? enter ? 2 : 1
            : enter && !combined ? 2 : 1;
    const confirmed = backendConfirmsDelivery(target.backend);
    if (opts.dryRun)
        return { ok: true, confirmed, backend: target.backend, writes, specs };
    // AppleScript backends: guard on the backend's own availability, but only for a
    // LOCAL run (a remote host is assumed to have the app — its ssh leg reports failure).
    if (target.backend === 'iterm' || target.backend === 'ghostty') {
        if (!opts.host || opts.host === 'local') {
            const backend = target.backend === 'iterm' ? itermBackend : ghosttyBackend;
            const ctx = opts.ctx ?? currentContext();
            if (!backend.isAvailable(ctx)) {
                return { ok: false, confirmed: false, backend: target.backend, writes: 0, specs, error: `${backend.label} is not available here (platform ${ctx.platform})` };
            }
        }
    }
    for (const spec of specs) {
        const res = await runSpec(spec, opts.host, opts.resolveHost);
        if (!res.ok)
            return { ok: false, confirmed: false, backend: target.backend, writes: 0, specs, error: res.error };
    }
    return { ok: true, confirmed, backend: target.backend, writes, specs };
}
