/**
 * Multi-tenancy support for AI governance.
 *
 * Enables namespaced governance contexts so multiple teams/projects
 * can share a single governance installation with isolated rules,
 * permissions, and audit trails.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
export class TenantNotFoundError extends Error {
    tenantId;
    constructor(tenantId) {
        super(`Tenant not found: ${tenantId}`);
        this.tenantId = tenantId;
        this.name = 'TenantNotFoundError';
    }
}
export class TenantCycleError extends Error {
    chain;
    constructor(chain) {
        super(`Circular tenant inheritance detected: ${chain.join(' -> ')}`);
        this.chain = chain;
        this.name = 'TenantCycleError';
    }
}
export class MultiTenantGovernance {
    config;
    tenants = new Map();
    registryFile;
    constructor(config) {
        this.config = {
            maxDepth: 3,
            ...config,
        };
        this.registryFile = path.join(this.config.baseDir, 'tenants.json');
        this.ensureDir(this.config.baseDir);
        this.loadRegistry();
    }
    /**
     * Register a new tenant.
     */
    registerTenant(config) {
        // Sanitize tenant ID to prevent directory traversal
        this.validateTenantId(config.id);
        if (this.tenants.has(config.id)) {
            throw new Error(`Tenant already exists: ${config.id}`);
        }
        // Validate parent exists if specified
        if (config.parentId && !this.tenants.has(config.parentId)) {
            throw new TenantNotFoundError(config.parentId);
        }
        // Check inheritance depth
        if (config.parentId) {
            const depth = this.getInheritanceDepth(config.parentId);
            if (depth >= this.config.maxDepth) {
                throw new Error(`Tenant nesting depth ${depth + 1} exceeds maximum ${this.config.maxDepth}`);
            }
        }
        const tenant = {
            active: true,
            packs: [],
            permissionOverrides: {},
            ...config,
        };
        this.tenants.set(tenant.id, tenant);
        // Create tenant directories
        const tenantDir = path.join(this.config.baseDir, tenant.id);
        this.ensureDir(tenantDir);
        this.ensureDir(path.join(tenantDir, 'permissions'));
        this.ensureDir(path.join(tenantDir, 'logs'));
        // Create default governance file if not specified
        const govFile = tenant.governanceFile || path.join(tenantDir, 'governance.md');
        if (!fs.existsSync(govFile)) {
            fs.writeFileSync(govFile, `# Governance Rules - ${tenant.name}\n\nTenant: ${tenant.id}\nCreated: ${new Date().toISOString()}\n`);
        }
        this.saveRegistry();
        return this.resolveTenantContext(tenant.id);
    }
    /**
     * Get a tenant's resolved context (with inheritance).
     */
    getTenantContext(tenantId) {
        if (!this.tenants.has(tenantId)) {
            throw new TenantNotFoundError(tenantId);
        }
        return this.resolveTenantContext(tenantId);
    }
    /**
     * List all registered tenants.
     */
    listTenants() {
        return Array.from(this.tenants.values());
    }
    /**
     * Update a tenant's configuration.
     */
    updateTenant(tenantId, updates) {
        const existing = this.tenants.get(tenantId);
        if (!existing) {
            throw new TenantNotFoundError(tenantId);
        }
        // Prevent changing ID
        const { id: _id, ...safeUpdates } = updates;
        // Validate new parent if changing
        if (safeUpdates.parentId !== undefined) {
            if (safeUpdates.parentId && !this.tenants.has(safeUpdates.parentId)) {
                throw new TenantNotFoundError(safeUpdates.parentId);
            }
            // Check for cycles
            if (safeUpdates.parentId) {
                this.checkCycle(tenantId, safeUpdates.parentId);
            }
        }
        const updated = { ...existing, ...safeUpdates };
        this.tenants.set(tenantId, updated);
        this.saveRegistry();
        return updated;
    }
    /**
     * Deactivate a tenant (soft delete).
     */
    deactivateTenant(tenantId) {
        this.updateTenant(tenantId, { active: false });
    }
    /**
     * Verify cross-tenant isolation.
     * Checks that no tenant can access another tenant's data paths.
     */
    verifyIsolation() {
        const violations = [];
        const tenantDirs = new Map();
        for (const [id, tenant] of this.tenants) {
            const tenantDir = path.resolve(this.config.baseDir, id);
            tenantDirs.set(id, tenantDir);
            // Check governance file is within tenant boundary or explicit override
            const govFile = tenant.governanceFile || path.join(tenantDir, 'governance.md');
            const resolvedGov = path.resolve(govFile);
            if (!tenant.governanceFile && !resolvedGov.startsWith(tenantDir)) {
                violations.push(`Tenant ${id}: governance file ${resolvedGov} escapes tenant boundary`);
            }
        }
        // Check no tenant directory is a subdirectory of another
        const dirs = Array.from(tenantDirs.entries());
        for (let i = 0; i < dirs.length; i++) {
            for (let j = i + 1; j < dirs.length; j++) {
                const [idA, dirA] = dirs[i];
                const [idB, dirB] = dirs[j];
                if (dirA.startsWith(dirB + path.sep)) {
                    violations.push(`Tenant ${idA} directory is inside tenant ${idB} directory`);
                }
                if (dirB.startsWith(dirA + path.sep)) {
                    violations.push(`Tenant ${idB} directory is inside tenant ${idA} directory`);
                }
            }
        }
        return {
            valid: violations.length === 0,
            violations,
        };
    }
    /**
     * Resolve effective permissions for a tenant (including inheritance).
     */
    resolvePermissions(tenantId) {
        if (!this.tenants.has(tenantId)) {
            throw new TenantNotFoundError(tenantId);
        }
        const chain = this.getInheritanceChain(tenantId);
        const permissions = {};
        // Apply from root ancestor to current (child overrides parent)
        for (const ancestorId of chain) {
            const tenant = this.tenants.get(ancestorId);
            if (tenant.permissionOverrides) {
                Object.assign(permissions, tenant.permissionOverrides);
            }
        }
        return permissions;
    }
    /**
     * Get the number of registered tenants.
     */
    get tenantCount() {
        return this.tenants.size;
    }
    // --- Private helpers ---
    resolveTenantContext(tenantId) {
        const tenant = this.tenants.get(tenantId);
        const tenantDir = path.join(this.config.baseDir, tenant.id);
        return {
            tenant,
            governanceFile: tenant.governanceFile || path.join(tenantDir, 'governance.md'),
            permissionsDir: path.join(tenantDir, 'permissions'),
            logDir: path.join(tenantDir, 'logs'),
            inheritedPermissions: this.resolvePermissions(tenantId),
        };
    }
    getInheritanceChain(tenantId) {
        const chain = [];
        const visited = new Set();
        let current = tenantId;
        while (current) {
            if (visited.has(current)) {
                throw new TenantCycleError([...chain, current]);
            }
            visited.add(current);
            chain.unshift(current);
            const tenant = this.tenants.get(current);
            current = tenant?.parentId;
        }
        return chain;
    }
    getInheritanceDepth(tenantId) {
        return this.getInheritanceChain(tenantId).length;
    }
    checkCycle(tenantId, newParentId) {
        const visited = new Set([tenantId]);
        let current = newParentId;
        while (current) {
            if (visited.has(current)) {
                throw new TenantCycleError([...visited, current]);
            }
            visited.add(current);
            const tenant = this.tenants.get(current);
            current = tenant?.parentId;
        }
    }
    validateTenantId(id) {
        if (!id || typeof id !== 'string') {
            throw new Error('Tenant ID must be a non-empty string');
        }
        // Block directory traversal characters and null bytes
        if (/[\/\\]|\.\.|\0/.test(id)) {
            throw new Error(`Invalid tenant ID '${id}': must not contain path separators, '..', or null bytes`);
        }
        // Only allow alphanumeric, hyphens, and underscores
        if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
            throw new Error(`Invalid tenant ID '${id}': only alphanumeric characters, hyphens, and underscores allowed`);
        }
        // Verify resolved path stays within baseDir
        const resolved = path.resolve(this.config.baseDir, id);
        if (!resolved.startsWith(path.resolve(this.config.baseDir) + path.sep)) {
            throw new Error(`Tenant ID '${id}' resolves outside base directory`);
        }
    }
    loadRegistry() {
        if (fs.existsSync(this.registryFile)) {
            try {
                const data = JSON.parse(fs.readFileSync(this.registryFile, 'utf-8'));
                if (Array.isArray(data)) {
                    for (const t of data) {
                        this.tenants.set(t.id, t);
                    }
                }
            }
            catch {
                // Corrupted registry - start fresh
                this.tenants.clear();
            }
        }
    }
    saveRegistry() {
        const data = Array.from(this.tenants.values());
        fs.writeFileSync(this.registryFile, JSON.stringify(data, null, 2));
    }
    ensureDir(dir) {
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
    }
}
//# sourceMappingURL=multi-tenant.js.map