/**
 * File-Based Storage Implementation
 *
 * Implements the IPersistence interface using a file-based approach with
 * directory-per-conversation structure. Provides transparent, human-readable
 * persistence.
 *
 * Directory Structure:
 *   .vibe/conversations/{conversationId}/
 *     state.json          - ConversationState data
 *     interactions.jsonl  - Append-only interaction logs (one JSON per line)
 */
import { promises as fs } from 'node:fs';
import { join, dirname } from 'node:path';
import { createLogger } from './logger.js';
const logger = createLogger('FileStorage');
/**
 * File-based persistence implementation
 */
export class FileStorage {
    conversationsDir;
    basePath;
    /**
     * @param basePath - Path to the storage location (e.g., .vibe/conversations)
     */
    constructor(basePath) {
        this.basePath = basePath;
        this.conversationsDir = join(dirname(basePath), 'conversations');
    }
    /**
     * Initialize file-based storage
     * Creates the conversations directory if it doesn't exist
     */
    async initialize() {
        try {
            // Create conversations directory
            await fs.mkdir(this.conversationsDir, { recursive: true });
            logger.debug('FileStorage initialized', {
                conversationsDir: this.conversationsDir,
            });
        }
        catch (error) {
            logger.error('Failed to initialize FileStorage', error);
            throw error;
        }
    }
    /**
     * Get the directory path for a conversation
     */
    getConversationDir(conversationId) {
        return join(this.conversationsDir, conversationId);
    }
    /**
     * Get the state file path for a conversation
     */
    getStateFilePath(conversationId) {
        return join(this.getConversationDir(conversationId), 'state.json');
    }
    /**
     * Get the interactions file path for a conversation
     */
    getInteractionsFilePath(conversationId) {
        return join(this.getConversationDir(conversationId), 'interactions.jsonl');
    }
    /**
     * Write data atomically using temp file + rename
     * Uses unique temp file names to prevent concurrent write collisions
     */
    async writeAtomic(filePath, data) {
        // Use timestamp + random suffix to ensure unique temp file names for concurrent operations
        const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
        const tempPath = `${filePath}.tmp.${uniqueSuffix}`;
        try {
            await fs.writeFile(tempPath, data, 'utf-8');
            await fs.rename(tempPath, filePath);
        }
        catch (error) {
            // Clean up temp file if it exists
            try {
                await fs.unlink(tempPath);
            }
            catch {
                // Ignore cleanup errors
            }
            throw error;
        }
    }
    /**
     * Save or update a conversation state
     */
    async saveConversationState(state) {
        const conversationDir = this.getConversationDir(state.conversationId);
        const stateFilePath = this.getStateFilePath(state.conversationId);
        try {
            // Ensure conversation directory exists
            await fs.mkdir(conversationDir, { recursive: true });
            // Write state atomically
            const stateJson = JSON.stringify(state, null, 2);
            await this.writeAtomic(stateFilePath, stateJson);
            logger.debug('Conversation state saved', {
                conversationId: state.conversationId,
                currentPhase: state.currentPhase,
            });
        }
        catch (error) {
            logger.error('Failed to save conversation state', error, {
                conversationId: state.conversationId,
            });
            throw error;
        }
    }
    /**
     * Get a conversation state by ID
     * Returns null if not found or corrupted
     */
    async getConversationState(conversationId) {
        const stateFilePath = this.getStateFilePath(conversationId);
        try {
            const stateJson = await fs.readFile(stateFilePath, 'utf-8');
            const state = JSON.parse(stateJson);
            return state;
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                // File doesn't exist - return null (expected for non-existent conversations)
                return null;
            }
            // JSON parse error or other error - log warning and return null
            logger.warn('Failed to read conversation state, returning null', {
                conversationId,
                error: error.message,
            });
            return null;
        }
    }
    /**
     * Get all conversation states
     * Skips corrupted state files
     */
    async getAllConversationStates() {
        try {
            const entries = await fs.readdir(this.conversationsDir, {
                withFileTypes: true,
            });
            const states = [];
            for (const entry of entries) {
                if (entry.isDirectory()) {
                    const conversationId = entry.name;
                    const state = await this.getConversationState(conversationId);
                    if (state) {
                        states.push(state);
                    }
                    // Corrupted states are already logged and skipped by getConversationState
                }
            }
            // Sort by updatedAt DESC (most recent first)
            states.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
            return states;
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                // Conversations directory doesn't exist yet - return empty array
                return [];
            }
            logger.error('Failed to get all conversation states', error);
            throw error;
        }
    }
    /**
     * Delete a conversation state
     * Returns true if deleted, false if not found
     */
    async deleteConversationState(conversationId) {
        const conversationDir = this.getConversationDir(conversationId);
        try {
            await fs.rm(conversationDir, { recursive: true, force: true });
            logger.debug('Conversation state deleted', { conversationId });
            return true;
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                // Directory doesn't exist - return false
                return false;
            }
            logger.error('Failed to delete conversation state', error, {
                conversationId,
            });
            throw error;
        }
    }
    /**
     * Log an interaction
     * Appends to interactions.jsonl file
     */
    async logInteraction(log) {
        const interactionsFilePath = this.getInteractionsFilePath(log.conversationId);
        const conversationDir = this.getConversationDir(log.conversationId);
        try {
            // Ensure conversation directory exists
            await fs.mkdir(conversationDir, { recursive: true });
            // Append to JSONL file (one JSON per line)
            const logLine = JSON.stringify(log) + '\n';
            await fs.appendFile(interactionsFilePath, logLine, 'utf-8');
            logger.debug('Interaction logged', {
                conversationId: log.conversationId,
                toolName: log.toolName,
            });
        }
        catch (error) {
            logger.error('Failed to log interaction', error, {
                conversationId: log.conversationId,
                toolName: log.toolName,
            });
            throw error;
        }
    }
    /**
     * Get interaction logs for a conversation
     * Skips corrupted JSONL lines, returns empty array if file doesn't exist
     */
    async getInteractionLogs(conversationId) {
        const interactionsFilePath = this.getInteractionsFilePath(conversationId);
        try {
            const content = await fs.readFile(interactionsFilePath, 'utf-8');
            const lines = content.split('\n').filter(line => line.trim());
            const logs = [];
            for (const line of lines) {
                try {
                    const log = JSON.parse(line);
                    logs.push(log);
                }
                catch {
                    // Skip corrupted line, log warning
                    logger.warn('Skipping corrupted interaction log line', {
                        conversationId,
                    });
                }
            }
            return logs;
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                // File doesn't exist - return empty array
                return [];
            }
            logger.error('Failed to get interaction logs', error, {
                conversationId,
            });
            throw error;
        }
    }
    /**
     * Get interaction logs by conversation ID (alias for compatibility)
     */
    async getInteractionsByConversationId(conversationId) {
        return this.getInteractionLogs(conversationId);
    }
    /**
     * Soft delete interaction logs
     * Actually performs hard delete (removes interactions.jsonl file)
     */
    async softDeleteInteractionLogs(conversationId) {
        const interactionsFilePath = this.getInteractionsFilePath(conversationId);
        try {
            await fs.unlink(interactionsFilePath);
            logger.debug('Interaction logs deleted', { conversationId });
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                // File doesn't exist - nothing to delete
                return;
            }
            logger.warn('Failed to delete interaction logs', {
                conversationId,
                error: error.message,
            });
        }
    }
    /**
     * Reset conversation state (update timestamp)
     */
    async resetConversationState(conversationId) {
        const state = await this.getConversationState(conversationId);
        if (!state) {
            throw new Error(`Conversation state not found for ID: ${conversationId}`);
        }
        const updatedState = {
            ...state,
            updatedAt: new Date().toISOString(),
        };
        await this.saveConversationState(updatedState);
        logger.debug('Conversation state reset', { conversationId });
    }
    /**
     * Close file storage (no-op for file-based storage)
     */
    async close() {
        logger.debug('FileStorage closed');
    }
}
//# sourceMappingURL=file-storage.js.map