import { isInteractive, uiStream } from "./runtime.js";
import { SPINNER_FRAMES, cyan, dim, glyph, gray, green, red, yellow, } from "./theme.js";
function elapsedLabel(step) {
    if (step.startedAt === undefined)
        return "";
    const end = step.endedAt ?? Date.now();
    const seconds = (end - step.startedAt) / 1000;
    if (seconds < 0.05)
        return "";
    return gray(` ${seconds.toFixed(1)}s`);
}
function symbolFor(step, frame) {
    switch (step.status) {
        case "pending":
            return gray(glyph.pending());
        case "active":
            return cyan(SPINNER_FRAMES[frame] ?? "-");
        case "done":
            return green(glyph.tick());
        case "failed":
            return red(glyph.cross());
        case "skipped":
            return yellow(glyph.bullet());
        default: {
            const exhaustive = step.status;
            throw new Error(`unknown step status: ${String(exhaustive)}`);
        }
    }
}
/**
 * A live checklist of stages. On a TTY it re-renders in place with per-stage
 * spinners and elapsed time; otherwise it prints one line per state transition
 * so non-interactive logs stay ordered and readable.
 */
export class StepList {
    steps;
    stream = uiStream();
    interactive = isInteractive();
    title;
    timer;
    frame = 0;
    renderedLines = 0;
    started = false;
    constructor(steps, options = {}) {
        this.steps = steps.map((step) => ({
            id: step.id,
            label: step.label,
            status: "pending",
        }));
        this.title = options.title;
    }
    start() {
        if (this.started)
            return this;
        this.started = true;
        if (this.interactive) {
            this.hideCursor();
            this.render();
            this.timer = setInterval(() => {
                this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
                this.render();
            }, 80);
            this.timer.unref();
        }
        else if (this.title !== undefined) {
            this.stream.write(`${this.title}\n`);
        }
        return this;
    }
    setActive(id, detail) {
        this.transition(id, "active", detail);
    }
    setDone(id, detail) {
        this.transition(id, "done", detail);
    }
    setFailed(id, detail) {
        this.transition(id, "failed", detail);
    }
    setSkipped(id, detail) {
        this.transition(id, "skipped", detail);
    }
    setDetail(id, detail) {
        const step = this.find(id);
        step.detail = detail;
        if (this.interactive)
            this.render();
    }
    /** Stop animation and leave the final frame in place. */
    stop() {
        if (this.timer !== undefined) {
            clearInterval(this.timer);
            this.timer = undefined;
        }
        if (this.interactive && this.started) {
            this.render();
            this.showCursor();
        }
        this.started = false;
    }
    transition(id, status, detail) {
        const step = this.find(id);
        if (status === "active" && step.startedAt === undefined)
            step.startedAt = Date.now();
        if ((status === "done" || status === "failed" || status === "skipped") &&
            step.endedAt === undefined) {
            step.endedAt = Date.now();
            if (step.startedAt === undefined)
                step.startedAt = step.endedAt;
        }
        step.status = status;
        if (detail !== undefined)
            step.detail = detail;
        if (this.interactive)
            this.render();
        else
            this.printLine(step);
    }
    printLine(step) {
        const detail = step.detail !== undefined ? ` ${dim(step.detail)}` : "";
        this.stream.write(`${symbolFor(step, 0)} ${step.label}${detail}\n`);
    }
    render() {
        const lines = [];
        if (this.title !== undefined)
            lines.push(this.title);
        for (const step of this.steps) {
            const detail = step.detail !== undefined ? ` ${dim(step.detail)}` : "";
            lines.push(`${symbolFor(step, this.frame)} ${step.label}${detail}${elapsedLabel(step)}`);
        }
        this.clear();
        this.stream.write(lines.join("\n") + "\n");
        this.renderedLines = lines.length;
    }
    clear() {
        if (!this.interactive || this.renderedLines === 0)
            return;
        this.stream.write(`\u001b[${this.renderedLines}A`);
        this.stream.write("\u001b[0J");
    }
    find(id) {
        const step = this.steps.find((candidate) => candidate.id === id);
        if (step === undefined)
            throw new Error(`unknown step id: ${id}`);
        return step;
    }
    hideCursor() {
        if (this.interactive)
            this.stream.write("\u001b[?25l");
    }
    showCursor() {
        if (this.interactive)
            this.stream.write("\u001b[?25h");
    }
}
