/**
 * Steps UI Component v2 - Clean state-driven implementation
 *
 * Key principle: Render entire step list from state on every update cycle.
 * Track total lines rendered to calculate cursor movement.
 */
import { ValidationInputError, ValidationOutputError } from '@agentuity/server';
import { clearLastLines, isTTYLike } from './tui';
import { appendLog, isLogCollectionEnabled } from './log-collector';
import { getOutputOptions, isJSONMode } from './output';
/**
 * Error thrown when step execution is interrupted by a signal (e.g., Ctrl+C).
 * Callers can catch this to perform cleanup before exiting.
 */
export class StepInterruptError extends Error {
    exitCode;
    constructor(exitCode = 130) {
        super('Step execution interrupted');
        this.name = 'StepInterruptError';
        this.exitCode = exitCode;
    }
}
// Spinner frames
const FRAMES = ['◐', '◓', '◑', '◒'];
// Icons
const ICONS = {
    success: '✓',
    skipped: '○',
    error: '✗',
    pending: '☐',
};
// Color definitions
const COLORS = {
    cyan: { light: '\x1b[36m', dark: '\x1b[96m' },
    blue: { light: '\x1b[34m', dark: '\x1b[94m' },
    magenta: { light: '\x1b[35m', dark: '\x1b[95m' },
    green: { light: '\x1b[32m', dark: '\x1b[92m' },
    yellow: { light: '\x1b[33m', dark: '\x1b[93m' },
    red: { light: '\x1b[31m', dark: '\x1b[91m' },
    gray: { light: '\x1b[90m', dark: '\x1b[37m' },
    bold: '\x1b[1m',
    strikethrough: '\x1b[9m',
    reset: '\x1b[0m',
};
const SPINNER_COLORS = ['cyan', 'blue', 'magenta', 'cyan'];
const currentColorScheme = process.env.CI ? 'light' : 'dark';
function getColor(colorKey) {
    const color = COLORS[colorKey];
    if (typeof color === 'string') {
        return color;
    }
    return color[currentColorScheme];
}
/**
 * Helper functions for creating step outcomes
 */
export const stepSuccess = (output) => ({ status: 'success', output });
export const stepSkipped = (reason, output) => ({
    status: 'skipped',
    reason,
    output,
});
export const stepError = (message, cause, output) => ({
    status: 'error',
    message,
    cause,
    output,
});
/**
 * Render a single step line (without output box)
 */
function renderStepLine(step, spinner) {
    const grayColor = getColor('gray');
    const greenColor = getColor('green');
    const yellowColor = getColor('yellow');
    const redColor = getColor('red');
    const cyanColor = getColor('cyan');
    if (step.status === 'success') {
        return `${greenColor}${ICONS.success}${COLORS.reset} ${grayColor}${COLORS.strikethrough}${step.label}${COLORS.reset}`;
    }
    else if (step.status === 'skipped') {
        const reason = step.skipReason ? ` ${grayColor}(${step.skipReason})${COLORS.reset}` : '';
        return `${yellowColor}${ICONS.skipped}${COLORS.reset} ${grayColor}${COLORS.strikethrough}${step.label}${COLORS.reset}${reason}`;
    }
    else if (step.status === 'error') {
        return `${redColor}${ICONS.error}${COLORS.reset} ${step.label}`;
    }
    else if (step.status === 'running' && spinner) {
        const progressIndicator = step.progress !== undefined
            ? ` ${cyanColor}${Math.floor(step.progress)}%${COLORS.reset}`
            : '';
        return `${spinner} ${step.label}${progressIndicator}`;
    }
    else {
        return `${grayColor}${ICONS.pending}${COLORS.reset} ${step.label}`;
    }
}
/**
 * Generate a clean log line for a completed step (no ANSI, no animation)
 */
function getCleanStepLog(step) {
    if (step.status === 'success') {
        return `${ICONS.success} ${step.label}`;
    }
    else if (step.status === 'skipped') {
        const reason = step.skipReason ? ` (${step.skipReason})` : '';
        return `${ICONS.skipped} ${step.label}${reason}`;
    }
    else if (step.status === 'error') {
        const error = step.errorMessage ? `: ${step.errorMessage}` : '';
        return `${ICONS.error} ${step.label}${error}`;
    }
    return `${ICONS.pending} ${step.label}`;
}
/**
 * Emit clean log for a completed step if log collection is enabled
 */
function emitCleanStepLog(step) {
    if (!isLogCollectionEnabled())
        return;
    appendLog(getCleanStepLog(step));
    // Also emit any output lines
    if (step.output && step.output.length > 0) {
        for (const line of step.output) {
            appendLog(`  ${line}`);
        }
    }
}
/**
 * Render all steps from state, including output boxes
 * Returns the rendered output and total line count
 */
function renderAllSteps(state, runningStepIndex, spinner) {
    const lines = [];
    let totalLines = 0;
    const grayColor = getColor('gray');
    for (let i = 0; i < state.length; i++) {
        const step = state[i];
        if (step === undefined)
            continue;
        const isRunning = i === runningStepIndex;
        const stepSpinner = isRunning && spinner ? spinner : undefined;
        // Render step line
        lines.push(renderStepLine(step, stepSpinner));
        totalLines++;
        // Render output box if present
        if (step.output && step.output.length > 0) {
            lines.push(`${grayColor}╭─ Output${COLORS.reset}`);
            totalLines++;
            for (const line of step.output) {
                lines.push(`${grayColor}│${COLORS.reset} ${line}`);
                totalLines++;
            }
            lines.push(`${grayColor}╰─${COLORS.reset}`);
            totalLines++;
            // Don't add blank line here - the '\n' we append during write creates separation
        }
    }
    return { output: lines.join('\n'), totalLines };
}
/**
 * Print validation issues (for ValidationInputError/ValidationOutputError)
 */
function printValidationIssues(issues) {
    const errorColor = getColor('red');
    console.error(`${errorColor}Validation details:${COLORS.reset}`);
    if (issues) {
        for (const issue of issues) {
            const path = issue.path.length > 0 ? issue.path.join('.') : '(root)';
            console.error(`  ${path}: ${issue.message}`);
        }
    }
}
/**
 * Global pause state and tracking
 */
let isPaused = false;
let getTotalLinesFn = null;
let forceRerenderFn = null;
export function isStepUIPaused() {
    return isPaused;
}
/**
 * Internal function to set pause capability
 */
function enablePauseResume(getTotalLines, forceRerender) {
    getTotalLinesFn = getTotalLines;
    forceRerenderFn = forceRerender;
}
/**
 * Pause step rendering for interactive input
 * Returns resume function
 */
export function pauseStepUI(clear = false) {
    if (!isTTYLike() || !getTotalLinesFn) {
        return () => { }; // No-op if not TTY or not in step context
    }
    isPaused = true;
    const originalStdoutWrite = process.stdout.write.bind(process.stdout);
    const originalStderrWrite = process.stderr.write.bind(process.stderr);
    // Intercept writes during pause (unused but prevents issues with interactive prompts)
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    process.stdout.write = ((chunk, ..._args) => {
        return originalStdoutWrite(chunk);
    });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    process.stderr.write = ((chunk, ..._args) => {
        return originalStderrWrite(chunk);
    });
    // Show cursor and add newline for separation
    if (clear) {
        const lines = getTotalLinesFn();
        if (lines) {
            clearLastLines(lines + 1, originalStdoutWrite);
        }
    }
    originalStdoutWrite('\x1B[?25h');
    if (!clear) {
        originalStdoutWrite('\n');
    }
    // Return resume function
    return () => {
        isPaused = false;
        // Restore original write functions
        process.stdout.write = originalStdoutWrite;
        process.stderr.write = originalStderrWrite;
        // Restore cursor to saved position (where steps began)
        if (!clear) {
            process.stdout.write('\x1B[u'); // Restore cursor position
            process.stdout.write('\x1B[0J'); // Clear from saved position to end of screen
        }
        process.stdout.write('\x1B[?25l'); // Hide cursor
        // Force immediate re-render (cursor already at step start)
        if (forceRerenderFn) {
            forceRerenderFn(true);
        }
    };
}
/**
 * Install interrupt handlers (SIGINT/SIGTERM + raw mode)
 */
function installInterruptHandlers(onInterrupt) {
    const cleanupFns = [];
    const sigHandler = () => onInterrupt();
    process.on('SIGINT', sigHandler);
    process.on('SIGTERM', sigHandler);
    cleanupFns.push(() => {
        process.off('SIGINT', sigHandler);
        process.off('SIGTERM', sigHandler);
    });
    // TTY raw mode fallback
    const stdin = process.stdin;
    if (stdin && stdin.isTTY) {
        const onData = (buf) => {
            if (buf.length === 1 && buf[0] === 0x03)
                onInterrupt();
        };
        try {
            stdin.setRawMode?.(true);
        }
        catch {
            // Ignore errors
        }
        stdin.resume?.();
        stdin.on('data', onData);
        cleanupFns.push(() => {
            stdin.off?.('data', onData);
            stdin.pause?.();
            try {
                stdin.setRawMode?.(false);
            }
            catch {
                // Ignore errors
            }
        });
    }
    return () => {
        for (const fn of cleanupFns.splice(0))
            fn();
    };
}
/**
 * Run steps with TUI (animated mode)
 */
async function runStepsTUI(steps) {
    // Initialize state
    const state = steps.map((s) => ({
        label: s.label,
        status: 'pending',
    }));
    let totalLinesFromLastRender = 0;
    let interrupted = false;
    let activeInterval = null;
    let currentStepIndex = -1;
    let currentFrameIndex = 0;
    // Create abort controller for cooperative cancellation
    const abortController = new AbortController();
    // Hide cursor
    process.stdout.write('\x1B[?25l');
    // Set up interrupt handler
    let restoreInterrupts = null;
    const onInterrupt = () => {
        if (interrupted)
            return;
        interrupted = true;
        abortController.abort();
        if (activeInterval)
            clearInterval(activeInterval);
        restoreInterrupts?.();
        restoreInterrupts = null;
        process.stdout.write('\x1B[?25h\n'); // Show cursor
    };
    restoreInterrupts = installInterruptHandlers(onInterrupt);
    // Force re-render function
    const forceRerender = (skipMove = false) => {
        if (currentStepIndex < 0 || currentStepIndex >= state.length)
            return;
        const colorKey = SPINNER_COLORS[currentFrameIndex % SPINNER_COLORS.length] ?? 'cyan';
        const color = getColor(colorKey);
        const frame = FRAMES[currentFrameIndex % FRAMES.length] ?? FRAMES[0] ?? '◐';
        const spinner = `${color}${COLORS.bold}${frame}${COLORS.reset}`;
        const rendered = renderAllSteps(state, currentStepIndex, spinner);
        // Optionally move up, then to column 0
        if (!skipMove && totalLinesFromLastRender > 0) {
            process.stdout.write(`\x1B[${totalLinesFromLastRender}A`);
            process.stdout.write('\x1B[0G');
        }
        process.stdout.write('\x1B[0J');
        process.stdout.write(rendered.output + '\n');
        totalLinesFromLastRender = rendered.totalLines;
    };
    try {
        // Enable pause/resume capability
        enablePauseResume(() => totalLinesFromLastRender, forceRerender);
        // Save cursor position BEFORE rendering steps
        process.stdout.write('\x1B[s');
        // Initial render
        const initialRender = renderAllSteps(state, -1);
        process.stdout.write(initialRender.output + '\n');
        totalLinesFromLastRender = initialRender.totalLines;
        // Execute steps
        for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
            if (interrupted)
                break;
            currentStepIndex = stepIndex;
            currentFrameIndex = 0;
            const step = steps[stepIndex];
            const stepState = state[stepIndex];
            if (step === undefined || stepState === undefined)
                continue;
            stepState.status = 'running';
            // Start spinner animation
            activeInterval = setInterval(() => {
                if (isPaused)
                    return;
                const colorKey = SPINNER_COLORS[currentFrameIndex % SPINNER_COLORS.length] ?? 'cyan';
                const color = getColor(colorKey);
                const frame = FRAMES[currentFrameIndex % FRAMES.length] ?? FRAMES[0] ?? '◐';
                const spinner = `${color}${COLORS.bold}${frame}${COLORS.reset}`;
                // Render all steps from state
                const rendered = renderAllSteps(state, currentStepIndex, spinner);
                // Move to start, clear, render
                if (totalLinesFromLastRender > 0) {
                    process.stdout.write(`\x1B[${totalLinesFromLastRender}A`); // Move up
                    process.stdout.write('\x1B[0G'); // Move to column 0 (using absolute positioning)
                }
                process.stdout.write('\x1B[0J'); // Clear from cursor to end
                process.stdout.write(rendered.output + '\n');
                totalLinesFromLastRender = rendered.totalLines;
                currentFrameIndex++;
            }, 120);
            // Progress callback
            const progressCallback = (progress) => {
                if (isPaused)
                    return;
                stepState.progress = Math.min(100, Math.max(0, progress));
                // Render all steps from state
                const colorKey = SPINNER_COLORS[currentFrameIndex % SPINNER_COLORS.length] ?? 'cyan';
                const color = getColor(colorKey);
                const frame = FRAMES[currentFrameIndex % FRAMES.length] ?? FRAMES[0] ?? '◐';
                const spinner = `${color}${COLORS.bold}${frame}${COLORS.reset}`;
                const rendered = renderAllSteps(state, currentStepIndex, spinner);
                // Move to start, clear, render
                if (totalLinesFromLastRender > 0) {
                    process.stdout.write(`\x1B[${totalLinesFromLastRender}A`);
                    process.stdout.write('\x1B[0G');
                }
                process.stdout.write('\x1B[0J');
                process.stdout.write(rendered.output + '\n');
                totalLinesFromLastRender = rendered.totalLines;
            };
            // Run the step
            try {
                const outcome = await step.run({
                    progress: progressCallback,
                    signal: abortController.signal,
                });
                // Update state from outcome
                if (outcome.status === 'success') {
                    stepState.status = 'success';
                    stepState.output = outcome.output;
                }
                else if (outcome.status === 'skipped') {
                    stepState.status = 'skipped';
                    stepState.skipReason = outcome.reason;
                    stepState.output = outcome.output;
                }
                else {
                    stepState.status = 'error';
                    stepState.errorMessage = outcome.message;
                    stepState.errorCause = outcome.cause;
                    stepState.output = outcome.output;
                }
            }
            catch (err) {
                // If the step was aborted due to cancellation, treat as interrupt
                if (err instanceof Error && err.name === 'AbortError') {
                    interrupted = true;
                    throw new StepInterruptError();
                }
                stepState.status = 'error';
                stepState.errorMessage = err instanceof Error ? err.message : String(err);
                stepState.errorCause = err instanceof Error ? err : undefined;
            }
            // Stop spinner
            if (activeInterval) {
                clearInterval(activeInterval);
                activeInterval = null;
            }
            // Emit clean log for this step (for external log collection)
            emitCleanStepLog(stepState);
            // Final render with outcome
            stepState.progress = undefined;
            const finalRender = renderAllSteps(state, -1);
            if (totalLinesFromLastRender > 0) {
                process.stdout.write(`\x1B[${totalLinesFromLastRender}A`);
                process.stdout.write('\x1B[0G');
            }
            process.stdout.write('\x1B[0J');
            process.stdout.write(finalRender.output + '\n');
            totalLinesFromLastRender = finalRender.totalLines;
            // Handle errors
            if (stepState.status === 'error') {
                // If the error is due to abort/cancellation, treat as interrupt
                if (stepState.errorCause instanceof Error &&
                    stepState.errorCause.name === 'AbortError') {
                    interrupted = true;
                    throw new StepInterruptError();
                }
                const errorColor = getColor('red');
                const errorMsg = stepState.errorMessage || 'An unknown error occurred';
                console.error(`\n${errorColor}Error: ${errorMsg}${COLORS.reset}`);
                if (stepState.errorCause instanceof ValidationInputError ||
                    stepState.errorCause instanceof ValidationOutputError) {
                    printValidationIssues(stepState.errorCause.issues);
                }
                console.error('');
                process.stdout.write('\x1B[?25h'); // Show cursor
                process.exit(1);
            }
        }
        // If interrupted during step execution, throw so callers' finally blocks run
        if (interrupted) {
            throw new StepInterruptError();
        }
        // Show cursor
        process.stdout.write('\x1B[?25h');
    }
    catch (err) {
        process.stdout.write('\x1B[?25h');
        throw err;
    }
    finally {
        restoreInterrupts?.();
        restoreInterrupts = null;
        getTotalLinesFn = null; // Clear pause capability
        forceRerenderFn = null;
    }
}
/**
 * Run steps in plain mode (no animations)
 */
async function runStepsPlain(steps) {
    const grayColor = getColor('gray');
    const greenColor = getColor('green');
    const yellowColor = getColor('yellow');
    const redColor = getColor('red');
    const abortController = new AbortController();
    let interrupted = false;
    const onInterrupt = () => {
        if (interrupted)
            return;
        interrupted = true;
        abortController.abort();
    };
    process.on('SIGINT', onInterrupt);
    process.on('SIGTERM', onInterrupt);
    try {
        for (const step of steps) {
            if (abortController.signal.aborted)
                break;
            let outcome;
            try {
                outcome = await step.run({ progress: () => { }, signal: abortController.signal });
            }
            catch (err) {
                // If the step was aborted due to cancellation, treat as interrupt
                if (err instanceof Error && err.name === 'AbortError') {
                    interrupted = true;
                    throw new StepInterruptError();
                }
                outcome = {
                    status: 'error',
                    message: err instanceof Error ? err.message : String(err),
                    cause: err instanceof Error ? err : undefined,
                };
            }
            // Build step state for clean log emission
            const stepState = {
                label: step.label,
                status: outcome.status,
                output: outcome.output,
                skipReason: outcome.status === 'skipped' ? outcome.reason : undefined,
                errorMessage: outcome.status === 'error' ? outcome.message : undefined,
                errorCause: outcome.status === 'error' ? outcome.cause : undefined,
            };
            // Emit clean log for this step
            emitCleanStepLog(stepState);
            // Print final state
            if (outcome.status === 'success') {
                console.log(`${greenColor}${ICONS.success}${COLORS.reset} ${step.label}`);
                if (outcome.output && outcome.output.length > 0) {
                    console.log(`${grayColor}╭─ Output${COLORS.reset}`);
                    for (const line of outcome.output) {
                        console.log(`${grayColor}│${COLORS.reset} ${line}`);
                    }
                    console.log(`${grayColor}╰─${COLORS.reset}`);
                    console.log('');
                }
            }
            else if (outcome.status === 'skipped') {
                const reason = outcome.reason ? ` ${grayColor}(${outcome.reason})${COLORS.reset}` : '';
                console.log(`${yellowColor}${ICONS.skipped}${COLORS.reset} ${step.label}${reason}`);
                if (outcome.output && outcome.output.length > 0) {
                    console.log(`${grayColor}╭─ Output${COLORS.reset}`);
                    for (const line of outcome.output) {
                        console.log(`${grayColor}│${COLORS.reset} ${line}`);
                    }
                    console.log(`${grayColor}╰─${COLORS.reset}`);
                    console.log('');
                }
            }
            else {
                // If the error is due to abort/cancellation, treat as interrupt
                if (outcome.cause instanceof Error && outcome.cause.name === 'AbortError') {
                    interrupted = true;
                    throw new StepInterruptError();
                }
                console.log(`${redColor}${ICONS.error}${COLORS.reset} ${step.label}`);
                if (outcome.output && outcome.output.length > 0) {
                    console.log(`${grayColor}╭─ Output${COLORS.reset}`);
                    for (const line of outcome.output) {
                        console.log(`${grayColor}│${COLORS.reset} ${line}`);
                    }
                    console.log(`${grayColor}╰─${COLORS.reset}`);
                    console.log('');
                }
                const errorColor = getColor('red');
                const errorMsg = outcome.message || 'An unknown error occurred';
                console.error(`\n${errorColor}Error: ${errorMsg}${COLORS.reset}`);
                if (outcome.cause instanceof ValidationInputError ||
                    outcome.cause instanceof ValidationOutputError) {
                    printValidationIssues(outcome.cause.issues);
                }
                console.error('');
                process.exit(1);
            }
        }
        // If interrupted during step execution, throw so callers' finally blocks run
        if (interrupted) {
            throw new StepInterruptError();
        }
    }
    finally {
        process.off('SIGINT', onInterrupt);
        process.off('SIGTERM', onInterrupt);
    }
}
/**
 * Run a series of steps with animated progress
 */
export async function runSteps(steps, logLevel) {
    const outputOptions = getOutputOptions();
    // In JSON mode, skip all UI rendering
    if (outputOptions && isJSONMode(outputOptions)) {
        const abortController = new AbortController();
        for (const step of steps) {
            if (abortController.signal.aborted)
                break;
            if (step) {
                const ctx = {
                    signal: abortController.signal,
                    progress: () => { },
                };
                let outcome;
                try {
                    outcome = await step.run(ctx);
                }
                catch (err) {
                    if (err instanceof Error && err.name === 'AbortError') {
                        throw new StepInterruptError();
                    }
                    outcome = {
                        status: 'error',
                        message: err instanceof Error ? err.message : String(err),
                        cause: err instanceof Error ? err : undefined,
                    };
                }
                if (outcome.status === 'error') {
                    if (outcome.cause instanceof Error && outcome.cause.name === 'AbortError') {
                        throw new StepInterruptError();
                    }
                    const errorMsg = outcome.message || 'An unknown error occurred';
                    throw new Error(errorMsg);
                }
            }
        }
        return;
    }
    const useTUI = isTTYLike() && (!logLevel || ['info', 'warn', 'error'].includes(logLevel));
    if (useTUI) {
        await runStepsTUI(steps);
    }
    else {
        await runStepsPlain(steps);
    }
}
//# sourceMappingURL=steps.js.map