/**
 * Loop Task Manager
 * CCW Loop System - JSONL task persistence layer for v2 loops
 * Reference: .workflow/.scratchpad/loop-system-complete-design-20260121.md section 4.2
 *
 * Storage format: .workflow/.loop/{loopId}/tasks.jsonl
 * JSONL format: one JSON object per line for efficient append-only operations
 */
import { readFile, writeFile, mkdir, copyFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import { randomBytes } from 'crypto';
/**
 * Task Storage Manager
 * Handles JSONL persistence for loop tasks
 */
export class TaskStorageManager {
    baseDir;
    constructor(workflowDir) {
        // Task files stored in .workflow/.loop/{loopId}/
        this.baseDir = join(workflowDir, '.workflow', '.loop');
    }
    /**
     * Add a new task to the loop
     */
    async addTask(loopId, request) {
        await this.ensureLoopDir(loopId);
        // Read existing tasks to determine next order
        const existingTasks = await this.readTasks(loopId);
        const nextOrder = existingTasks.length > 0
            ? Math.max(...existingTasks.map(t => t.order)) + 1
            : 0;
        const task = {
            task_id: this.generateTaskId(),
            description: request.description,
            tool: request.tool,
            mode: request.mode,
            prompt_template: request.prompt_template,
            order: nextOrder,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
            command: request.command,
            on_error: request.on_error
        };
        await this.appendTask(loopId, task);
        return task;
    }
    /**
     * Get all tasks for a loop
     */
    async getTasks(loopId) {
        return this.readTasks(loopId);
    }
    /**
     * Get single task by ID
     */
    async getTask(loopId, taskId) {
        const tasks = await this.readTasks(loopId);
        return tasks.find(t => t.task_id === taskId) || null;
    }
    /**
     * Update existing task
     */
    async updateTask(loopId, taskId, updates) {
        const tasks = await this.readTasks(loopId);
        const taskIndex = tasks.findIndex(t => t.task_id === taskId);
        if (taskIndex === -1) {
            return null;
        }
        const task = tasks[taskIndex];
        const updatedTask = {
            ...task,
            description: updates.description ?? task.description,
            tool: updates.tool ?? task.tool,
            mode: updates.mode ?? task.mode,
            prompt_template: updates.prompt_template ?? task.prompt_template,
            command: updates.command ?? task.command,
            on_error: updates.on_error ?? task.on_error,
            updated_at: new Date().toISOString()
        };
        tasks[taskIndex] = updatedTask;
        await this.writeTasks(loopId, tasks);
        return updatedTask;
    }
    /**
     * Delete task and reorder remaining tasks
     */
    async deleteTask(loopId, taskId) {
        const tasks = await this.readTasks(loopId);
        const filteredTasks = tasks.filter(t => t.task_id !== taskId);
        if (filteredTasks.length === tasks.length) {
            return false; // Task not found
        }
        // Reorder remaining tasks
        const reorderedTasks = this.reorderTasksByOrder(filteredTasks);
        await this.writeTasks(loopId, reorderedTasks);
        return true;
    }
    /**
     * Reorder tasks based on provided task ID sequence
     */
    async reorderTasks(loopId, request) {
        const tasks = await this.readTasks(loopId);
        const taskMap = new Map(tasks.map(t => [t.task_id, t]));
        // Verify all provided task IDs exist
        for (const taskId of request.ordered_task_ids) {
            if (!taskMap.has(taskId)) {
                throw new Error(`Task not found: ${taskId}`);
            }
        }
        // Reorder tasks and update order indices
        const reorderedTasks = [];
        for (let i = 0; i < request.ordered_task_ids.length; i++) {
            const task = taskMap.get(request.ordered_task_ids[i]);
            reorderedTasks.push({
                ...task,
                order: i,
                updated_at: new Date().toISOString()
            });
        }
        // Add any tasks not in the reorder list (shouldn't happen normally)
        for (const task of tasks) {
            if (!request.ordered_task_ids.includes(task.task_id)) {
                reorderedTasks.push({
                    ...task,
                    order: reorderedTasks.length,
                    updated_at: new Date().toISOString()
                });
            }
        }
        await this.writeTasks(loopId, reorderedTasks);
        return reorderedTasks;
    }
    /**
     * Delete all tasks for a loop
     */
    async deleteAllTasks(loopId) {
        const tasksPath = this.getTasksPath(loopId);
        if (existsSync(tasksPath)) {
            const { unlink } = await import('fs/promises');
            await unlink(tasksPath).catch(() => { });
        }
        // Also delete backup
        const backupPath = `${tasksPath}.backup`;
        if (existsSync(backupPath)) {
            const { unlink } = await import('fs/promises');
            await unlink(backupPath).catch(() => { });
        }
    }
    /**
     * Read tasks with recovery from backup
     */
    async readTasksWithRecovery(loopId) {
        try {
            return await this.readTasks(loopId);
        }
        catch (error) {
            console.warn(`Tasks file corrupted, attempting recovery for ${loopId}...`);
            const backupPath = `${this.getTasksPath(loopId)}.backup`;
            if (existsSync(backupPath)) {
                const content = await readFile(backupPath, 'utf-8');
                const tasks = this.parseTasksJsonl(content);
                // Restore from backup
                await this.writeTasks(loopId, tasks);
                return tasks;
            }
            throw error;
        }
    }
    /**
     * Get tasks file path
     */
    getTasksPath(loopId) {
        return join(this.baseDir, this.sanitizeLoopId(loopId), 'tasks.jsonl');
    }
    /**
     * Read tasks from JSONL file
     */
    async readTasks(loopId) {
        const filePath = this.getTasksPath(loopId);
        if (!existsSync(filePath)) {
            return [];
        }
        const content = await readFile(filePath, 'utf-8');
        return this.parseTasksJsonl(content);
    }
    /**
     * Parse JSONL content into tasks array
     */
    parseTasksJsonl(content) {
        const tasks = [];
        const lines = content.split('\n').filter(line => line.trim().length > 0);
        for (const line of lines) {
            try {
                const task = JSON.parse(line);
                tasks.push(task);
            }
            catch (error) {
                console.error('Failed to parse task line:', error);
            }
        }
        return tasks;
    }
    /**
     * Write tasks array to JSONL file
     */
    async writeTasks(loopId, tasks) {
        await this.ensureLoopDir(loopId);
        const filePath = this.getTasksPath(loopId);
        // Create backup if file exists
        if (existsSync(filePath)) {
            const backupPath = `${filePath}.backup`;
            await copyFile(filePath, backupPath).catch(() => { });
        }
        // Write each task as a JSON line
        const jsonlContent = tasks.map(t => JSON.stringify(t)).join('\n');
        await writeFile(filePath, jsonlContent, 'utf-8');
    }
    /**
     * Append single task to JSONL file
     */
    async appendTask(loopId, task) {
        await this.ensureLoopDir(loopId);
        const filePath = this.getTasksPath(loopId);
        // Create backup if file exists
        if (existsSync(filePath)) {
            const backupPath = `${filePath}.backup`;
            await copyFile(filePath, backupPath).catch(() => { });
        }
        // Append task as new line
        const line = JSON.stringify(task) + '\n';
        await writeFile(filePath, line, { flag: 'a' });
    }
    /**
     * Ensure loop directory exists
     */
    async ensureLoopDir(loopId) {
        const dirPath = join(this.baseDir, this.sanitizeLoopId(loopId));
        if (!existsSync(dirPath)) {
            await mkdir(dirPath, { recursive: true });
        }
    }
    /**
     * Generate unique task ID
     */
    generateTaskId() {
        const timestamp = Date.now();
        const random = randomBytes(4).toString('hex');
        return `task-${timestamp}-${random}`;
    }
    /**
     * Sanitize loop ID for filesystem usage
     */
    sanitizeLoopId(loopId) {
        // Remove any path traversal characters
        return loopId.replace(/[\/\\]/g, '-').replace(/\.\./g, '').replace(/^\./, '');
    }
    /**
     * Reorder tasks array by updating order indices sequentially
     */
    reorderTasksByOrder(tasks) {
        return tasks
            .sort((a, b) => a.order - b.order)
            .map((task, index) => ({
            ...task,
            order: index
        }));
    }
}
//# sourceMappingURL=loop-task-manager.js.map