/**
 * Transcript Logger
 *
 * Records agent interactions with Merkle chain integrity.
 * Each entry is cryptographically linked to the previous,
 * enabling tamper detection and audit trails.
 *
 * @module transcripts/logger
 */
import { createHash, randomUUID } from "crypto";
import { readFile, writeFile, mkdir, appendFile } from "fs/promises";
import { dirname } from "path";
// ============================================================================
// Hash Functions
// ============================================================================
/**
 * Calculate SHA-256 hash
 */
function sha256(content) {
    return createHash("sha256").update(content).digest("hex");
}
/**
 * Calculate entry hash (links to previous)
 */
function calculateEntryHash(entry) {
    const data = JSON.stringify({
        id: entry.id,
        timestamp: entry.timestamp,
        type: entry.type,
        sessionId: entry.sessionId,
        contentHash: entry.contentHash,
        previousHash: entry.previousHash,
    });
    return sha256(data);
}
// ============================================================================
// Transcript Logger Class
// ============================================================================
/**
 * Logger for recording agent transcripts with Merkle chain integrity
 */
export class TranscriptLogger {
    sessionId;
    entries = [];
    previousHash = "genesis";
    filePath = null;
    startedAt;
    hasRedactions = false;
    /**
     * Create a new transcript logger
     */
    constructor(sessionId) {
        this.sessionId = sessionId || randomUUID();
        this.startedAt = new Date().toISOString();
    }
    /**
     * Get the session ID
     */
    getSessionId() {
        return this.sessionId;
    }
    /**
     * Append an entry to the transcript
     */
    append(entry) {
        const id = `${this.sessionId}-${String(this.entries.length + 1).padStart(5, "0")}`;
        const timestamp = new Date().toISOString();
        const contentHash = sha256(entry.content);
        const fullEntry = {
            id,
            timestamp,
            type: entry.type,
            sessionId: this.sessionId,
            modelId: entry.modelId,
            toolName: entry.toolName,
            content: entry.content,
            contentHash,
            previousHash: this.previousHash,
            metadata: entry.metadata,
        };
        const entryHash = calculateEntryHash(fullEntry);
        const finalEntry = { ...fullEntry, entryHash };
        this.entries.push(finalEntry);
        this.previousHash = entryHash;
        if (entry.type === "redaction") {
            this.hasRedactions = true;
        }
        // Append to file if configured
        if (this.filePath) {
            this.appendToFile(finalEntry).catch(() => {
                // Silently fail file writes
            });
        }
        return finalEntry;
    }
    /**
     * Log a prompt
     */
    logPrompt(content, modelId, metadata) {
        return this.append({
            type: "prompt",
            sessionId: this.sessionId,
            content,
            modelId,
            metadata,
        });
    }
    /**
     * Log a tool call
     */
    logToolCall(toolName, input, metadata) {
        return this.append({
            type: "tool_call",
            sessionId: this.sessionId,
            toolName,
            content: input,
            metadata,
        });
    }
    /**
     * Log a tool result
     */
    logToolResult(toolName, output, metadata) {
        return this.append({
            type: "tool_result",
            sessionId: this.sessionId,
            toolName,
            content: output,
            metadata,
        });
    }
    /**
     * Log a model response
     */
    logModelResponse(content, modelId, metadata) {
        return this.append({
            type: "model_response",
            sessionId: this.sessionId,
            content,
            modelId,
            metadata,
        });
    }
    /**
     * Log a system event
     */
    logSystem(content, metadata) {
        return this.append({
            type: "system",
            sessionId: this.sessionId,
            content,
            metadata,
        });
    }
    /**
     * Log an error
     */
    logError(content, metadata) {
        return this.append({
            type: "error",
            sessionId: this.sessionId,
            content,
            metadata,
        });
    }
    /**
     * Log a redaction event (for audit trail)
     */
    logRedaction(originalContentHash, redactionType, metadata) {
        return this.append({
            type: "redaction",
            sessionId: this.sessionId,
            content: `Redacted: ${redactionType}`,
            metadata: {
                ...metadata,
                originalContentHash,
                redactionType,
            },
        });
    }
    /**
     * Get all entries
     */
    getChain() {
        return [...this.entries];
    }
    /**
     * Get entry count
     */
    getEntryCount() {
        return this.entries.length;
    }
    /**
     * Verify the Merkle chain integrity
     */
    verify() {
        if (this.entries.length === 0) {
            return {
                valid: true,
                entriesVerified: 0,
                rootHash: "genesis",
                tipHash: "genesis",
            };
        }
        const rootHash = this.entries[0].entryHash;
        const tipHash = this.entries[this.entries.length - 1].entryHash;
        // Verify first entry links to genesis
        if (this.entries[0].previousHash !== "genesis") {
            return {
                valid: false,
                entriesVerified: 0,
                brokenAt: 0,
                issue: "First entry does not link to genesis",
                rootHash,
                tipHash,
            };
        }
        // Verify each entry
        for (let i = 0; i < this.entries.length; i++) {
            const entry = this.entries[i];
            // Recalculate hash
            const expectedHash = calculateEntryHash({
                id: entry.id,
                timestamp: entry.timestamp,
                type: entry.type,
                sessionId: entry.sessionId,
                modelId: entry.modelId,
                toolName: entry.toolName,
                content: entry.content,
                contentHash: entry.contentHash,
                previousHash: entry.previousHash,
                metadata: entry.metadata,
            });
            if (entry.entryHash !== expectedHash) {
                return {
                    valid: false,
                    entriesVerified: i,
                    brokenAt: i,
                    issue: `Entry ${i} hash mismatch: expected ${expectedHash}, got ${entry.entryHash}`,
                    rootHash,
                    tipHash,
                };
            }
            // Verify chain link
            if (i > 0 && entry.previousHash !== this.entries[i - 1].entryHash) {
                return {
                    valid: false,
                    entriesVerified: i,
                    brokenAt: i,
                    issue: `Entry ${i} chain broken: previousHash doesn't match previous entry`,
                    rootHash,
                    tipHash,
                };
            }
        }
        return {
            valid: true,
            entriesVerified: this.entries.length,
            rootHash,
            tipHash,
        };
    }
    /**
     * Generate a signed transcript summary
     */
    generateSignedTranscript() {
        const entrySummary = {
            prompt: 0,
            tool_call: 0,
            tool_result: 0,
            model_response: 0,
            system: 0,
            error: 0,
            redaction: 0,
        };
        for (const entry of this.entries) {
            entrySummary[entry.type]++;
        }
        return {
            sessionId: this.sessionId,
            startedAt: this.startedAt,
            endedAt: new Date().toISOString(),
            entryCount: this.entries.length,
            rootHash: this.entries.length > 0 ? this.entries[0].entryHash : "empty",
            tipHash: this.entries.length > 0 ? this.entries[this.entries.length - 1].entryHash : "empty",
            entrySummary,
            hasRedactions: this.hasRedactions,
        };
    }
    /**
     * Configure file persistence
     */
    async setFilePath(path) {
        this.filePath = path;
        await mkdir(dirname(path), { recursive: true });
        // Write existing entries if any
        if (this.entries.length > 0) {
            const content = this.entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
            await writeFile(path, content);
        }
    }
    /**
     * Append entry to file
     */
    async appendToFile(entry) {
        if (!this.filePath)
            return;
        await appendFile(this.filePath, JSON.stringify(entry) + "\n");
    }
    /**
     * Export to JSONL format
     */
    toJSONL() {
        return this.entries.map((e) => JSON.stringify(e)).join("\n");
    }
    /**
     * Export entries to JSON
     */
    toJSON() {
        return JSON.stringify(this.entries, null, 2);
    }
}
// ============================================================================
// Logger Factory Functions
// ============================================================================
/**
 * Active loggers by session ID
 */
const activeLoggers = new Map();
/**
 * Create a new transcript logger
 */
export function createLogger(sessionId) {
    const logger = new TranscriptLogger(sessionId);
    activeLoggers.set(logger.getSessionId(), logger);
    return logger;
}
/**
 * Get an existing logger by session ID
 */
export function getLogger(sessionId) {
    return activeLoggers.get(sessionId);
}
/**
 * Remove a logger
 */
export function removeLogger(sessionId) {
    return activeLoggers.delete(sessionId);
}
/**
 * Get all active session IDs
 */
export function getActiveSessions() {
    return [...activeLoggers.keys()];
}
// ============================================================================
// Load from File
// ============================================================================
/**
 * Load a transcript from a JSONL file
 */
export async function loadTranscript(filePath) {
    const content = await readFile(filePath, "utf-8");
    const lines = content.split("\n").filter((l) => l.trim());
    if (lines.length === 0) {
        throw new Error("Empty transcript file");
    }
    const entries = lines.map((line) => JSON.parse(line));
    const sessionId = entries[0].sessionId;
    const logger = new TranscriptLogger(sessionId);
    // Replay entries to rebuild state
    for (const entry of entries) {
        logger.entries.push(entry);
        logger.previousHash = entry.entryHash;
    }
    return logger;
}
/**
 * Verify a transcript file
 */
export async function verifyTranscriptFile(filePath) {
    const logger = await loadTranscript(filePath);
    return logger.verify();
}
//# sourceMappingURL=logger.js.map