"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeployRenderer = void 0;
const node_stream_1 = require("node:stream");
// cli-progress is already a runtime dep of harper (see package.json); using its
// SingleBar to render the upload phase here doesn't add a new dependency.
const cli_progress_1 = __importDefault(require("cli-progress"));
/**
 * Deploy-time renderer that owns the progress display across two phases:
 *
 *   1. Local upload — driven by `tapUploadStream`, which wraps the multipart body so we
 *      can update a `cli-progress` bar against the precomputed uncompressed source-tree
 *      total. The bar moves as gzipped bytes are sent and snaps to 100% on completion.
 *      In a non-TTY environment a single "Uploaded X MiB" line is printed on completion.
 *
 *   2. Server-side phases — driven by `renderEvent`, called for each SSE message the
 *      CLI receives from the operations API. Phase events print one-liners; live
 *      `install` events (npm/pnpm/yarn stdout) are throttled to one line under a
 *      "[install]" header so a noisy `npm install` doesn't drown the terminal.
 *
 * Designed so the two phases hand off cleanly: `endUpload()` tears the bar down (so
 * it doesn't compete with subsequent prints) before any SSE events render.
 */
class DeployRenderer {
    upload = { bar: null, sent: 0, finished: false };
    phase = { installLineCount: 0 };
    output;
    isTty;
    uploadTotal;
    constructor(options = {}) {
        this.output = options.output ?? process.stderr;
        // Only render a bar when stderr is a real terminal. CI runners, log redirection,
        // and pipes look identical from Node's perspective: !isTTY.
        this.isTty = Boolean(this.output.isTTY);
        this.uploadTotal = options.uploadTotal ?? 0;
    }
    /**
     * Wrap an outbound stream so each byte flowing through it advances the upload bar.
     * The Transform is identity — chunks pass through unmodified.
     */
    tapUploadStream(stream) {
        this.upload.bar = this.isTty
            ? new cli_progress_1.default.SingleBar({
                // {value_fmt} and {total_fmt} are payload tokens updated in tickUpload/endUpload.
                // uploadTotal is the uncompressed source size; gzip output is smaller so the
                // bar won't naturally reach 100% — endUpload() snaps it on completion.
                format: 'Uploading [{bar}] {percentage}% | {value_fmt} / ~{total_fmt}',
                barCompleteChar: '█',
                barIncompleteChar: '░',
                hideCursor: true,
                stream: this.output,
            }, cli_progress_1.default.Presets.shades_classic)
            : null;
        this.upload.bar?.start(this.uploadTotal || 1, 0, {
            value_fmt: formatBytes(0),
            total_fmt: formatBytes(this.uploadTotal),
        });
        const counter = new node_stream_1.Transform({
            transform: (chunk, _enc, cb) => {
                // Bytes are counted externally via countUploadBytes() on the pre-gzip tar
                // stream so progress and total are both in uncompressed units. The Transform
                // is kept here solely to get the flush callback that signals upload completion.
                cb(null, chunk);
            },
            flush: (cb) => {
                this.endUpload();
                cb();
            },
        });
        stream.on('error', (err) => counter.destroy(err));
        stream.pipe(counter);
        return counter;
    }
    /**
     * Record `n` pre-gzip bytes read from the tar pack stream. Called for each
     * raw tar chunk by the `onBytes` callback passed to `streamPackagedDirectory`,
     * keeping progress and total in the same (uncompressed) unit so the bar
     * tracks smoothly and doesn't terminate far short of 100%.
     */
    countUploadBytes(n) {
        if (this.upload.finished)
            return;
        this.upload.sent += n;
        this.tickUpload();
    }
    endUpload() {
        if (this.upload.finished)
            return;
        this.upload.finished = true;
        if (this.upload.bar) {
            // Snap to total so the bar shows 100% even when our uncompressed-total estimate
            // is slightly off (gzip output is usually smaller than the source tree).
            const finalPayload = { value_fmt: formatBytes(this.upload.sent), total_fmt: formatBytes(this.uploadTotal) };
            if (this.uploadTotal > 0)
                this.upload.bar.update(this.uploadTotal, finalPayload);
            this.upload.bar.stop();
            this.upload.bar = null;
        }
        else {
            // Non-TTY: single completion line, no intermediate chatter.
            this.output.write(`Uploaded ${formatBytes(this.upload.sent)}\n`);
        }
    }
    tickUpload() {
        if (this.upload.finished)
            return;
        if (this.upload.bar) {
            this.upload.bar.update(this.upload.sent, { value_fmt: formatBytes(this.upload.sent) });
        }
        // Non-TTY: no intermediate lines — endUpload() prints the final size on completion.
    }
    renderEvent(message) {
        let parsed;
        try {
            parsed = JSON.parse(message.data);
        }
        catch {
            parsed = message.data;
        }
        switch (message.event) {
            case 'phase':
                this.renderPhase(parsed);
                break;
            case 'install':
                this.renderInstall(parsed);
                break;
            case 'error': {
                const e = parsed;
                this.output.write(`error: ${e.message ?? message.data}${e.code ? ` (${e.code})` : ''}\n`);
                break;
            }
            case 'done':
                // Caller picks up final result via the SSE iterator; nothing to render here.
                break;
        }
    }
    renderPhase(data) {
        const label = data.phase ?? '?';
        if (data.status === 'start') {
            if (this.phase.current !== label) {
                this.output.write(`${label}…\n`);
                this.phase.current = label;
                this.phase.installLineCount = 0;
            }
        }
        else if (data.status === 'done') {
            if (label === 'install' && this.phase.installLineCount > 0) {
                this.output.write(`install done (${this.phase.installLineCount} log lines)\n`);
            }
            else {
                this.output.write(`${label} done\n`);
            }
        }
        else if (data.status === 'error') {
            this.output.write(`${label} ERROR: ${data.message ?? 'failed'}\n`);
        }
    }
    renderInstall(data) {
        const line = (data.line ?? '').trimEnd();
        if (!line)
            return;
        if (data.manager && data.manager !== this.phase.installManager) {
            this.phase.installManager = data.manager;
            this.output.write(`install: using ${data.manager}\n`);
        }
        this.phase.installLineCount++;
        // Prefix with stream so users can distinguish stderr noise from stdout warnings.
        const tag = data.stream === 'stderr' ? '!' : '|';
        this.output.write(`  ${tag} ${line}\n`);
    }
}
exports.DeployRenderer = DeployRenderer;
function formatBytes(bytes) {
    if (bytes < 1024)
        return `${bytes} B`;
    if (bytes < 1024 * 1024)
        return `${(bytes / 1024).toFixed(1)} KiB`;
    if (bytes < 1024 * 1024 * 1024)
        return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
}
//# sourceMappingURL=deployRenderer.js.map