/**
 * `~/.olam/upgrade.log` — JSONL append-only audit log for `olam upgrade`.
 *
 * Phase 2c — C1.
 *
 * Schema:
 *   {
 *     ts: ISO 8601,
 *     started_at: epoch ms,
 *     ended_at: epoch ms,
 *     sha_target: string (40-char SHA, captured-after-pull),
 *     sha_before: { hostCp, authService, devbox },
 *     sha_after:  { hostCp, authService, devbox },
 *     status: "success" | "failed" | "rolled_back",
 *     failed_step: string | null,
 *     durations_ms: { [step_label]: number }
 *   }
 *
 * Open-per-write semantics — open() + write() + close() per row so
 * external log-rotators can rename the file mid-run without us writing
 * to a now-unlinked fd. Best-effort; errors logged to stderr and
 * swallowed so the audit log never blocks an upgrade from completing.
 *
 * `failed_step` MUST hold a step LABEL only (e.g. "bash build-auth.sh"),
 * NEVER raw stdout/stderr — paths and credentials may leak. (Per audit
 * security finding A6-007.) The CLI's terminal already shows the full
 * stderr; the log row records the step name for queryability.
 */
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
/**
 * Resolve the upgrade-log path lazily so tests can override HOME via
 * process.env. Lazy resolution is also forward-compatible with operators
 * who set XDG_DATA_HOME or similar overrides at session start.
 */
export function getUpgradeLogPath() {
    const home = process.env['HOME'] ?? os.homedir();
    return path.join(home, '.olam', 'upgrade.log');
}
/** Convenience constant for callers who want the production path string. */
export const UPGRADE_LOG_PATH = getUpgradeLogPath();
/**
 * Append a single row to ~/.olam/upgrade.log.
 *
 * Open-per-write to survive log rotation. Errors swallowed to stderr —
 * audit log failure must never block an upgrade.
 *
 * `logPath` parameter is a test seam; production callers omit it.
 */
export function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
    try {
        fs.mkdirSync(path.dirname(logPath), { recursive: true });
        const line = JSON.stringify(row) + '\n';
        fs.appendFileSync(logPath, line, { mode: 0o644 });
    }
    catch (err) {
        process.stderr.write(`[upgrade-log] failed to append: ${err instanceof Error ? err.message : String(err)}\n`);
    }
}
/**
 * Read up to N most-recent rows from ~/.olam/upgrade.log.
 *
 * Returns an empty array on missing file (first-run UX).
 *
 * Per audit invariant: corrupt JSON lines (partial mid-write, manual
 * tampering) are SKIPPED with a stderr warning rather than crashing
 * `--history`.
 */
export function readUpgradeLog(limit = 10, logPath = getUpgradeLogPath()) {
    if (!fs.existsSync(logPath))
        return [];
    let raw;
    try {
        raw = fs.readFileSync(logPath, 'utf-8');
    }
    catch (err) {
        process.stderr.write(`[upgrade-log] failed to read: ${err instanceof Error ? err.message : String(err)}\n`);
        return [];
    }
    const lines = raw.split('\n').filter((l) => l.length > 0);
    const rows = [];
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        try {
            const parsed = JSON.parse(line);
            // Defensive shape check — silently skip rows that don't look right.
            if (typeof parsed.ts === 'string' &&
                typeof parsed.started_at === 'number' &&
                typeof parsed.status === 'string') {
                rows.push(parsed);
            }
            else {
                process.stderr.write(`[upgrade-log] skipped malformed row at line ${i + 1}\n`);
            }
        }
        catch {
            process.stderr.write(`[upgrade-log] skipped corrupt JSON at line ${i + 1}\n`);
        }
    }
    // Most recent last (file is chronological); return last N.
    return rows.slice(-Math.max(0, limit));
}
/** Format duration ms → "1.4s" / "12m04s" / "1h23m" for table display. */
export function formatDuration(ms) {
    if (ms < 1000)
        return `${ms}ms`;
    const totalSec = Math.round(ms / 1000);
    if (totalSec < 60)
        return `${totalSec}s`;
    const min = Math.floor(totalSec / 60);
    const sec = totalSec % 60;
    if (min < 60)
        return `${min}m${String(sec).padStart(2, '0')}s`;
    const hr = Math.floor(min / 60);
    const remMin = min % 60;
    return `${hr}h${String(remMin).padStart(2, '0')}m`;
}
/**
 * Format an array of upgrade-log rows as a 5-column ASCII table:
 *   timestamp | sha (8-char) | status | duration | failed_step
 *
 * Status column uses ✓/✗/↩ icons + plain text for grep-friendliness.
 */
export function formatHistoryTable(rows) {
    if (rows.length === 0) {
        return 'No upgrade history yet. Run `olam upgrade` to create your first record.';
    }
    const lines = [];
    lines.push('TIMESTAMP                   SHA       STATUS         DURATION   FAILED-STEP');
    lines.push('────────────────────────────────────────────────────────────────────────────────');
    for (const r of rows) {
        const ts = r.ts.slice(0, 19).replace('T', ' ');
        const sha = r.sha_target.slice(0, 8);
        const statusIcon = r.status === 'success' ? '✓ success' : r.status === 'rolled_back' ? '↩ rolled_back' : '✗ failed';
        const dur = formatDuration(r.ended_at - r.started_at);
        const failed = r.failed_step ?? '';
        lines.push(`${ts.padEnd(28)}${sha.padEnd(10)}${statusIcon.padEnd(15)}${dur.padEnd(11)}${failed}`);
    }
    return lines.join('\n');
}
/** Format rows as one JSON object per line (JSONL passthrough — same as on-disk format). */
export function formatHistoryJson(rows) {
    return rows.map((r) => JSON.stringify(r)).join('\n');
}
//# sourceMappingURL=upgrade-log.js.map