import * as pty from "node-pty";
import xterm from "@xterm/headless";
import { randomUUID } from "crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
import { tmpdir, userInfo } from "node:os";
import { basename, dirname, join } from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { createExternalProcessEnv } from "../server/paseo-env.js";
import { writePrivateFileAtomicSync } from "../server/private-files.js";
import { TerminalInputModeTracker } from "@kong-link/protocol/terminal-input-mode";
const { Terminal } = xterm;
const require = createRequire(import.meta.url);
let nodePtySpawnHelperChecked = false;
const TERMINAL_TITLE_DEBOUNCE_MS = 150;
const TERMINAL_EXIT_OUTPUT_LINE_LIMIT = 12;
const TERMINAL_EXIT_OUTPUT_CHAR_LIMIT = 16000;
const TERMINAL_OSC_COLOR_QUERY_RESPONSES = new Map([
    [10, "rgb:e6e6/e6e6/e6e6"],
    [11, "rgb:0b0b/0b0b/0b0b"],
    [12, "rgb:e6e6/e6e6/e6e6"],
]);
function parseCommandFinishedOsc(data) {
    // OSC 633 is terminal control traffic, but a foreground command can still
    // print arbitrary control bytes. Keep this boundary to the exact VS Code
    // command-finished shape emitted by our shell integration.
    const parts = data.split(";");
    if (parts[0] !== "D") {
        return null;
    }
    if (parts.length === 1) {
        return { exitCode: null };
    }
    if (parts.length !== 2 || !/^-?\d+$/.test(parts[1])) {
        return null;
    }
    return { exitCode: Number(parts[1]) };
}
function resolveNodePtyPackageRoot() {
    try {
        const packageJsonPath = require.resolve("node-pty/package.json");
        return dirname(packageJsonPath);
    }
    catch {
        return null;
    }
}
function ensureExecutableBit(path) {
    if (!existsSync(path)) {
        return;
    }
    const stat = statSync(path);
    if (!stat.isFile()) {
        return;
    }
    // node-pty 1.1.0 shipped darwin prebuild spawn-helper without execute bit.
    if ((stat.mode & 0o111) === 0o111) {
        return;
    }
    chmodSync(path, stat.mode | 0o111);
}
export function ensureNodePtySpawnHelperExecutableForCurrentPlatform(options = {}) {
    const platform = options.platform ?? process.platform;
    if (platform !== "darwin") {
        return;
    }
    if (nodePtySpawnHelperChecked && !options.force) {
        return;
    }
    const packageRoot = options.packageRoot ?? resolveNodePtyPackageRoot();
    if (!packageRoot) {
        return;
    }
    const arch = options.arch ?? process.arch;
    const candidates = [
        join(packageRoot, "build", "Release", "spawn-helper"),
        join(packageRoot, "build", "Debug", "spawn-helper"),
        join(packageRoot, "prebuilds", `darwin-${arch}`, "spawn-helper"),
    ];
    for (const candidate of candidates) {
        try {
            ensureExecutableBit(candidate);
        }
        catch {
            // best-effort hardening only
        }
    }
    if (!options.force) {
        nodePtySpawnHelperChecked = true;
    }
}
export function resolveDefaultTerminalShell(options = {}) {
    const platform = options.platform ?? process.platform;
    const env = options.env ?? process.env;
    if (platform === "win32") {
        return env.ComSpec || env.COMSPEC || "C:\\Windows\\System32\\cmd.exe";
    }
    return env.SHELL || "/bin/sh";
}
export function resolveZshShellIntegrationDir() {
    return fileURLToPath(new URL("./shell-integration/zsh", import.meta.url));
}
function resolveExternalProcessPath(filePath) {
    return filePath.replace(/\.asar(?=\/|$)/, ".asar.unpacked");
}
function resolveZshShellIntegrationRuntimeDir() {
    let username = "unknown";
    try {
        username = userInfo().username || username;
    }
    catch {
        // keep fallback
    }
    return join(tmpdir(), `${username}-paseo-zsh`);
}
function prepareZshShellIntegrationRuntimeDir(sourceDir = resolveZshShellIntegrationDir()) {
    const readableSourceDir = resolveExternalProcessPath(sourceDir);
    const runtimeDir = resolveZshShellIntegrationRuntimeDir();
    mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
    chmodSync(runtimeDir, 0o700);
    writePrivateFileAtomicSync(join(runtimeDir, ".zshenv"), readFileSync(join(readableSourceDir, ".zshenv")));
    writePrivateFileAtomicSync(join(runtimeDir, "paseo-integration.zsh"), readFileSync(join(readableSourceDir, "paseo-integration.zsh")));
    return runtimeDir;
}
export function buildTerminalEnvironment(input) {
    const baseEnv = createExternalProcessEnv(process.env, input.env, {
        TERM: "xterm-256color",
        TERM_PROGRAM: "kitty",
    });
    if (basename(input.shell) !== "zsh") {
        return baseEnv;
    }
    const originalZdotdir = baseEnv.ZDOTDIR ?? "";
    return {
        ...baseEnv,
        PASEO_ZSH_ZDOTDIR: originalZdotdir,
        ZDOTDIR: prepareZshShellIntegrationRuntimeDir(input.zshShellIntegrationDir),
    };
}
function extractCell(terminal, row, col) {
    const buffer = terminal.buffer.active;
    const line = buffer.getLine(row);
    if (!line) {
        return { char: " ", fg: undefined, bg: undefined };
    }
    const cell = line.getCell(col);
    if (!cell) {
        return { char: " ", fg: undefined, bg: undefined };
    }
    // Color modes from xterm.js: 0=DEFAULT, 1=16 colors (ANSI), 2=256 colors, 3=RGB
    // getFgColorMode() returns packed value with mode in upper byte (e.g. 0x01000000 for mode 1)
    const fgModeRaw = cell.getFgColorMode();
    const bgModeRaw = cell.getBgColorMode();
    const fgMode = fgModeRaw >> 24;
    const bgMode = bgModeRaw >> 24;
    // Only return color if not default (mode 0)
    const fg = fgMode !== 0 ? cell.getFgColor() : undefined;
    const bg = bgMode !== 0 ? cell.getBgColor() : undefined;
    return {
        char: cell.getChars() || " ",
        fg,
        bg,
        fgMode: fgMode !== 0 ? fgMode : undefined,
        bgMode: bgMode !== 0 ? bgMode : undefined,
        bold: cell.isBold() !== 0,
        italic: cell.isItalic() !== 0,
        underline: cell.isUnderline() !== 0,
        dim: cell.isDim() !== 0,
        inverse: cell.isInverse() !== 0,
        strikethrough: cell.isStrikethrough() !== 0,
    };
}
function extractGrid(terminal) {
    const grid = [];
    const buffer = terminal.buffer.active;
    // Visible viewport starts at baseY
    const baseY = buffer.baseY;
    for (let row = 0; row < terminal.rows; row++) {
        const rowCells = [];
        for (let col = 0; col < terminal.cols; col++) {
            rowCells.push(extractCell(terminal, baseY + row, col));
        }
        grid.push(rowCells);
    }
    return grid;
}
function extractScrollback(terminal, options) {
    const scrollback = [];
    const buffer = terminal.buffer.active;
    // baseY is the first row of the visible viewport (0-indexed)
    // Lines 0 to baseY-1 are in scrollback, lines baseY onwards are visible
    const scrollbackLines = buffer.baseY;
    const startRow = typeof options?.scrollbackLines === "number"
        ? Math.max(0, scrollbackLines - options.scrollbackLines)
        : 0;
    for (let row = startRow; row < scrollbackLines; row++) {
        const rowCells = [];
        const line = buffer.getLine(row);
        for (let col = 0; col < terminal.cols; col++) {
            if (line) {
                const cell = line.getCell(col);
                if (cell) {
                    const fgModeRaw = cell.getFgColorMode();
                    const bgModeRaw = cell.getBgColorMode();
                    const fgMode = fgModeRaw >> 24;
                    const bgMode = bgModeRaw >> 24;
                    const fg = fgMode !== 0 ? cell.getFgColor() : undefined;
                    const bg = bgMode !== 0 ? cell.getBgColor() : undefined;
                    rowCells.push({
                        char: cell.getChars() || " ",
                        fg,
                        bg,
                        fgMode: fgMode !== 0 ? fgMode : undefined,
                        bgMode: bgMode !== 0 ? bgMode : undefined,
                        bold: cell.isBold() !== 0,
                        italic: cell.isItalic() !== 0,
                        underline: cell.isUnderline() !== 0,
                        dim: cell.isDim() !== 0,
                        inverse: cell.isInverse() !== 0,
                        strikethrough: cell.isStrikethrough() !== 0,
                    });
                }
                else {
                    rowCells.push({ char: " ", fg: undefined, bg: undefined });
                }
            }
            else {
                rowCells.push({ char: " ", fg: undefined, bg: undefined });
            }
        }
        scrollback.push(rowCells);
    }
    return scrollback;
}
// xterm marks a line `isWrapped` when it is a continuation of the PREVIOUS line.
// The snapshot carries the inverse, tmux-style flag — "this row continues onto the
// next row" — so the client can rejoin and reflow logical lines. So row y's flag is
// whether line y+1 is a wrapped continuation.
function lineContinuesToNext(terminal, absoluteRow) {
    return terminal.buffer.active.getLine(absoluteRow + 1)?.isWrapped === true;
}
function extractGridWrapped(terminal) {
    const baseY = terminal.buffer.active.baseY;
    const wrapped = [];
    for (let row = 0; row < terminal.rows; row++) {
        wrapped.push(lineContinuesToNext(terminal, baseY + row));
    }
    return wrapped;
}
function extractScrollbackWrapped(terminal, options) {
    const scrollbackLines = terminal.buffer.active.baseY;
    const startRow = typeof options?.scrollbackLines === "number"
        ? Math.max(0, scrollbackLines - options.scrollbackLines)
        : 0;
    const wrapped = [];
    for (let row = startRow; row < scrollbackLines; row++) {
        wrapped.push(lineContinuesToNext(terminal, row));
    }
    return wrapped;
}
function extractCursorState(terminal) {
    const coreService = terminal
        ._core?.coreService;
    const cursorStyle = coreService?.decPrivateModes?.cursorStyle;
    const normalizedCursorStyle = cursorStyle === "block" || cursorStyle === "underline" || cursorStyle === "bar"
        ? cursorStyle
        : undefined;
    const cursorBlink = typeof coreService?.decPrivateModes?.cursorBlink === "boolean"
        ? coreService.decPrivateModes.cursorBlink
        : undefined;
    const hidden = Boolean(coreService?.isCursorHidden);
    return {
        row: terminal.buffer.active.cursorY,
        col: terminal.buffer.active.cursorX,
        ...(hidden ? { hidden: true } : {}),
        ...(normalizedCursorStyle ? { style: normalizedCursorStyle } : {}),
        ...(typeof cursorBlink === "boolean" ? { blink: cursorBlink } : {}),
    };
}
function normalizeProcessToken(token) {
    if (token.length === 0) {
        return token;
    }
    let quote;
    if (token.startsWith('"') && token.endsWith('"')) {
        quote = '"';
    }
    else if (token.startsWith("'") && token.endsWith("'")) {
        quote = "'";
    }
    else {
        quote = "";
    }
    const rawToken = quote ? token.slice(1, -1) : token;
    if (rawToken.length === 0) {
        return token;
    }
    const assignmentMatch = rawToken.match(/^([A-Za-z_][A-Za-z0-9_]*=)(.+)$/);
    const prefix = assignmentMatch ? assignmentMatch[1] : "";
    const value = assignmentMatch ? assignmentMatch[2] : rawToken;
    if (!value.includes("/")) {
        return token;
    }
    const normalized = `${prefix}${basename(value)}`;
    return quote ? `${quote}${normalized}${quote}` : normalized;
}
export function normalizeProcessTitle(processTitle) {
    const trimmed = processTitle.trim().replace(/\s+/g, " ");
    if (trimmed.length === 0) {
        return undefined;
    }
    const normalized = trimmed
        .split(" ")
        .map((token) => normalizeProcessToken(token))
        .join(" ")
        .trim();
    return normalized.length > 0 ? normalized : undefined;
}
const PROCESS_INTERPRETERS = new Set([
    "bash",
    "bun",
    "deno",
    "node",
    "nodejs",
    "python",
    "python3",
    "ruby",
    "sh",
    "tsx",
    "zsh",
]);
const PACKAGE_MANAGER_SCRIPT_NAMES = new Map([
    ["bun.js", "bun"],
    ["npm-cli.js", "npm"],
    ["npx-cli.js", "npx"],
    ["pnpm.cjs", "pnpm"],
    ["pnpm.js", "pnpm"],
    ["yarn.cjs", "yarn"],
    ["yarn.js", "yarn"],
]);
export function humanizeProcessTitle(processTitle) {
    const normalized = normalizeProcessTitle(processTitle);
    if (!normalized) {
        return undefined;
    }
    const tokens = normalized.split(" ").filter(Boolean);
    if (tokens.length === 0) {
        return undefined;
    }
    while (tokens[0] === "env") {
        tokens.shift();
        while (tokens[0] && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0])) {
            tokens.shift();
        }
    }
    if (tokens.length === 0) {
        return normalized;
    }
    const first = tokens[0];
    const second = tokens[1];
    if (PROCESS_INTERPRETERS.has(first) && second) {
        const packageManager = PACKAGE_MANAGER_SCRIPT_NAMES.get(second);
        if (packageManager) {
            return [packageManager, ...tokens.slice(2)].join(" ").trim() || packageManager;
        }
        if (!second.startsWith("-")) {
            return [second, ...tokens.slice(2)].join(" ").trim();
        }
    }
    return normalized;
}
function extractLastOutputLines(terminal, limit) {
    const buffer = terminal.buffer.active;
    const mergedLines = [];
    for (let row = 0; row < buffer.length; row++) {
        const line = buffer.getLine(row);
        if (!line) {
            continue;
        }
        const text = line.translateToString(true);
        const isWrapped = line.isWrapped === true;
        if (isWrapped && mergedLines.length > 0) {
            mergedLines[mergedLines.length - 1] += text;
            continue;
        }
        mergedLines.push(text);
    }
    while (mergedLines.length > 0 && mergedLines[0]?.trim().length === 0) {
        mergedLines.shift();
    }
    while (mergedLines.length > 0 && mergedLines[mergedLines.length - 1]?.trim().length === 0) {
        mergedLines.pop();
    }
    return mergedLines.slice(-limit);
}
const ESC = String.fromCharCode(0x1b);
const BEL = String.fromCharCode(0x07);
const ANSI_SEQUENCE_PATTERN = new RegExp(`${ESC}(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~]|\\].*?(?:${BEL}|${ESC}\\\\))`, "g");
function stripAnsiSequences(input) {
    return input.replace(ANSI_SEQUENCE_PATTERN, "");
}
function extractLastOutputLinesFromText(text, limit) {
    const normalized = stripAnsiSequences(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
    const lines = normalized.split("\n").map((line) => line.trimEnd());
    while (lines[0]?.trim().length === 0) {
        lines.shift();
    }
    while (lines[lines.length - 1]?.trim().length === 0) {
        lines.pop();
    }
    return lines.slice(-limit);
}
export async function createTerminal(options) {
    const { cwd, shell, env = {}, rows = 24, cols = 80, name = "Terminal", title: presetTitle, command, args = [], } = options;
    const resolvedShell = shell ?? resolveDefaultTerminalShell();
    const id = options.id ?? randomUUID();
    const listeners = new Set();
    const exitListeners = new Set();
    const commandFinishedListeners = new Set();
    const titleChangeListeners = new Set();
    let killed = false;
    let disposed = false;
    let exitEmitted = false;
    let processExited = false;
    const processExitWaiters = new Set();
    let exitInfo = null;
    let recentOutputText = "";
    let title;
    let titleMode = presetTitle?.trim() ? "manual" : "auto";
    let pendingTitle;
    let titleDebounceTimer = null;
    let pendingInput = "";
    let inputFlushImmediate = null;
    let stateRevision = 0;
    const inputModeTracker = new TerminalInputModeTracker();
    let titleChangeSubscription = null;
    // Create xterm.js headless terminal
    const terminal = new Terminal({
        rows,
        cols,
        scrollback: 1000,
        allowProposedApi: true,
    });
    ensureNodePtySpawnHelperExecutableForCurrentPlatform();
    // Create PTY
    const spawnCommand = command ?? resolvedShell;
    const spawnArgs = command ? args : [];
    const ptyProcess = pty.spawn(spawnCommand, spawnArgs, {
        name: "xterm-256color",
        cols,
        rows,
        cwd,
        env: buildTerminalEnvironment({ shell: spawnCommand, env }),
    });
    function emitTitleChange(nextTitle) {
        if (title === nextTitle) {
            return;
        }
        title = nextTitle;
        for (const listener of Array.from(titleChangeListeners)) {
            try {
                listener(title);
            }
            catch {
                // no-op
            }
        }
        for (const listener of Array.from(listeners)) {
            try {
                listener({ type: "titleChange", title });
            }
            catch {
                // no-op
            }
        }
    }
    function clearPendingTitleChange() {
        pendingTitle = undefined;
        if (titleDebounceTimer) {
            clearTimeout(titleDebounceTimer);
            titleDebounceTimer = null;
        }
    }
    function disposeTitleChangeSubscription() {
        titleChangeSubscription?.dispose();
        titleChangeSubscription = null;
    }
    function setTitle(nextTitle) {
        const manualTitle = nextTitle.trim();
        if (!manualTitle) {
            return;
        }
        titleMode = "manual";
        disposeTitleChangeSubscription();
        clearPendingTitleChange();
        emitTitleChange(manualTitle);
    }
    const initialManualTitle = presetTitle?.trim() || undefined;
    const processTitle = command ? [command, ...args].join(" ") : null;
    let initialTitle = initialManualTitle;
    if (!initialTitle && processTitle) {
        initialTitle = humanizeProcessTitle(processTitle) ?? normalizeProcessTitle(processTitle);
    }
    emitTitleChange(initialTitle);
    // Respond to DA1 queries (CSI c or CSI 0 c) — apps like nvim query terminal capabilities
    terminal.parser.registerCsiHandler({ final: "c" }, (params) => {
        if (params.length === 0 || (params.length === 1 && params[0] === 0)) {
            ptyProcess.write("\x1b[?62;4;22c");
            return true;
        }
        return false;
    });
    terminal.parser.registerCsiHandler({ final: "n" }, (params) => {
        if (params.length !== 1) {
            return false;
        }
        if (params[0] === 5) {
            ptyProcess.write("\x1b[0n");
            return true;
        }
        if (params[0] === 6) {
            const buffer = terminal.buffer.active;
            ptyProcess.write(`\x1b[${buffer.cursorY + 1};${buffer.cursorX + 1}R`);
            return true;
        }
        return false;
    });
    terminal.parser.registerCsiHandler({ prefix: "?", final: "n" }, (params) => {
        if (params.length !== 1 || params[0] !== 6) {
            return false;
        }
        const buffer = terminal.buffer.active;
        ptyProcess.write(`\x1b[?${buffer.cursorY + 1};${buffer.cursorX + 1}R`);
        return true;
    });
    for (const [code, response] of TERMINAL_OSC_COLOR_QUERY_RESPONSES) {
        terminal.parser.registerOscHandler(code, (data) => {
            if (data.trim() !== "?") {
                return false;
            }
            ptyProcess.write(`\x1b]${code};${response}\x1b\\`);
            return true;
        });
    }
    if (titleMode === "auto") {
        titleChangeSubscription = terminal.onTitleChange((nextTitle) => {
            if (disposed || killed) {
                return;
            }
            pendingTitle = nextTitle.trim().length > 0 ? nextTitle : undefined;
            if (titleDebounceTimer) {
                clearTimeout(titleDebounceTimer);
            }
            titleDebounceTimer = setTimeout(() => {
                titleDebounceTimer = null;
                emitTitleChange(pendingTitle);
                pendingTitle = undefined;
            }, TERMINAL_TITLE_DEBOUNCE_MS);
        });
    }
    const disposeCommandLifecycleSubscription = terminal.parser.registerOscHandler(633, (data) => {
        const commandFinished = parseCommandFinishedOsc(data);
        if (!commandFinished) {
            return true;
        }
        for (const listener of Array.from(commandFinishedListeners)) {
            try {
                listener(commandFinished);
            }
            catch {
                // no-op
            }
        }
        return true;
    });
    function buildExitInfo(input) {
        const lastOutputLines = extractLastOutputLines(terminal, TERMINAL_EXIT_OUTPUT_LINE_LIMIT);
        return {
            exitCode: input?.exitCode ?? null,
            signal: input?.signal && input.signal > 0 ? input.signal : null,
            lastOutputLines: lastOutputLines.length > 0
                ? lastOutputLines
                : extractLastOutputLinesFromText(recentOutputText, TERMINAL_EXIT_OUTPUT_LINE_LIMIT),
        };
    }
    function emitExit(info) {
        if (exitEmitted) {
            return;
        }
        exitEmitted = true;
        exitInfo = info;
        for (const listener of Array.from(exitListeners)) {
            try {
                listener(info);
            }
            catch {
                // no-op
            }
        }
        exitListeners.clear();
    }
    function disposeResources() {
        if (disposed) {
            return;
        }
        disposed = true;
        pendingInput = "";
        inputModeTracker.reset();
        if (inputFlushImmediate) {
            clearImmediate(inputFlushImmediate);
            inputFlushImmediate = null;
        }
        clearPendingTitleChange();
        disposeTitleChangeSubscription();
        disposeCommandLifecycleSubscription.dispose();
        terminal.dispose();
        listeners.clear();
        exitListeners.clear();
        commandFinishedListeners.clear();
        titleChangeListeners.clear();
    }
    function writeOutputToHeadless(data) {
        terminal.write(data, () => {
            if (disposed || killed) {
                return;
            }
            stateRevision += 1;
            for (const listener of listeners) {
                listener({ type: "output", data, revision: stateRevision });
            }
        });
    }
    // Pipe PTY output to terminal emulator
    ptyProcess.onData((data) => {
        if (killed)
            return;
        const inputModeUpdate = inputModeTracker.feed(data);
        for (const response of inputModeUpdate.responses) {
            ptyProcess.write(response);
        }
        recentOutputText = `${recentOutputText}${data}`;
        if (recentOutputText.length > TERMINAL_EXIT_OUTPUT_CHAR_LIMIT) {
            recentOutputText = recentOutputText.slice(-TERMINAL_EXIT_OUTPUT_CHAR_LIMIT);
        }
        writeOutputToHeadless(data);
    });
    ptyProcess.onExit((event) => {
        killed = true;
        processExited = true;
        for (const waiter of Array.from(processExitWaiters)) {
            try {
                waiter();
            }
            catch {
                // no-op
            }
        }
        processExitWaiters.clear();
        emitExit(buildExitInfo({
            exitCode: event.exitCode,
            signal: event.signal,
        }));
        disposeResources();
    });
    async function waitForPtyProcessStart() {
        if (process.platform !== "win32") {
            return;
        }
        const started = () => {
            const windowsPtyProcess = ptyProcess;
            return ptyProcess.pid > 0 || (windowsPtyProcess._agent?.innerPid ?? 0) > 0 || processExited;
        };
        const deadline = Date.now() + 5000;
        while (!started() && Date.now() < deadline) {
            await new Promise((resolve) => setTimeout(resolve, 10));
        }
    }
    function getState(snapshotOptions) {
        return {
            rows: terminal.rows,
            cols: terminal.cols,
            grid: extractGrid(terminal),
            scrollback: extractScrollback(terminal, {
                scrollbackLines: snapshotOptions?.scrollbackLines,
            }),
            cursor: extractCursorState(terminal),
            ...(title ? { title } : {}),
            ...(snapshotOptions?.includeWrapFlags
                ? {
                    gridWrapped: extractGridWrapped(terminal),
                    scrollbackWrapped: extractScrollbackWrapped(terminal, {
                        scrollbackLines: snapshotOptions?.scrollbackLines,
                    }),
                }
                : {}),
        };
    }
    function getStateSnapshot(snapshotOptions) {
        return {
            state: getState(snapshotOptions),
            revision: stateRevision,
        };
    }
    function getSize() {
        return {
            rows: terminal.rows,
            cols: terminal.cols,
        };
    }
    function getReplayPreamble() {
        return inputModeTracker.getPreamble();
    }
    function writeInputToPty(data) {
        ptyProcess.write(data);
    }
    function flushPendingInput() {
        if (inputFlushImmediate) {
            clearImmediate(inputFlushImmediate);
            inputFlushImmediate = null;
        }
        const data = pendingInput;
        pendingInput = "";
        if (!data || killed || disposed) {
            return;
        }
        writeInputToPty(data);
    }
    function scheduleInputFlush() {
        if (inputFlushImmediate) {
            return;
        }
        inputFlushImmediate = setImmediate(() => {
            inputFlushImmediate = null;
            flushPendingInput();
        });
    }
    function send(msg) {
        if (killed)
            return;
        switch (msg.type) {
            case "input": {
                pendingInput += msg.data;
                scheduleInputFlush();
                break;
            }
            case "resize":
                flushPendingInput();
                terminal.resize(msg.cols, msg.rows);
                ptyProcess.resize(msg.cols, msg.rows);
                stateRevision += 1;
                break;
            case "mouse":
                // Mouse events can be sent as escape sequences if terminal supports it
                // For now, we'll just ignore them - can be implemented later
                break;
        }
    }
    function subscribe(listener, subscribeOptions) {
        let active = true;
        let snapshotDelivered = false;
        const queuedMessages = [];
        const initialSnapshot = subscribeOptions?.initialSnapshot ?? "state";
        const subscriptionListener = (msg) => {
            if (!active) {
                return;
            }
            if (!snapshotDelivered) {
                queuedMessages.push(msg);
                return;
            }
            listener(msg);
        };
        listeners.add(subscriptionListener);
        terminal.write("", () => {
            if (!disposed && active && listeners.has(subscriptionListener)) {
                snapshotDelivered = true;
                if (initialSnapshot === "ready") {
                    listener({ type: "snapshotReady", revision: stateRevision });
                }
                else {
                    listener({ type: "snapshot", ...getStateSnapshot() });
                }
                for (const message of queuedMessages.splice(0)) {
                    listener(message);
                }
            }
        });
        return () => {
            active = false;
            queuedMessages.length = 0;
            listeners.delete(subscriptionListener);
        };
    }
    function onExit(listener) {
        if (killed) {
            queueMicrotask(() => {
                try {
                    listener(exitInfo ?? buildExitInfo());
                }
                catch {
                    // no-op
                }
            });
            return () => { };
        }
        exitListeners.add(listener);
        return () => {
            exitListeners.delete(listener);
        };
    }
    function onCommandFinished(listener) {
        commandFinishedListeners.add(listener);
        return () => {
            commandFinishedListeners.delete(listener);
        };
    }
    function onTitleChange(listener) {
        titleChangeListeners.add(listener);
        if (title !== undefined) {
            queueMicrotask(() => {
                if (disposed || !titleChangeListeners.has(listener)) {
                    return;
                }
                try {
                    listener(title);
                }
                catch {
                    // no-op
                }
            });
        }
        return () => {
            titleChangeListeners.delete(listener);
        };
    }
    function getTitle() {
        return title;
    }
    function getExitInfo() {
        return exitInfo;
    }
    function kill() {
        if (!killed) {
            killed = true;
            if (!processExited) {
                killPtyProcess();
            }
            emitExit(buildExitInfo());
        }
        if (processExited) {
            disposeResources();
            return;
        }
        void waitForProcessExit(1000).finally(disposeResources);
    }
    function killPtyProcess(signal) {
        if (process.platform === "win32") {
            ptyProcess.kill();
            return;
        }
        ptyProcess.kill(signal);
    }
    function waitForProcessExit(timeoutMs) {
        if (processExited) {
            return Promise.resolve(true);
        }
        return new Promise((resolve) => {
            let pendingResolve = resolve;
            const settle = (value) => {
                if (!pendingResolve)
                    return;
                const fn = pendingResolve;
                pendingResolve = null;
                fn(value);
            };
            const waiter = () => {
                clearTimeout(timer);
                settle(true);
            };
            const timer = setTimeout(() => {
                processExitWaiters.delete(waiter);
                settle(false);
            }, timeoutMs);
            processExitWaiters.add(waiter);
        });
    }
    async function killAndWait(killOptions) {
        const gracefulTimeoutMs = killOptions?.gracefulTimeoutMs ?? 2000;
        const forceTimeoutMs = killOptions?.forceTimeoutMs ?? 1000;
        if (processExited) {
            kill();
            return;
        }
        try {
            killPtyProcess();
        }
        catch {
            // process may already be gone
        }
        const exitedGracefully = await waitForProcessExit(gracefulTimeoutMs);
        if (!exitedGracefully) {
            try {
                killPtyProcess("SIGKILL");
            }
            catch {
                // process may already be gone
            }
            await waitForProcessExit(forceTimeoutMs);
        }
        // Finalize bookkeeping (idempotent if ptyProcess.onExit already fired).
        kill();
    }
    await waitForPtyProcessStart();
    // Small delay to let shell initialize
    await new Promise((resolve) => setTimeout(resolve, 50));
    return {
        id,
        name,
        cwd,
        send,
        subscribe,
        onExit,
        onCommandFinished,
        onTitleChange,
        getSize,
        getState,
        getStateSnapshot,
        getReplayPreamble,
        getTitle,
        setTitle,
        getExitInfo,
        kill,
        killAndWait,
    };
}
//# sourceMappingURL=terminal.js.map