/**
 * Agent audit log — append-only JSONL with size-based rotation.
 *
 * Every gated tool call writes a row. Rows deliberately carry NO argument
 * values and NO response bodies — the agent already saw both, and logging
 * them would create a parallel on-disk copy of the user's email. We store
 * a truncated sha256 hash of the args instead so "same call repeated" is
 * observable without content leakage.
 *
 * File: ~/.mailpouch-agent-audit.jsonl (0600)
 * Rotation: when current file exceeds ROTATE_BYTES, rename to .1.gz and
 *           start fresh. Keep KEEP_GENERATIONS compressed generations.
 */
import { appendFileSync, existsSync, statSync, renameSync, writeFileSync, readFileSync, unlinkSync } from "fs";
import { createHash } from "crypto";
import { gzipSync } from "zlib";
import { logger } from "../utils/logger.js";
const ROTATE_BYTES = 10 * 1024 * 1024; // 10 MB per generation
const KEEP_GENERATIONS = 3;
export class AgentAuditLog {
    path;
    now;
    constructor(deps) {
        this.path = deps.path;
        this.now = deps.now ?? Date.now;
    }
    /** Append a row. Best-effort — logging failures are warned, not thrown. */
    write(row) {
        try {
            this.rotateIfNeeded();
            appendFileSync(this.path, JSON.stringify(row) + "\n", { encoding: "utf-8", mode: 0o600 });
        }
        catch (err) {
            logger.warn(`AgentAuditLog: append failed for ${this.path}`, "AgentAuditLog", err);
        }
    }
    rotateIfNeeded() {
        if (!existsSync(this.path))
            return;
        let sizeBytes = 0;
        try {
            sizeBytes = statSync(this.path).size;
        }
        catch { /* swallow */
            return;
        }
        if (sizeBytes < ROTATE_BYTES)
            return;
        // Age the existing .N.gz files up by one.
        for (let i = KEEP_GENERATIONS - 1; i >= 1; i--) {
            const from = `${this.path}.${i}.gz`;
            const to = `${this.path}.${i + 1}.gz`;
            if (existsSync(from)) {
                try {
                    renameSync(from, to);
                }
                catch (err) {
                    logger.debug(`rotate rename ${from} → ${to} failed`, "AgentAuditLog", err);
                }
            }
        }
        // PERM-012: rename the live file out of the way FIRST, then gzip the
        // renamed copy out-of-line. `rename(2)` is atomic on POSIX, so any
        // concurrent `appendFileSync(this.path, …)` lands in a fresh file rather
        // than in the snapshot we're about to compress — closing the
        // read→truncate window where a row written between `readFileSync` and the
        // truncating `writeFileSync` was silently lost.
        const staged = `${this.path}.rotating`;
        try {
            renameSync(this.path, staged);
        }
        catch (err) {
            logger.warn(`AgentAuditLog: rotation rename failed, keeping current file`, "AgentAuditLog", err);
            return;
        }
        try {
            const raw = readFileSync(staged);
            writeFileSync(`${this.path}.1.gz`, gzipSync(raw), { encoding: undefined, mode: 0o600 });
            unlinkSync(staged);
        }
        catch (err) {
            // Compression failed after the live file was renamed away. Don't leave the
            // rows orphaned in `${path}.rotating` — the NEXT rotation's rename would
            // clobber them. If no new live file exists yet (the common case), move the
            // staged copy back so appends continue it; otherwise park it under a unique
            // name for manual recovery rather than losing it.
            try {
                if (!existsSync(this.path)) {
                    renameSync(staged, this.path);
                }
                else {
                    renameSync(staged, `${this.path}.recover-${this.now()}`);
                }
            }
            catch { /* best-effort recovery */ }
            logger.warn(`AgentAuditLog: rotation compress failed; staged copy recovered`, "AgentAuditLog", err);
        }
        // Evict generations beyond KEEP_GENERATIONS.
        for (let i = KEEP_GENERATIONS + 1; i < KEEP_GENERATIONS + 5; i++) {
            const p = `${this.path}.${i}.gz`;
            if (existsSync(p)) {
                try {
                    unlinkSync(p);
                }
                catch { /* ignore */ }
            }
        }
    }
    /**
     * Read the last `limit` rows. Used by the (forthcoming) Log UI tab.
     * Cheap for typical sizes because the file is append-only JSONL; for
     * very large logs consider tailing with a seek, but we're at most 10 MB
     * so a full load + slice is fine.
     */
    readTail(limit = 200) {
        if (!existsSync(this.path))
            return [];
        try {
            const raw = readFileSync(this.path, "utf-8");
            const lines = raw.split("\n").filter(l => l.length > 0);
            const slice = lines.slice(-limit);
            const rows = [];
            for (const line of slice) {
                try {
                    rows.push(JSON.parse(line));
                }
                catch { /* skip malformed */ }
            }
            return rows;
        }
        catch (err) {
            logger.warn(`AgentAuditLog: readTail failed`, "AgentAuditLog", err);
            return [];
        }
    }
}
/**
 * Truncated sha256 of JSON.stringify(args). Gives us a stable "same args"
 * fingerprint without storing the content. 16 hex chars = 64 bits of
 * collision resistance, plenty for a dedup hint at mailpouch scale.
 */
export function hashArgs(args) {
    if (args === undefined || args === null)
        return "";
    try {
        const s = JSON.stringify(args);
        if (!s)
            return "";
        return createHash("sha256").update(s, "utf-8").digest("hex").slice(0, 16);
    }
    catch {
        return "";
    }
}
//# sourceMappingURL=audit.js.map