/**
 * @fileoverview A filesystem-based storage provider.
 * Persists data to the local filesystem in a specified directory.
 * Each key-value pair is stored as a separate JSON file.
 *
 * Performance note: List operations with TTL filtering can be slow on large datasets
 * as each file must be read and parsed to check expiration. Consider implementing
 * periodic cleanup jobs for production use with large key counts.
 *
 * @module src/storage/providers/fileSystem/fileSystemProvider
 */
import { existsSync, mkdirSync } from 'node:fs';
import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { decodeCursor, encodeCursor, validateTenantId } from '../../../storage/core/storageValidation.js';
import { configurationError, JsonRpcErrorCode, McpError, validationError, } from '../../../types-global/errors.js';
import { ErrorHandler } from '../../../utils/internal/error-handler/errorHandler.js';
import { sanitization } from '../../../utils/security/sanitization.js';
import { isErrorWithCode } from '../../../utils/types/guards.js';
const DEFAULT_LIST_LIMIT = 1000;
const FILE_ENVELOPE_VERSION = 1;
export class FileSystemProvider {
    storagePath;
    constructor(storagePath) {
        if (!storagePath) {
            throw configurationError('FileSystemProvider requires a valid storagePath.');
        }
        this.storagePath = path.resolve(storagePath);
        if (!existsSync(this.storagePath)) {
            mkdirSync(this.storagePath, { recursive: true });
        }
    }
    getTenantPath(tenantId, context) {
        // Defense-in-depth: re-validate at the provider boundary. The upstream
        // StorageService gate also runs this check; mirroring it here protects
        // callers that construct the provider directly (custom providers,
        // tests, the add-provider skill's reference path).
        validateTenantId(tenantId, context);
        const sanitizedTenantId = sanitization.sanitizePath(tenantId, {
            toPosix: true,
        }).sanitizedPath;
        if (sanitizedTenantId.includes('/') || sanitizedTenantId.includes('..')) {
            throw validationError('Invalid tenantId contains path characters.');
        }
        const tenantPath = path.join(this.storagePath, sanitizedTenantId);
        if (!existsSync(tenantPath)) {
            mkdirSync(tenantPath, { recursive: true });
        }
        return tenantPath;
    }
    getFilePath(tenantId, key, context) {
        const tenantPath = this.getTenantPath(tenantId, context);
        const sanitizedKey = sanitization.sanitizePath(key, {
            rootDir: tenantPath,
            toPosix: true,
        }).sanitizedPath;
        const filePath = path.join(tenantPath, sanitizedKey);
        if (!path.resolve(filePath).startsWith(path.resolve(tenantPath))) {
            throw validationError('Invalid key results in path traversal attempt.');
        }
        return filePath;
    }
    buildEnvelope(value, options) {
        // Fix: Check for undefined instead of truthy to handle ttl=0 correctly
        const expiresAt = options?.ttl !== undefined ? Date.now() + options.ttl * 1000 : undefined;
        return {
            __mcp: {
                v: FILE_ENVELOPE_VERSION,
                ...(expiresAt !== undefined ? { expiresAt } : {}),
            },
            value,
        };
    }
    async parseAndValidate(raw, tenantId, key, filePath, context) {
        try {
            const parsed = JSON.parse(raw);
            // Envelope-aware parsing
            if (parsed && typeof parsed === 'object' && '__mcp' in parsed) {
                const env = parsed;
                const expiresAt = env.__mcp?.expiresAt;
                if (expiresAt && Date.now() > expiresAt) {
                    // Expired: best-effort delete and return null
                    try {
                        await rm(filePath);
                    }
                    catch {
                        // ignore
                    }
                    return null;
                }
                return env.value;
            }
            // Legacy: return parsed directly
            return parsed;
        }
        catch (error) {
            throw new McpError(JsonRpcErrorCode.SerializationError, `Failed to parse stored JSON for key "${key}" (tenant "${tenantId}").`, { ...context, error });
        }
    }
    async get(tenantId, key, context) {
        const filePath = this.getFilePath(tenantId, key, context);
        return await ErrorHandler.tryCatch(async () => {
            try {
                const data = await readFile(filePath, 'utf-8');
                return this.parseAndValidate(data, tenantId, key, filePath, context);
            }
            catch (error) {
                if (isErrorWithCode(error) && error.code === 'ENOENT') {
                    return null; // File not found
                }
                throw error; // Re-throw other errors
            }
        }, {
            operation: 'FileSystemProvider.get',
            context,
            input: { tenantId, key },
        });
    }
    async set(tenantId, key, value, context, options) {
        const filePath = this.getFilePath(tenantId, key, context);
        return await ErrorHandler.tryCatch(async () => {
            const envelope = this.buildEnvelope(value, options);
            const content = JSON.stringify(envelope, null, 2);
            mkdirSync(path.dirname(filePath), { recursive: true });
            await writeFile(filePath, content, 'utf-8');
        }, {
            operation: 'FileSystemProvider.set',
            context,
            input: { tenantId, key },
        });
    }
    async delete(tenantId, key, context) {
        const filePath = this.getFilePath(tenantId, key, context);
        return await ErrorHandler.tryCatch(async () => {
            try {
                await rm(filePath);
                return true;
            }
            catch (error) {
                if (isErrorWithCode(error) && error.code === 'ENOENT') {
                    return false; // File didn't exist
                }
                throw error;
            }
        }, {
            operation: 'FileSystemProvider.delete',
            context,
            input: { tenantId, key },
        });
    }
    async listFilesRecursively(dir, baseDir) {
        const entries = await readdir(dir, { withFileTypes: true });
        const results = [];
        for (const entry of entries) {
            const fullPath = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                results.push(...(await this.listFilesRecursively(fullPath, baseDir)));
            }
            else if (entry.isFile()) {
                const rel = path.relative(baseDir, fullPath);
                // Normalize to POSIX-style keys for consistency
                results.push(rel.split(path.sep).join('/'));
            }
        }
        return results;
    }
    async list(tenantId, prefix, context, options) {
        return await ErrorHandler.tryCatch(async () => {
            const tenantPath = this.getTenantPath(tenantId, context);
            const allKeys = await this.listFilesRecursively(tenantPath, tenantPath);
            const candidateKeys = allKeys.filter((k) => k.startsWith(prefix));
            // TTL-aware filtering: reads each file to check expiration.
            // Retains parsed values to populate ListResult.values, avoiding
            // a redundant getMany() in ContextState.list().
            const validKeys = [];
            const validValues = new Map();
            for (const k of candidateKeys) {
                const filePath = this.getFilePath(tenantId, k, context);
                try {
                    const raw = await readFile(filePath, 'utf-8');
                    const value = await this.parseAndValidate(raw, tenantId, k, filePath, context);
                    if (value !== null) {
                        validKeys.push(k);
                        validValues.set(k, value);
                    }
                }
                catch (_e) { }
            }
            // Sort for consistent pagination
            validKeys.sort();
            // Apply pagination with opaque cursors
            const limit = options?.limit ?? DEFAULT_LIST_LIMIT;
            let startIndex = 0;
            if (options?.cursor) {
                // Decode and validate cursor
                const lastKey = decodeCursor(options.cursor, tenantId, context);
                const cursorIndex = validKeys.indexOf(lastKey);
                if (cursorIndex !== -1) {
                    startIndex = cursorIndex + 1;
                }
                else {
                    // Key was deleted between pages; resume from the next key after it
                    const insertionPoint = validKeys.findIndex((k) => k > lastKey);
                    startIndex = insertionPoint === -1 ? validKeys.length : insertionPoint;
                }
            }
            const paginatedKeys = validKeys.slice(startIndex, startIndex + limit);
            const nextCursor = startIndex + limit < validKeys.length && paginatedKeys.length > 0
                ? encodeCursor(paginatedKeys[paginatedKeys.length - 1], tenantId)
                : undefined;
            // Build a values map for the paginated slice only.
            const paginatedValues = new Map();
            for (const k of paginatedKeys) {
                const v = validValues.get(k);
                if (v !== undefined)
                    paginatedValues.set(k, v);
            }
            return {
                keys: paginatedKeys,
                nextCursor,
                values: paginatedValues,
            };
        }, {
            operation: 'FileSystemProvider.list',
            context,
            input: { tenantId, prefix },
        });
    }
    async getMany(tenantId, keys, context) {
        return await ErrorHandler.tryCatch(async () => {
            if (keys.length === 0) {
                return new Map();
            }
            // Parallel fetch for better performance
            const promises = keys.map((key) => this.get(tenantId, key, context));
            const values = await Promise.all(promises);
            const results = new Map();
            keys.forEach((key, i) => {
                const value = values[i];
                if (value !== null) {
                    results.set(key, value);
                }
            });
            return results;
        }, {
            operation: 'FileSystemProvider.getMany',
            context,
            input: { tenantId, keyCount: keys.length },
        });
    }
    async setMany(tenantId, entries, context, options) {
        return await ErrorHandler.tryCatch(async () => {
            if (entries.size === 0) {
                return;
            }
            // Parallel set for better performance
            const promises = Array.from(entries.entries()).map(([key, value]) => this.set(tenantId, key, value, context, options));
            await Promise.all(promises);
        }, {
            operation: 'FileSystemProvider.setMany',
            context,
            input: { tenantId, entryCount: entries.size },
        });
    }
    async deleteMany(tenantId, keys, context) {
        return await ErrorHandler.tryCatch(async () => {
            if (keys.length === 0) {
                return 0;
            }
            // Parallel delete for better performance
            const promises = keys.map((key) => this.delete(tenantId, key, context));
            const results = await Promise.all(promises);
            return results.filter((deleted) => deleted).length;
        }, {
            operation: 'FileSystemProvider.deleteMany',
            context,
            input: { tenantId, keyCount: keys.length },
        });
    }
    async clear(tenantId, context) {
        return await ErrorHandler.tryCatch(async () => {
            const tenantPath = this.getTenantPath(tenantId, context);
            const allKeys = await this.listFilesRecursively(tenantPath, tenantPath);
            let deletedCount = 0;
            for (const key of allKeys) {
                const deleted = await this.delete(tenantId, key, context);
                if (deleted) {
                    deletedCount++;
                }
            }
            return deletedCount;
        }, {
            operation: 'FileSystemProvider.clear',
            context,
            input: { tenantId },
        });
    }
}
//# sourceMappingURL=fileSystemProvider.js.map