import { createInterface } from "node:readline";
export function isTTY() {
    return !!(process.stdin.isTTY && process.stdout.isTTY);
}
/**
 * Strip ANSI CSI escape sequences (e.g. arrow keys) from a line.
 * Returns the remaining content trimmed.
 */
function stripEscapeSequences(line) {
    return line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").trim();
}
/** Strip all ANSI escape sequences for visible-width measurement */
function stripAnsi(s) {
    return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\x1b\]8;;[^\x07]*\x07/g, "");
}
/** Truncate plain text to maxLen, adding ellipsis if needed */
function truncateText(s, maxLen) {
    if (s.length <= maxLen)
        return s;
    if (maxLen <= 1)
        return "\u2026";
    return s.slice(0, maxLen - 1) + "\u2026";
}
/**
 * Returns true if the line consists entirely of ANSI escape sequences
 * (no real typed content). Used to detect stray arrow-key input.
 */
export function isEscapeSequence(line) {
    return line.length > 0 && stripEscapeSequences(line).length === 0;
}
/**
 * Parse a toggle input string into 0-based indices.
 *
 * Supported formats (1-based user input):
 *   "3"       → [2]
 *   "1-3"     → [0, 1, 2]
 *   "1,3,5"   → [0, 2, 4]
 *   "1-3,5"   → [0, 1, 2, 4]
 *
 * Invalid ranges (start > end) or out-of-bounds entries are silently ignored.
 * Returns a sorted, deduplicated array of 0-based indices.
 */
export function parseToggleInput(input, maxIndex) {
    const indices = new Set();
    const tokens = input.split(",");
    for (const token of tokens) {
        const trimmed = token.trim();
        const dashIdx = trimmed.indexOf("-");
        if (dashIdx > 0) {
            // Range: e.g. "1-3"
            const start = parseInt(trimmed.slice(0, dashIdx), 10);
            const end = parseInt(trimmed.slice(dashIdx + 1), 10);
            if (isNaN(start) || isNaN(end) || start > end)
                continue;
            for (let i = start; i <= end; i++) {
                const idx = i - 1;
                if (idx >= 0 && idx < maxIndex)
                    indices.add(idx);
            }
        }
        else {
            // Single number
            const num = parseInt(trimmed, 10);
            if (isNaN(num))
                continue;
            const idx = num - 1;
            if (idx >= 0 && idx < maxIndex)
                indices.add(idx);
        }
    }
    return Array.from(indices).sort((a, b) => a - b);
}
// ---------------------------------------------------------------------------
// ANSI helpers
// ---------------------------------------------------------------------------
const ESC = "\x1b[";
const HIDE_CURSOR = `${ESC}?25l`;
const SHOW_CURSOR = `${ESC}?25h`;
const CLEAR_LINE = `${ESC}2K`;
const CLEAR_BELOW = `${ESC}J`;
function moveUp(n) {
    return n > 0 ? `${ESC}${n}A` : "";
}
/**
 * Count how many physical terminal rows a string occupies,
 * accounting for line wrapping at the given column width.
 */
function physicalRows(line, cols) {
    const visible = stripAnsi(line).length;
    if (visible === 0)
        return 1;
    return Math.ceil(visible / cols);
}
// ---------------------------------------------------------------------------
// Raw mode keypress reader
// ---------------------------------------------------------------------------
const KEY_UP = "\x1b[A";
const KEY_DOWN = "\x1b[B";
const KEY_SPACE = " ";
const KEY_ENTER_CR = "\r";
const KEY_ENTER_LF = "\n";
const KEY_CTRL_C = "\x03";
/**
 * Queue-based keypress reader that prevents race conditions.
 * A persistent listener buffers incoming data, so no keystrokes
 * are lost between reads (e.g. during render cycles).
 */
const keyQueue = [];
const keyWaiters = [];
let keyListenerAttached = false;
function ensureKeyListener() {
    if (keyListenerAttached)
        return;
    keyListenerAttached = true;
    process.stdin.on("data", (data) => {
        const key = data.toString();
        const waiter = keyWaiters.shift();
        if (waiter) {
            waiter(key);
        }
        else {
            keyQueue.push(key);
        }
    });
}
function detachKeyListener() {
    if (!keyListenerAttached)
        return;
    process.stdin.removeAllListeners("data");
    keyListenerAttached = false;
    keyQueue.length = 0;
    keyWaiters.length = 0;
}
/**
 * Read a single keypress from stdin in raw mode.
 * Returns the raw key string (may be multi-byte for arrow keys).
 */
function readKey() {
    ensureKeyListener();
    const buffered = keyQueue.shift();
    if (buffered !== undefined) {
        return Promise.resolve(buffered);
    }
    return new Promise((resolve) => {
        keyWaiters.push(resolve);
    });
}
// ---------------------------------------------------------------------------
// Interactive prompter (raw mode, arrow keys, real TTY)
// ---------------------------------------------------------------------------
function createInteractivePrompter() {
    const out = process.stdout;
    function write(s) {
        out.write(s);
    }
    return {
        async promptCheckboxList(items, options) {
            const checked = items.map((item) => item.checked ?? false);
            let cursor = 0;
            let scrollOffset = 0;
            let renderedLines = 0;
            // Scrolling viewport: fit within terminal, leave room for header/footer
            const termRows = out.rows || 24;
            const maxVisible = Math.min(items.length, Math.max(8, termRows - 6));
            function selectedCount() {
                return checked.filter(Boolean).length;
            }
            function render() {
                const termCols = out.columns || 80;
                // Move up to overwrite previous render, then wipe everything below
                // to prevent ghost lines from wrapped or longer previous renders
                if (renderedLines > 0) {
                    write(moveUp(renderedLines) + "\r" + CLEAR_BELOW);
                }
                // Keep cursor within viewport
                if (cursor < scrollOffset)
                    scrollOffset = cursor;
                if (cursor >= scrollOffset + maxVisible)
                    scrollOffset = cursor - maxVisible + 1;
                const lines = [];
                // Scroll indicator: above
                if (scrollOffset > 0) {
                    lines.push(`${CLEAR_LINE}  \x1b[2m  \u2191 ${scrollOffset} more above\x1b[0m`);
                }
                // Visible window
                const end = Math.min(scrollOffset + maxVisible, items.length);
                for (let i = scrollOffset; i < end; i++) {
                    const isFocused = i === cursor;
                    const isChecked = checked[i];
                    const dot = isChecked ? "\x1b[32m\u25cf\x1b[39m" : "\x1b[2m\u25cb\x1b[22m";
                    const pointer = isFocused ? "\x1b[36m>\x1b[39m" : " ";
                    const labelText = isFocused ? `\x1b[1m${items[i].label}\x1b[22m` : items[i].label;
                    const prefix = `  ${pointer} ${dot} ${labelText}`;
                    const prefixWidth = stripAnsi(prefix).length;
                    let desc = "";
                    if (items[i].description) {
                        const available = termCols - prefixWidth - 3; // 3 = " — "
                        if (available > 10) {
                            desc = ` \x1b[2m\u2014 ${truncateText(items[i].description, available)}\x1b[0m`;
                        }
                    }
                    lines.push(`${CLEAR_LINE}${prefix}${desc}`);
                }
                // Scroll indicator: below
                const remaining = items.length - end;
                if (remaining > 0) {
                    lines.push(`${CLEAR_LINE}  \x1b[2m  \u2193 ${remaining} more below\x1b[0m`);
                }
                // Footer
                lines.push(`${CLEAR_LINE}  \x1b[2m${selectedCount()}/${items.length} selected \u00b7 \u2191/\u2193 move \u00b7 space toggle \u00b7 a all \u00b7 enter done\x1b[0m`);
                write(lines.join("\n") + "\n");
                // Track physical rows (not logical lines) for accurate cursor repositioning
                let rows = 0;
                for (const line of lines) {
                    rows += physicalRows(line, termCols);
                }
                renderedLines = rows;
            }
            // Print title
            if (options?.title) {
                write(`\n${options.title}\n`);
            }
            write(HIDE_CURSOR);
            process.stdin.setRawMode(true);
            process.stdin.resume();
            render();
            try {
                while (true) {
                    const key = await readKey();
                    if (key === KEY_UP) {
                        cursor = cursor > 0 ? cursor - 1 : items.length - 1;
                        // Wrap to bottom: scroll to end
                        if (cursor === items.length - 1)
                            scrollOffset = Math.max(0, items.length - maxVisible);
                        render();
                    }
                    else if (key === KEY_DOWN) {
                        cursor = cursor < items.length - 1 ? cursor + 1 : 0;
                        // Wrap to top: scroll to start
                        if (cursor === 0)
                            scrollOffset = 0;
                        render();
                    }
                    else if (key === KEY_SPACE) {
                        checked[cursor] = !checked[cursor];
                        render();
                    }
                    else if (key.toLowerCase() === "a") {
                        const allChecked = checked.every(Boolean);
                        checked.fill(!allChecked);
                        render();
                    }
                    else if (key === KEY_ENTER_CR || key === KEY_ENTER_LF) {
                        break;
                    }
                    else if (key === KEY_CTRL_C) {
                        write(SHOW_CURSOR + "\n");
                        process.stdin.setRawMode(false);
                        process.exit(0);
                    }
                }
            }
            finally {
                detachKeyListener();
                process.stdin.setRawMode(false);
                process.stdin.pause();
                write(SHOW_CURSOR);
            }
            return checked.reduce((acc, val, idx) => {
                if (val)
                    acc.push(idx);
                return acc;
            }, []);
        },
        async promptChoice(question, choices) {
            let cursor = 0;
            let renderedLines = 0;
            function render() {
                const termCols = out.columns || 80;
                if (renderedLines > 0) {
                    write(moveUp(renderedLines) + "\r" + CLEAR_BELOW);
                }
                const lines = [];
                for (let i = 0; i < choices.length; i++) {
                    const isFocused = i === cursor;
                    const pointer = isFocused ? "\x1b[36m>\x1b[39m" : " ";
                    const dot = isFocused ? "\x1b[36m\u25cf\x1b[39m" : "\x1b[2m\u25cb\x1b[22m";
                    const label = isFocused ? `\x1b[1m${choices[i].label}\x1b[22m` : choices[i].label;
                    const hint = choices[i].hint ? ` \x1b[2m(${choices[i].hint})\x1b[0m` : "";
                    lines.push(`${CLEAR_LINE}  ${pointer} ${dot} ${label}${hint}`);
                }
                lines.push(`${CLEAR_LINE}  \x1b[2m\u2191/\u2193 move \u00b7 enter select\x1b[0m`);
                write(lines.join("\n") + "\n");
                let rows = 0;
                for (const line of lines) {
                    rows += physicalRows(line, termCols);
                }
                renderedLines = rows;
            }
            write(`\n${question}\n`);
            write(HIDE_CURSOR);
            process.stdin.setRawMode(true);
            process.stdin.resume();
            render();
            try {
                while (true) {
                    const key = await readKey();
                    if (key === KEY_UP) {
                        cursor = cursor > 0 ? cursor - 1 : choices.length - 1;
                        render();
                    }
                    else if (key === KEY_DOWN) {
                        cursor = cursor < choices.length - 1 ? cursor + 1 : 0;
                        render();
                    }
                    else if (key === KEY_ENTER_CR || key === KEY_ENTER_LF) {
                        break;
                    }
                    else if (key === KEY_CTRL_C) {
                        write(SHOW_CURSOR + "\n");
                        process.stdin.setRawMode(false);
                        process.exit(0);
                    }
                }
            }
            finally {
                detachKeyListener();
                process.stdin.setRawMode(false);
                process.stdin.pause();
                write(SHOW_CURSOR);
            }
            return cursor;
        },
        async promptConfirm(question, defaultYes) {
            const hint = defaultYes ? "Y/n" : "y/N";
            write(`${question} (${hint}): `);
            process.stdin.setRawMode(true);
            process.stdin.resume();
            try {
                while (true) {
                    const key = await readKey();
                    if (key === KEY_ENTER_CR || key === KEY_ENTER_LF) {
                        write("\n");
                        return !!defaultYes;
                    }
                    if (key.toLowerCase() === "y") {
                        write("y\n");
                        return true;
                    }
                    if (key.toLowerCase() === "n") {
                        write("n\n");
                        return false;
                    }
                    if (key === KEY_CTRL_C) {
                        write("\n");
                        process.stdin.setRawMode(false);
                        process.exit(0);
                    }
                }
            }
            finally {
                detachKeyListener();
                process.stdin.setRawMode(false);
                process.stdin.pause();
            }
        },
    };
}
// ---------------------------------------------------------------------------
// Text-based prompter (fallback for non-TTY / custom streams / tests)
// ---------------------------------------------------------------------------
function createTextPrompter(input, output) {
    const rl = createInterface({ input, output, terminal: false });
    const lineQueue = [];
    const waiters = [];
    rl.on("line", (line) => {
        const waiter = waiters.shift();
        if (waiter) {
            waiter(line.trim());
        }
        else {
            lineQueue.push(line.trim());
        }
    });
    function ask(prompt) {
        output.write(prompt);
        return new Promise((resolve) => {
            const buffered = lineQueue.shift();
            if (buffered !== undefined) {
                resolve(buffered);
            }
            else {
                waiters.push(resolve);
            }
        });
    }
    function writeLine(text) {
        output.write(text + "\n");
    }
    function renderCheckbox(items, checked) {
        for (let i = 0; i < items.length; i++) {
            const mark = checked[i] ? "x" : " ";
            const desc = items[i].description ? ` - ${items[i].description}` : "";
            writeLine(`  ${i + 1}) [${mark}] ${items[i].label}${desc}`);
        }
        writeLine(`  a) Toggle all`);
        writeLine(`  Type: number, range (1-3), list (1,3,5). Enter=done`);
    }
    return {
        async promptCheckboxList(items, options) {
            const checked = items.map((item) => item.checked ?? false);
            if (options?.title) {
                writeLine(`\n${options.title}`);
            }
            renderCheckbox(items, checked);
            while (true) {
                const raw = await ask("> ");
                const line = stripEscapeSequences(raw);
                if (raw !== "" && line === "")
                    continue;
                if (line === "")
                    break;
                if (line.toLowerCase() === "a") {
                    const allChecked = checked.every(Boolean);
                    for (let i = 0; i < checked.length; i++) {
                        checked[i] = !allChecked;
                    }
                    renderCheckbox(items, checked);
                    continue;
                }
                const indices = parseToggleInput(line, items.length);
                if (indices.length > 0) {
                    for (const idx of indices) {
                        checked[idx] = !checked[idx];
                    }
                    renderCheckbox(items, checked);
                }
            }
            rl.close();
            return checked.reduce((acc, val, idx) => {
                if (val)
                    acc.push(idx);
                return acc;
            }, []);
        },
        async promptChoice(question, choices) {
            writeLine(`\n${question}`);
            for (let i = 0; i < choices.length; i++) {
                const hint = choices[i].hint ? ` (${choices[i].hint})` : "";
                writeLine(`  ${i + 1}) ${choices[i].label}${hint}`);
            }
            while (true) {
                const raw = await ask("> ");
                const line = stripEscapeSequences(raw);
                if (raw !== "" && line === "")
                    continue;
                const num = parseInt(line, 10);
                if (num >= 1 && num <= choices.length) {
                    rl.close();
                    return num - 1;
                }
            }
        },
        async promptConfirm(question, defaultYes) {
            const hint = defaultYes ? "Y/n" : "y/N";
            const line = await ask(`${question} (${hint}): `);
            rl.close();
            if (line === "")
                return !!defaultYes;
            return line.toLowerCase() === "y";
        },
    };
}
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
/**
 * Create a Prompter instance.
 *
 * - When called without arguments on a real TTY → interactive raw mode
 *   (arrow keys, space toggle, proper cursor movement)
 * - When called with custom streams or on non-TTY → text-based fallback
 *   (type numbers, compatible with piped input and tests)
 */
export function createPrompter(input, output) {
    const canUseInteractive = !input && !output && isTTY() && typeof process.stdin.setRawMode === "function";
    if (canUseInteractive) {
        return createInteractivePrompter();
    }
    return createTextPrompter(input ?? process.stdin, output ?? process.stdout);
}
//# sourceMappingURL=prompts.js.map