/**
 * JSONL file operations service
 * Handles reading and writing JSONL (JSON Lines) session files for improved performance
 */
import { appendFile, readFile, writeFile, stat, mkdir } from "fs/promises";
import { dirname } from "path";
import { getLastLine } from "../utils/fileUtils.js";
/**
 * JSONL handler class for message persistence operations
 */
export class JsonlHandler {
    constructor() {
        this.defaultWriteOptions = {
            atomic: true,
        };
    }
    /**
     * Create a new session file (simplified - no metadata header)
     */
    async createSession(filePath) {
        // Ensure directory exists
        await this.ensureDirectory(dirname(filePath));
        // Create empty file (no metadata line needed)
        await writeFile(filePath, "", "utf8");
    }
    /**
     * Append a single message to JSONL file
     */
    async appendMessage(filePath, message) {
        return this.appendMessages(filePath, [message]);
    }
    /**
     * Append multiple messages to JSONL file
     */
    async appendMessages(filePath, messages) {
        if (messages.length === 0) {
            return;
        }
        // Convert to SessionMessage format with timestamps
        const sessionMessages = messages.map((message) => ({
            ...message,
            timestamp: new Date().toISOString(),
        }));
        return this.append(filePath, sessionMessages);
    }
    /**
     * Append messages to JSONL file
     */
    async append(filePath, messages, options) {
        if (messages.length === 0) {
            return;
        }
        const opts = { ...this.defaultWriteOptions, ...options };
        // Validate messages (always enabled for data integrity)
        this.validateMessages(messages);
        // Ensure directory exists
        await this.ensureDirectory(dirname(filePath));
        // Convert messages to JSONL lines (always compact JSON)
        const lines = messages.map((message) => {
            const { timestamp: existingTimestamp, ...messageWithoutTimestamp } = message;
            const messageWithTimestamp = {
                timestamp: existingTimestamp || new Date().toISOString(),
                ...messageWithoutTimestamp,
            };
            return JSON.stringify(messageWithTimestamp);
        });
        const content = lines.join("\n") + "\n";
        if (opts.atomic) {
            // Write to temp file first, then rename
            const tempPath = `${filePath}.tmp`;
            await writeFile(tempPath, content, "utf8");
            // Atomic rename
            const { rename } = await import("fs/promises");
            await rename(tempPath, filePath);
        }
        else {
            // Direct append
            await appendFile(filePath, content, "utf8");
        }
    }
    /**
     * Read all messages from JSONL file (simplified - no metadata handling)
     */
    async read(filePath) {
        try {
            const content = await readFile(filePath, "utf8");
            const lines = content
                .split(/\r?\n/)
                .map((line) => line.trim())
                .filter((line) => line.length > 0);
            if (lines.length === 0) {
                return [];
            }
            const allMessages = [];
            // Parse all messages (no metadata line to skip)
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                try {
                    const message = JSON.parse(line);
                    if (message.timestamp)
                        allMessages.push(message);
                }
                catch (error) {
                    // Throw error for invalid JSON lines with line number
                    throw new Error(`Invalid JSON at line ${i + 1}: ${error}`);
                }
            }
            return allMessages;
        }
        catch (error) {
            if (error.code === "ENOENT") {
                return [];
            }
            throw new Error(`Failed to read JSONL file "${filePath}": ${error}`);
        }
    }
    /**
     * Get the last message from JSONL file using efficient file reading (simplified)
     */
    async getLastMessage(filePath) {
        try {
            // First check if file exists
            try {
                await stat(filePath);
            }
            catch (err) {
                if (err.code === "ENOENT") {
                    return null;
                }
                throw err;
            }
            // Use our elegant utility to get the last line
            const lastLine = await getLastLine(filePath);
            if (!lastLine) {
                return null;
            }
            try {
                const parsed = JSON.parse(lastLine);
                return parsed;
            }
            catch (error) {
                throw new Error(`Invalid JSON in last line of "${filePath}": ${error}`);
            }
        }
        catch (error) {
            throw new Error(`Failed to get last message from "${filePath}": ${error}`);
        }
    }
    /**
     * Validate messages before writing
     */
    validateMessages(messages) {
        for (let i = 0; i < messages.length; i++) {
            const message = messages[i];
            if (!message.id) {
                throw new Error(`Message at index ${i} is missing required field: id`);
            }
            if (!message.role) {
                throw new Error(`Message at index ${i} is missing required field: role`);
            }
            if (!message.blocks) {
                throw new Error(`Message at index ${i} is missing required field: blocks`);
            }
            if (!Array.isArray(message.blocks)) {
                throw new Error(`Message at index ${i} has invalid blocks field: must be an array`);
            }
        }
    }
    /**
     * Ensure directory exists for the given file path
     */
    async ensureDirectory(dirPath) {
        try {
            await mkdir(dirPath, { recursive: true });
        }
        catch (error) {
            const err = error;
            if (err.code !== "EEXIST") {
                throw error;
            }
        }
    }
    /**
     * Parse session metadata from filename
     * @param filePath - Path to the session file
     * @returns Parsed session filename metadata
     */
    parseSessionFilename(filePath) {
        // Extract filename from path
        const filename = filePath.split("/").pop() || "";
        // Check if it's a subagent session
        const subagentMatch = filename.match(/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
        if (subagentMatch) {
            return {
                sessionId: subagentMatch[1],
                sessionType: "subagent",
            };
        }
        // Check if it's a main session
        const mainMatch = filename.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
        if (mainMatch) {
            return {
                sessionId: mainMatch[1],
                sessionType: "main",
            };
        }
        throw new Error(`Invalid session filename format: ${filename}`);
    }
    /**
     * Validate filename format
     * @param filename - Filename to validate
     * @returns True if valid, false otherwise
     */
    isValidSessionFilename(filename) {
        // UUID validation patterns
        const uuidPattern = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/;
        const subagentPattern = /^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/;
        return uuidPattern.test(filename) || subagentPattern.test(filename);
    }
    /**
     * Generate simple filename for sessions
     * @param sessionId - UUID session identifier
     * @param sessionType - Type of session ("main" or "subagent")
     * @returns Generated filename
     */
    generateSessionFilename(sessionId, sessionType) {
        // Validate sessionId is a valid UUID
        const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
        if (!uuidPattern.test(sessionId)) {
            throw new Error(`Invalid session ID format: ${sessionId}`);
        }
        if (sessionType === "subagent") {
            return `subagent-${sessionId}.jsonl`;
        }
        else {
            return `${sessionId}.jsonl`;
        }
    }
}
