/**
 * RecordingManager - Central manager for recording AI interactions.
 *
 * This class coordinates the recording of socket messages, tool calls,
 * and LLM interactions. It manages recording sessions and provides
 * a unified interface for all recording operations.
 */
import { pick, throttle } from "lodash-es";
import { getErrorMeta, getLogger } from "../../util/logger.js";
import { SessionRecordingStorage } from "./storage/session-recording-storage.js";
/**
 * Central manager for recording AI interactions.
 *
 * Provides session-based recording with support for socket messages,
 * tool executions, and LLM calls. Entries are written to JSONL files
 * in {recordingsDir}/{sessionId}/recording.jsonl.
 *
 * Sessions are tracked in a persistent index at {recordingsDir}/sessions.json.
 *
 * @example
 * ```typescript
 * const recorder = new RecordingManager({
 *   recordingsDir: '/path/to/.superblocks/recordings',
 *   appRootDirPath: '/path/to/app',
 * });
 *
 * // Start recording with optional initial commit ID
 * recorder.start(initialCommitId);
 * recorder.recordSocket({ method: 'aiGenerate', request: {...}, peerId: '123' });
 *
 * // Update commit ID when new commits occur
 * recorder.setCurrentCommitId(newCommitId);
 * ```
 */
/** Throttle interval for rotation checks (ms) */
const ROTATION_CHECK_INTERVAL_MS = 5000;
export class RecordingManager {
    config;
    storage;
    enabled = false;
    currentSessionId = null;
    maxSessionSizeBytes;
    maxSessions;
    maxSnapshotFileSizeBytes;
    /** Throttled rotation check - runs at most every 5 seconds */
    checkAndRotateIfNeeded = throttle(() => {
        void this.doRotationCheck();
    }, ROTATION_CHECK_INTERVAL_MS);
    constructor(config) {
        this.config = config;
        this.storage = new SessionRecordingStorage(config.recordingsDir);
        this.maxSessionSizeBytes = config.maxSessionSizeBytes ?? 512000; // 500KB
        this.maxSessions = config.maxSessions ?? 3;
        this.maxSnapshotFileSizeBytes = config.maxSnapshotFileSizeBytes ?? 1048576; // 1MB
        getLogger().info(`[recording] Using file storage at: ${config.recordingsDir}`);
    }
    /**
     * Enable recording and start a new session.
     *
     * @param initialCommitId - Optional commit ID to set as starting/current commit
     * @returns The session ID for the new session
     */
    start(initialCommitId) {
        this.enabled = true;
        return this.startSession(initialCommitId);
    }
    /**
     * Start a new recording session (internal).
     * Does not enable recording - use start() to enable and start.
     *
     * @param initialCommitId - Optional commit ID to set as starting/current commit
     * @returns The session ID for the new session
     */
    startSession(initialCommitId) {
        this.currentSessionId = crypto.randomUUID();
        const session = {
            id: this.currentSessionId,
            startedAt: Date.now(),
            startingCommitId: initialCommitId,
            currentCommitId: initialCommitId,
        };
        // Add to storage (single source of truth)
        void this.storage.addSession(session);
        // Cleanup old sessions if over limit
        void this.storage.deleteOldestSessions(this.maxSessions);
        getLogger().info(`[recording] Started recording session: ${this.currentSessionId}`, { initialCommitId });
        // Create snapshot async (fire-and-forget, non-blocking)
        void this.createSessionSnapshot(this.currentSessionId);
        return this.currentSessionId;
    }
    /**
     * Create a snapshot of the app directory for a session.
     * Fire-and-forget, does not block recording.
     * Records a meta entry with snapshot info (including any skipped files).
     */
    async createSessionSnapshot(sessionId) {
        try {
            const result = await this.storage.createSnapshot(sessionId, this.config.appRootDirPath, this.maxSnapshotFileSizeBytes);
            getLogger().info(`[recording] Created snapshot for session: ${sessionId}`);
            // Record meta entry with snapshot info
            this.recordMeta({
                skippedFiles: result.skippedFiles.length > 0 ? result.skippedFiles : undefined,
                message: "Session snapshot created",
            });
        }
        catch (error) {
            getLogger().warn(`[recording] Failed to create snapshot for session`, getErrorMeta(error));
        }
    }
    /**
     * Set the current commit ID for the active session.
     * Called when commits occur during the session.
     */
    async setCurrentCommitId(commitId) {
        if (!this.currentSessionId)
            return;
        const session = await this.storage.getSession(this.currentSessionId);
        if (session) {
            session.currentCommitId = commitId;
            await this.storage.updateSession(session);
            getLogger().info(`[recording] Updated currentCommitId`, {
                sessionId: this.currentSessionId,
                commitId,
            });
        }
    }
    /**
     * Check if recording is currently enabled.
     */
    isEnabled() {
        return this.enabled && this.currentSessionId !== null;
    }
    /**
     * Perform the actual rotation check.
     * Called by the throttled checkAndRotateIfNeeded.
     */
    async doRotationCheck() {
        if (!this.currentSessionId)
            return;
        const size = await this.storage.getSessionFileSize(this.currentSessionId);
        if (size >= this.maxSessionSizeBytes) {
            getLogger().info(`[recording] Session file size ${size} exceeds limit ${this.maxSessionSizeBytes}, rotating`);
            // Get current commit ID to pass to new session
            const session = await this.storage.getSession(this.currentSessionId);
            const currentCommitId = session?.currentCommitId;
            // Start new session (this also handles cleanup)
            this.startSession(currentCommitId);
        }
    }
    /**
     * Get the current session ID.
     */
    getCurrentSessionId() {
        return this.currentSessionId;
    }
    /**
     * List all recorded sessions from the persistent index.
     *
     * @returns Promise resolving to array of sessions (oldest first)
     */
    async listSessions() {
        return this.storage.listSessions();
    }
    /**
     * Record a socket message.
     *
     * By default, only records method name and peerId.
     * For specific methods (like aiGenerate), also records cleaned request data.
     */
    recordSocket(data) {
        if (!this.isEnabled())
            return;
        // Skip noisy methods entirely
        if (data.method === "editor.ping")
            return;
        let cleanedData;
        try {
            // Method-specific data cleaning
            switch (data.method) {
                case "aiGenerate":
                    // Record cleaned request data for aiGenerate
                    cleanedData = {
                        method: data.method,
                        peerId: data.peerId,
                        request: pick(data.request, [
                            "prompt",
                            "promptContext",
                            "planContext",
                            "mockGeneration",
                            "llmConfig",
                            "mode",
                            "responseMetadata",
                        ]),
                    };
                    break;
                // Record full request for mutation methods (useful for replay/debugging)
                case "aiRestoreToCommit":
                case "apiUpdate":
                case "apiDelete":
                case "renameApi":
                case "renameElement":
                    cleanedData = {
                        method: data.method,
                        peerId: data.peerId,
                        request: data.request,
                    };
                    break;
                default:
                    // For all other methods, record just method name and peerId
                    cleanedData = {
                        method: data.method,
                        peerId: data.peerId,
                        request: `[Truncated]`,
                    };
            }
        }
        catch {
            cleanedData = {
                message: "Error cleaning this record",
            };
        }
        const entry = {
            id: crypto.randomUUID(),
            timestamp: Date.now(),
            type: "socket",
            data: cleanedData,
        };
        this.storage.add(this.currentSessionId, entry);
        this.checkAndRotateIfNeeded();
    }
    /**
     * Record a tool call.
     */
    recordTool(data) {
        if (!this.isEnabled())
            return;
        const entry = {
            id: crypto.randomUUID(),
            timestamp: Date.now(),
            type: "tool",
            data,
        };
        this.storage.add(this.currentSessionId, entry);
        this.checkAndRotateIfNeeded();
    }
    /**
     * Record an LLM call.
     */
    recordLLM(data) {
        if (!this.isEnabled())
            return;
        let cleanedData = {};
        try {
            cleanedData = {
                request: {
                    model: data.request.model,
                    messages: data.request.messages.map((message, idx) => {
                        if (idx < data.request.messages.length - 1) {
                            return `[Truncated]`;
                        }
                        return message;
                    }),
                    tools: data.request.tools?.map((tool) => {
                        return typeof tool === "object" && tool != null && "name" in tool
                            ? `[Truncated] ${tool.name}`
                            : "[Truncated] Tool";
                    }),
                    system: `[Truncated]`,
                },
                response: data.response,
            };
        }
        catch {
            cleanedData = {
                message: "Error cleaning this record",
            };
        }
        const entry = {
            id: crypto.randomUUID(),
            timestamp: Date.now(),
            type: "llm",
            data: cleanedData,
        };
        this.storage.add(this.currentSessionId, entry);
        this.checkAndRotateIfNeeded();
    }
    /**
     * Record a meta entry (session metadata like snapshot info).
     */
    recordMeta(data) {
        if (!this.isEnabled())
            return;
        const entry = {
            id: crypto.randomUUID(),
            timestamp: Date.now(),
            type: "meta",
            data,
        };
        this.storage.add(this.currentSessionId, entry);
    }
    /**
     * Clear all recordings, optionally for a specific session.
     */
    async clear(sessionId) {
        await this.storage.clear(sessionId);
    }
}
//# sourceMappingURL=recording-manager.js.map