// per-call audit of `role=public` tool invocations.
//
// Tails each public session's claude-owned JSONL transcript as it's written.
// For every assistant `tool_use` content block the manager stashes
// `{toolUseId → {name, ts}}`; the matching user `tool_result` block pops the
// entry and emits one log line:
//   tool-call role=public sessionId=<claude-id> tool=<name> ms=<delta>
//
// An operator grepping the manager log can now answer "what did every
// unauthenticated session do today?" without parsing JSONLs by hand. tool_use
// entries that never receive a matching tool_result by teardown (PTY exit,
// session DELETE, manager shutdown) are emitted as `tool-call-orphan` so
// errored-out calls aren't silently lost.
import { existsSync, statSync, createReadStream, watchFile, unwatchFile } from 'node:fs';
export function createAuditState(opts) {
    const pending = new Map();
    const tag = opts.sessionId;
    function tsFromLine(parsed, fallback) {
        if (parsed && typeof parsed === 'object' && 'timestamp' in parsed) {
            const raw = parsed.timestamp;
            if (typeof raw === 'string') {
                const n = Date.parse(raw);
                if (Number.isFinite(n))
                    return n;
            }
        }
        return fallback;
    }
    return {
        ingest(line, fallbackTsMs) {
            const trimmed = line.trim();
            if (!trimmed)
                return [];
            let parsed;
            try {
                parsed = JSON.parse(trimmed);
            }
            catch {
                opts.logger(`audit-parse-failed sessionId=${tag} bytes=${trimmed.length}`);
                return [];
            }
            if (!parsed || typeof parsed !== 'object')
                return [];
            const root = parsed;
            const message = root.message;
            if (!message || typeof message !== 'object')
                return [];
            const content = message.content;
            if (!Array.isArray(content))
                return [];
            const ts = tsFromLine(parsed, fallbackTsMs);
            const emissions = [];
            for (const block of content) {
                if (!block || typeof block !== 'object')
                    continue;
                const b = block;
                if (b.type === 'tool_use' && typeof b.id === 'string' && typeof b.name === 'string') {
                    pending.set(b.id, { name: b.name, tsMs: ts });
                    continue;
                }
                if (b.type === 'tool_result' && typeof b.tool_use_id === 'string') {
                    const open = pending.get(b.tool_use_id);
                    if (open) {
                        pending.delete(b.tool_use_id);
                        const ms = Math.max(0, ts - open.tsMs);
                        emissions.push(`tool-call role=public sessionId=${tag} tool=${open.name} ms=${ms}`);
                    }
                }
            }
            return emissions;
        },
        teardown() {
            const orphans = [];
            for (const [, info] of pending) {
                orphans.push(`tool-call-orphan role=public sessionId=${tag} tool=${info.name}`);
            }
            pending.clear();
            return orphans;
        },
    };
}
export function createAuditRegistry(deps) {
    const handles = new Map();
    function startWatcher(sessionId, jsonlPath) {
        const state = createAuditState({ sessionId, logger: deps.logger });
        let position = existsSync(jsonlPath) ? statSync(jsonlPath).size : 0;
        let buffer = '';
        let active = true;
        const flush = (chunk) => {
            buffer += chunk.toString('utf8');
            // Process complete lines only; keep the trailing partial in `buffer`.
            let nl;
            const now = Date.now();
            while ((nl = buffer.indexOf('\n')) >= 0) {
                const line = buffer.slice(0, nl);
                buffer = buffer.slice(nl + 1);
                for (const out of state.ingest(line, now))
                    deps.logger(out);
            }
        };
        const listener = () => {
            if (!active)
                return;
            if (!existsSync(jsonlPath)) {
                deps.logger(`audit-watcher-stopped sessionId=${sessionId} reason=jsonl-removed`);
                stop();
                return;
            }
            const newSize = statSync(jsonlPath).size;
            if (newSize <= position)
                return;
            const tail = createReadStream(jsonlPath, { start: position, end: newSize - 1 });
            position = newSize;
            tail.on('data', flush);
        };
        watchFile(jsonlPath, { interval: deps.pollIntervalMs ?? 250 }, listener);
        deps.logger(`audit-watcher-attached sessionId=${sessionId} path=${jsonlPath}`);
        function stop() {
            if (!active)
                return;
            active = false;
            unwatchFile(jsonlPath, listener);
            for (const orphan of state.teardown())
                deps.logger(orphan);
        }
        return { stop };
    }
    return {
        attach(sessionId, jsonlPath) {
            if (handles.has(sessionId))
                return; // idempotent: /resume reusing same id
            handles.set(sessionId, startWatcher(sessionId, jsonlPath));
        },
        detach(sessionId) {
            const h = handles.get(sessionId);
            if (!h)
                return;
            handles.delete(sessionId);
            h.stop();
        },
        drain() {
            for (const [id, h] of handles) {
                h.stop();
                handles.delete(id);
            }
        },
    };
}
//# sourceMappingURL=public-tool-audit.js.map