import { SessionError } from '../errors.js';
import { validateIdentifier, validateUuidV7 } from './validation.js';
const MANIFEST = 'manifest.json';
const SNAPSHOT_LATEST = 'snapshot_latest.json';
const IMMUTABLE_HISTORY = 'immutable_history';
const SNAPSHOT_REGEX = /snapshot_([\w-]+)\.json$/;
const SCHEMA_VERSION = '1.0';
/**
 * File-based implementation of SnapshotStorage.
 * Persists session snapshots to the local filesystem under a configurable base directory.
 *
 * Directory layout:
 * ```
 * <baseDir>/<sessionId>/scopes/<scope>/<scopeId>/snapshots/
 *   snapshot_latest.json
 *   immutable_history/
 *     snapshot_<uuid7>.json
 * ```
 */
export class FileStorage {
    /** Absolute path to the root directory where all session data is stored. */
    _baseDir;
    /**
     * @param baseDir - Absolute path to the root directory for storing session snapshots.
     */
    constructor(baseDir) {
        this._baseDir = baseDir;
    }
    /**
     * Resolves the absolute file path for a given scope location and filename.
     * Validates sessionId and scopeId before constructing the path.
     */
    async _getPath(location, filename) {
        const { join } = await import('path');
        validateIdentifier(location.sessionId);
        validateIdentifier(location.scopeId);
        return join(this._baseDir, location.sessionId, 'scopes', location.scope, location.scopeId, 'snapshots', filename);
    }
    /**
     * Resolves the absolute path to the root directory for a session.
     * Used by deleteSession to remove all data under `<baseDir>/<sessionId>/`.
     */
    async _getSessionDir(sessionId) {
        const { join } = await import('path');
        validateIdentifier(sessionId);
        return join(this._baseDir, sessionId);
    }
    /**
     * Persists a snapshot to disk.
     * If `isLatest` is true, writes to `snapshot_latest.json` (overwriting any previous).
     * Otherwise, writes to `immutable_history/snapshot_<snapshotId>.json`.
     */
    async saveSnapshot(params) {
        const path = params.isLatest
            ? await this._getLatestSnapshotPath(params.location)
            : await this._getHistorySnapshotPath(params.location, params.snapshotId);
        await this._writeJSON(path, params.snapshot);
    }
    /**
     * Loads a snapshot from disk.
     * If `snapshotId` is omitted, loads `snapshot_latest.json`.
     * Returns null if the file does not exist.
     */
    async loadSnapshot(params) {
        const path = params.snapshotId === undefined
            ? await this._getLatestSnapshotPath(params.location)
            : await this._getHistorySnapshotPath(params.location, params.snapshotId);
        return this._readJSON(path);
    }
    /**
     * Lists immutable snapshot IDs for a scope, sorted chronologically.
     * Since IDs are UUID v7, lexicographic sort equals chronological order.
     * `startAfter` filters to IDs after the given UUID v7 (exclusive cursor).
     * `limit` caps the number of returned IDs.
     * Returns an empty array if no snapshots exist yet.
     */
    async listSnapshotIds(params) {
        if (params.limit !== undefined && params.limit <= 0)
            return [];
        if (params.startAfter)
            validateUuidV7(params.startAfter);
        const dirPath = await this._getPath(params.location, IMMUTABLE_HISTORY);
        try {
            const { promises: fs } = await import('fs');
            const files = await fs.readdir(dirPath);
            let ids = files
                .map((file) => file.match(SNAPSHOT_REGEX)?.[1])
                .filter((id) => id !== undefined)
                .sort();
            if (params.startAfter) {
                ids = ids.filter((id) => id > params.startAfter);
            }
            if (params.limit !== undefined) {
                ids = ids.slice(0, params.limit);
            }
            return ids;
        }
        catch (error) {
            if (this._isFileNotFoundError(error))
                return [];
            throw new SessionError(`Failed to list snapshots for session ${params.location.sessionId}`, { cause: error });
        }
    }
    /**
     * Deletes all data for a session by removing its root directory (`<baseDir>/<sessionId>/`) recursively.
     * No-ops if the session directory does not exist.
     */
    async deleteSession(params) {
        const sessionDir = await this._getSessionDir(params.sessionId);
        try {
            const { promises: fs } = await import('fs');
            await fs.rm(sessionDir, { recursive: true, force: true });
        }
        catch (error) {
            throw new SessionError(`Failed to delete session ${params.sessionId}`, { cause: error });
        }
    }
    /**
     * Loads the snapshot manifest for a scope.
     * Returns a default manifest with the current timestamp if none exists yet.
     */
    async loadManifest(params) {
        const path = await this._getPath(params.location, MANIFEST);
        const manifest = await this._readJSON(path);
        return (manifest ?? {
            schemaVersion: SCHEMA_VERSION,
            updatedAt: new Date().toISOString(),
        });
    }
    /**
     * Persists the snapshot manifest for a scope to disk.
     */
    async saveManifest(params) {
        const path = await this._getPath(params.location, MANIFEST);
        await this._writeJSON(path, params.manifest);
    }
    /**
     * Atomically writes JSON to a file using a `.tmp` intermediary to prevent partial writes.
     * Creates parent directories if they do not exist.
     */
    async _writeJSON(path, data) {
        try {
            const { promises: fs } = await import('fs');
            const { dirname } = await import('path');
            await fs.mkdir(dirname(path), { recursive: true });
            const tmpPath = `${path}.tmp`;
            await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf8');
            await fs.rename(tmpPath, path);
        }
        catch (error) {
            throw new SessionError(`Failed to write file ${path}`, { cause: error });
        }
    }
    /**
     * Reads and parses a JSON file. Returns null if the file does not exist.
     * Throws SessionError on parse failure or unexpected filesystem errors.
     */
    async _readJSON(path) {
        try {
            const { promises: fs } = await import('fs');
            const content = await fs.readFile(path, 'utf8');
            return JSON.parse(content);
        }
        catch (error) {
            if (this._isFileNotFoundError(error)) {
                return null;
            }
            if (error instanceof SyntaxError) {
                throw new SessionError(`Invalid JSON in file ${path}`, { cause: error });
            }
            throw new SessionError(`File system error reading ${path}`, { cause: error });
        }
    }
    /** Returns true if the error represents a missing file or directory (ENOENT). */
    _isFileNotFoundError(error) {
        return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
    }
    /** Returns the file path for `snapshot_latest.json` within the given scope. */
    async _getLatestSnapshotPath(location) {
        return this._getPath(location, SNAPSHOT_LATEST);
    }
    /**
     * Returns the file path for an immutable snapshot in `immutable_history/`.
     * Validates the snapshotId and guards against path traversal outside `_baseDir`.
     */
    async _getHistorySnapshotPath(location, snapshotId) {
        validateIdentifier(snapshotId);
        const resolved = await this._getPath(location, `${IMMUTABLE_HISTORY}/snapshot_${snapshotId}.json`);
        if (!resolved.startsWith(this._baseDir)) {
            throw new SessionError(`Invalid snapshotId '${snapshotId}': resolves outside storage directory`);
        }
        return resolved;
    }
}
//# sourceMappingURL=file-storage.js.map