/**
 * StorageBackend -- Pluggable storage interface for governance data.
 *
 * Provides a uniform API for persisting governance state, allowing
 * future backends (S3, database, etc.) without changing module code.
 * Ships with FileSystemBackend as the default zero-dependency implementation.
 *
 * T-026: StorageBackend interface for swappable persistence.
 * T-028: State file versioning with migration support.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { createLogger } from './log.js';
const log = createLogger('storage');
/** Current schema version for state files */
export const CURRENT_SCHEMA_VERSION = 1;
/**
 * FileSystemBackend -- Default storage using the local filesystem.
 */
export class FileSystemBackend {
    baseDir;
    migrations = new Map();
    constructor(baseDir) {
        this.baseDir = baseDir;
        if (!fs.existsSync(baseDir)) {
            fs.mkdirSync(baseDir, { recursive: true });
        }
    }
    /**
     * Register a migration function for upgrading from a specific version.
     */
    registerMigration(fromVersion, migration) {
        this.migrations.set(fromVersion, migration);
    }
    async get(key) {
        const filePath = this.keyToPath(key);
        if (!fs.existsSync(filePath))
            return undefined;
        try {
            const raw = fs.readFileSync(filePath, 'utf-8');
            const versioned = JSON.parse(raw);
            // Verify checksum
            const expectedChecksum = this.computeChecksum(versioned.data);
            if (versioned.checksum !== expectedChecksum) {
                log.error('Storage checksum mismatch', { key });
                return undefined;
            }
            // Run migrations if needed
            if (versioned.schemaVersion < CURRENT_SCHEMA_VERSION) {
                const migrated = this.migrate(versioned.data, versioned.schemaVersion);
                // Save migrated data
                await this.set(key, migrated);
                return migrated;
            }
            return versioned.data;
        }
        catch (err) {
            log.error('Storage read failed', { key, error: err.message });
            return undefined;
        }
    }
    async set(key, value) {
        const filePath = this.keyToPath(key);
        const dir = path.dirname(filePath);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        const now = new Date().toISOString();
        let createdAt = now;
        // Preserve original createdAt if updating
        if (fs.existsSync(filePath)) {
            try {
                const existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
                createdAt = existing.createdAt;
            }
            catch { /* use now */ }
        }
        const versioned = {
            schemaVersion: CURRENT_SCHEMA_VERSION,
            data: value,
            createdAt,
            updatedAt: now,
            checksum: this.computeChecksum(value),
        };
        fs.writeFileSync(filePath, JSON.stringify(versioned, null, 2));
    }
    async delete(key) {
        const filePath = this.keyToPath(key);
        if (!fs.existsSync(filePath))
            return false;
        fs.unlinkSync(filePath);
        return true;
    }
    async has(key) {
        return fs.existsSync(this.keyToPath(key));
    }
    async keys(prefix) {
        return this.scanKeys(this.baseDir, '', prefix);
    }
    async metadata(key) {
        const filePath = this.keyToPath(key);
        if (!fs.existsSync(filePath))
            return undefined;
        try {
            const stat = fs.statSync(filePath);
            const raw = fs.readFileSync(filePath, 'utf-8');
            const versioned = JSON.parse(raw);
            return {
                key,
                sizeBytes: stat.size,
                createdAt: versioned.createdAt,
                updatedAt: versioned.updatedAt,
                schemaVersion: versioned.schemaVersion,
                checksum: versioned.checksum,
            };
        }
        catch {
            return undefined;
        }
    }
    // --- Private ---
    keyToPath(key) {
        // Keys can use / as separators to create subdirectories
        const sanitized = key.replace(/[^a-zA-Z0-9/_.-]/g, '_');
        return path.join(this.baseDir, sanitized + '.json');
    }
    computeChecksum(data) {
        return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
    }
    migrate(data, fromVersion) {
        let current = data;
        let version = fromVersion;
        while (version < CURRENT_SCHEMA_VERSION) {
            const migration = this.migrations.get(version);
            if (migration) {
                current = migration(current, version);
                log.info('Migration applied', { from: version, to: version + 1 });
            }
            version++;
        }
        return current;
    }
    scanKeys(dir, prefix, filter) {
        const keys = [];
        try {
            const entries = fs.readdirSync(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullKey = prefix ? `${prefix}/${entry.name}` : entry.name;
                if (entry.isDirectory()) {
                    keys.push(...this.scanKeys(path.join(dir, entry.name), fullKey, filter));
                }
                else if (entry.name.endsWith('.json')) {
                    const key = fullKey.replace(/\.json$/, '');
                    if (!filter || key.startsWith(filter)) {
                        keys.push(key);
                    }
                }
            }
        }
        catch { /* ignore */ }
        return keys;
    }
}
//# sourceMappingURL=storage-backend.js.map