import { forbidden, invalidRequest, notFound } from '../../../types-global/errors.js';
import { idGenerator } from '../../../utils/security/idGenerator.js';
import { isTerminal } from './taskTypes.js';
/**
 * A TaskStore implementation backed by StorageService for persistent task storage.
 *
 * Unlike InMemoryTaskStore, this implementation:
 * - Persists tasks across server restarts
 * - Works with any configured storage backend (filesystem, Supabase, SurrealDB, etc.)
 * - Supports TTL via storage provider options
 *
 * @example
 * ```typescript
 * const taskStore = new StorageBackedTaskStore(storageService, {
 *   tenantId: 'my-app-tasks',
 *   defaultTtl: 3600000, // 1 hour
 * });
 * ```
 *
 * @experimental
 */
export class StorageBackedTaskStore {
    storage;
    tenantId;
    keyPrefix;
    defaultTtl;
    pageSize;
    constructor(storage, options = {}) {
        this.storage = storage;
        this.tenantId = options.tenantId ?? 'system-tasks';
        this.keyPrefix = options.keyPrefix ?? 'tasks';
        this.defaultTtl = options.defaultTtl ?? null;
        this.pageSize = options.pageSize ?? 10;
    }
    /**
     * Creates a request context for storage operations.
     */
    createContext(operation) {
        return {
            operation: `StorageBackedTaskStore.${operation}`,
            requestId: idGenerator.generate('req'),
            timestamp: new Date().toISOString(),
            tenantId: this.tenantId,
        };
    }
    /**
     * Generates the storage key for a task.
     */
    getTaskKey(taskId) {
        return `${this.keyPrefix}/${taskId}`;
    }
    /**
     * Validates that the caller's sessionId matches the stored task's sessionId.
     * Tasks created without a sessionId (legacy/pre-ownership) are accessible by any session.
     * @throws {McpError} Forbidden if sessionId mismatch
     */
    assertOwnership(stored, callerSessionId, taskId) {
        if (!stored.sessionId)
            return;
        if (stored.sessionId !== callerSessionId) {
            throw forbidden(`Access denied to task ${taskId}`);
        }
    }
    /**
     * Generates a unique task ID.
     * Uses the framework's idGenerator for consistent ID format.
     */
    generateTaskId() {
        // Generate a longer ID for global uniqueness (16 chars)
        return idGenerator.generate('task', { length: 16 });
    }
    async createTask(taskParams, requestId, request, sessionId) {
        const context = this.createContext('createTask');
        const taskId = this.generateTaskId();
        const actualTtl = taskParams.ttl ?? this.defaultTtl;
        const createdAt = new Date().toISOString();
        const task = {
            taskId,
            status: 'working',
            ttl: actualTtl,
            createdAt,
            lastUpdatedAt: createdAt,
            pollInterval: taskParams.pollInterval ?? 1000,
        };
        const storedTask = {
            task,
            request,
            requestId,
        };
        // Bind session ownership if provided
        if (sessionId) {
            storedTask.sessionId = sessionId;
        }
        // Store with TTL if specified (convert ms to seconds for StorageService)
        await this.storage.set(this.getTaskKey(taskId), storedTask, context, actualTtl ? { ttl: Math.ceil(actualTtl / 1000) } : undefined);
        return task;
    }
    async getTask(taskId, sessionId) {
        const context = this.createContext('getTask');
        const stored = await this.storage.get(this.getTaskKey(taskId), context);
        if (!stored)
            return null;
        this.assertOwnership(stored, sessionId, taskId);
        return { ...stored.task };
    }
    async storeTaskResult(taskId, status, result, sessionId) {
        const context = this.createContext('storeTaskResult');
        const key = this.getTaskKey(taskId);
        const stored = await this.storage.get(key, context);
        if (!stored) {
            throw notFound(`Task with ID ${taskId} not found`);
        }
        this.assertOwnership(stored, sessionId, taskId);
        // Don't allow storing results for tasks already in terminal state
        if (isTerminal(stored.task.status)) {
            throw invalidRequest(`Cannot store result for task ${taskId} in terminal status '${stored.task.status}'. Task results can only be stored once.`);
        }
        stored.result = result;
        stored.task.status = status;
        stored.task.lastUpdatedAt = new Date().toISOString();
        // Re-store with TTL reset (if ttl is set, convert ms to seconds)
        await this.storage.set(key, stored, context, stored.task.ttl ? { ttl: Math.ceil(stored.task.ttl / 1000) } : undefined);
    }
    async getTaskResult(taskId, sessionId) {
        const context = this.createContext('getTaskResult');
        const stored = await this.storage.get(this.getTaskKey(taskId), context);
        if (!stored) {
            throw notFound(`Task with ID ${taskId} not found`);
        }
        this.assertOwnership(stored, sessionId, taskId);
        if (!stored.result) {
            throw invalidRequest(`Task ${taskId} has no result stored`);
        }
        return stored.result;
    }
    async updateTaskStatus(taskId, status, statusMessage, sessionId) {
        const context = this.createContext('updateTaskStatus');
        const key = this.getTaskKey(taskId);
        const stored = await this.storage.get(key, context);
        if (!stored) {
            throw notFound(`Task with ID ${taskId} not found`);
        }
        this.assertOwnership(stored, sessionId, taskId);
        // Don't allow transitions from terminal states
        if (isTerminal(stored.task.status)) {
            throw invalidRequest(`Cannot update task ${taskId} from terminal status '${stored.task.status}' to '${status}'. Terminal states (completed, failed, cancelled) cannot transition to other states.`);
        }
        stored.task.status = status;
        if (statusMessage) {
            stored.task.statusMessage = statusMessage;
        }
        stored.task.lastUpdatedAt = new Date().toISOString();
        // Re-store, reset TTL if transitioning to terminal state (convert ms to seconds)
        const shouldResetTtl = isTerminal(status) && stored.task.ttl;
        await this.storage.set(key, stored, context, shouldResetTtl ? { ttl: Math.ceil(stored.task.ttl / 1000) } : undefined);
    }
    async listTasks(cursor, sessionId) {
        const context = this.createContext('listTasks');
        // List all task keys with pagination
        const listResult = await this.storage.list(this.keyPrefix, context, cursor ? { cursor, limit: this.pageSize } : { limit: this.pageSize });
        // Fetch all tasks in parallel, filtering by session ownership
        const taskPromises = listResult.keys.map(async (key) => {
            const stored = await this.storage.get(key, context);
            if (!stored)
                return null;
            // Filter: hide tasks whose sessionId doesn't match the caller's.
            // Consistent with assertOwnership: session-bound tasks are only
            // visible to the session that created them.
            if (stored.sessionId && stored.sessionId !== sessionId) {
                return null;
            }
            return { ...stored.task };
        });
        const tasksOrNull = await Promise.all(taskPromises);
        const tasks = tasksOrNull.filter((t) => t !== null);
        // Handle exactOptionalPropertyTypes - only include nextCursor if defined
        if (listResult.nextCursor !== undefined) {
            return { tasks, nextCursor: listResult.nextCursor };
        }
        return { tasks };
    }
    /**
     * Deletes a task from storage.
     * Useful for manual cleanup or testing.
     *
     * @param taskId - The task ID to delete
     */
    async deleteTask(taskId) {
        const context = this.createContext('deleteTask');
        await this.storage.delete(this.getTaskKey(taskId), context);
    }
    /**
     * Clears all tasks from storage.
     * Useful for testing or administrative purposes.
     *
     * WARNING: This deletes ALL tasks for the configured tenant.
     */
    async clearAllTasks() {
        const context = this.createContext('clearAllTasks');
        // List and delete all tasks
        let cursor;
        do {
            const result = await this.storage.list(this.keyPrefix, context, cursor ? { cursor, limit: 100 } : { limit: 100 });
            await Promise.all(result.keys.map((key) => this.storage.delete(key, context)));
            cursor = result.nextCursor;
        } while (cursor);
    }
}
//# sourceMappingURL=storageBackedTaskStore.js.map