/**
 * GovernanceConfig -- Unified configuration for all governance modules.
 *
 * Loads configuration from (in precedence order):
 *   1. Constructor overrides (highest)
 *   2. Environment variables (GOVERNANCE_* prefix)
 *   3. .governance.json file (lowest)
 *
 * All modules accept GovernanceConfig instead of individual config objects.
 *
 * SECURITY (SYSTEMIC-H3, fail-open pattern audit 2026-04-21):
 *   - If `.governance.json` is missing, we return defaults (legitimate
 *     first-boot scenario).
 *   - If `.governance.json` EXISTS but is malformed / truncated /
 *     tampered, we THROW and refuse to proceed. A silent `{}` fallback
 *     on parse failure would let an attacker who can corrupt the file
 *     disable audit logging or expand capability bundles silently.
 *   - Operators can temporarily bypass with FORCE_EMPTY_GOVERNANCE_CONFIG=true
 *     (env) or --force-empty-config (CLI). When set, the override is
 *     AUDIT-LOGGED via `consumeForceEmptyOverride()` which returns a
 *     structured event the caller MUST push through
 *     `storage.appendAuditChain`.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import { ensureDir } from './util/fs.js';
export class GovernanceConfig {
    static instance = null;
    /**
     * Pending FORCE_EMPTY override event. Populated during construction when
     * the operator bypasses a malformed config. The server boot path should
     * call `consumeForceEmptyOverride()` right after building the config so
     * the bypass is written to the audit chain.
     */
    static pendingForceEmptyOverride = null;
    /**
     * One-time boot log guard so we do not spam the console when multiple
     * GovernanceConfig instances are created in the same process (tests,
     * CLI sub-commands).
     */
    static bootLoggedMissingFile = false;
    config;
    constructor(overrides = {}) {
        // Layer 3: Defaults
        const defaults = {
            rootDir: '.governance',
            logDir: '.governance/logs',
            permissionsDir: '.governance/permissions',
            stateDir: '.governance/state',
            memoryDir: '.governance/memory',
            queueDir: '.governance/queue',
            governanceFile: 'GOVERNANCE_RULES.md',
            agentsDir: '.claude/agents',
            tamperDetection: true,
            encryptionKey: '',
            secretSeed: '',
            cycleTimeoutMs: 600000, // 10 min (DEV MODE -- production: 300000 / 5min)
            signatureWindowMs: 600000, // 10 min (DEV MODE -- production: 300000 / 5min)
            logLevel: 'INFO',
            pluginIntegrity: {},
            memory: {
                session: {
                    enabled: true,
                    maxIdleMs: 15 * 60 * 1000,
                    maxSessionMs: 60 * 60 * 1000,
                    clearOnRegulatedPackExit: false,
                },
                crossSession: {
                    enabled: true,
                    pepper: process.env['GOVERNANCE_MEMORY_PEPPER'] ?? '',
                },
                chain: {
                    enabled: true,
                },
                retrievalAllowlist: {
                    enabled: true,
                    defaultPolicy: 'deny',
                },
            },
        };
        // Layer 2: .governance.json
        const fileConfig = this.loadConfigFile(overrides.rootDir || defaults.rootDir);
        // Layer 1: Environment variables
        const envConfig = this.loadEnvConfig();
        // Merge: overrides > env > file > defaults
        this.config = {
            ...defaults,
            ...fileConfig,
            ...envConfig,
            ...overrides,
        };
        // Resolve relative paths against rootDir
        this.config.logDir = this.resolve(this.config.logDir);
        this.config.permissionsDir = this.resolve(this.config.permissionsDir);
        this.config.stateDir = this.resolve(this.config.stateDir);
        this.config.memoryDir = this.resolve(this.config.memoryDir);
        this.config.queueDir = this.resolve(this.config.queueDir);
    }
    /**
     * Get the singleton instance. Creates with defaults if not initialized.
     */
    static getInstance(overrides) {
        if (!GovernanceConfig.instance) {
            GovernanceConfig.instance = new GovernanceConfig(overrides);
        }
        return GovernanceConfig.instance;
    }
    /**
     * Reset the singleton (for testing).
     */
    static reset() {
        GovernanceConfig.instance = null;
        GovernanceConfig.pendingForceEmptyOverride = null;
        GovernanceConfig.bootLoggedMissingFile = false;
    }
    /**
     * If the operator used FORCE_EMPTY_GOVERNANCE_CONFIG / --force-empty-config
     * to bypass a malformed config, this returns the structured override event.
     * The server boot path MUST push this through `storage.appendAuditChain`
     * so the bypass is recorded in the tamper-evident audit chain.
     *
     * Returns null if no override was used. The value is consumed (cleared)
     * on first call so it cannot be double-logged.
     */
    static consumeForceEmptyOverride() {
        const override = GovernanceConfig.pendingForceEmptyOverride;
        GovernanceConfig.pendingForceEmptyOverride = null;
        return override;
    }
    /**
     * Get the full resolved configuration.
     */
    getAll() {
        return { ...this.config };
    }
    /**
     * Get a specific config value.
     */
    get(key) {
        return this.config[key];
    }
    /**
     * Ensure all required directories exist.
     */
    ensureDirectories() {
        ensureDir(this.config.rootDir);
        ensureDir(this.config.logDir);
        ensureDir(this.config.permissionsDir);
        ensureDir(this.config.stateDir);
        ensureDir(this.config.memoryDir);
        ensureDir(this.config.queueDir);
    }
    /**
     * Export current config to .governance.json.
     */
    exportToFile(outputPath) {
        const filePath = outputPath || path.join(this.config.rootDir, '.governance.json');
        ensureDir(path.dirname(filePath));
        // Exclude secrets from file export
        const safe = { ...this.config };
        safe.encryptionKey = safe.encryptionKey ? '[SET]' : '';
        safe.secretSeed = safe.secretSeed ? '[SET]' : '';
        fs.writeFileSync(filePath, JSON.stringify(safe, null, 2));
    }
    // --- Private ---
    loadConfigFile(rootDir) {
        const candidates = [
            path.join(rootDir, '.governance.json'),
            '.governance.json',
        ];
        for (const filePath of candidates) {
            if (!fs.existsSync(filePath))
                continue;
            // File exists. Decide between: empty (treat as absent),
            // valid JSON (use it), or malformed (fail closed).
            let raw;
            try {
                raw = fs.readFileSync(filePath, 'utf-8');
            }
            catch (err) {
                // Unreadable despite existsSync (permissions, race). Fail closed
                // unless the operator explicitly opted into empty-config boot.
                const msg = err instanceof Error ? err.message : String(err);
                return this.handleConfigFailure(filePath, `read failed: ${msg}`);
            }
            // Zero-byte or whitespace-only file: treat as file-not-configured.
            // This is a legitimate operator state (e.g. `touch .governance.json`
            // during staging setup). No warning, no throw.
            if (raw.trim() === '') {
                return {};
            }
            try {
                const parsed = JSON.parse(raw);
                // JSON.parse accepts any valid JSON value including null, arrays,
                // numbers, strings. A governance config MUST be an object.
                if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
                    return this.handleConfigFailure(filePath, `config root must be a JSON object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);
                }
                return parsed;
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                return this.handleConfigFailure(filePath, msg);
            }
        }
        // No candidate file exists. This is a valid first-boot state, but we
        // log once per process so operators can see that no config was found
        // and defaults are in use.
        if (!GovernanceConfig.bootLoggedMissingFile) {
            GovernanceConfig.bootLoggedMissingFile = true;
            // eslint-disable-next-line no-console
            console.log(`[governance-config] No .governance.json found in ${rootDir} or current directory. ` +
                `Using built-in defaults.`);
        }
        return {};
    }
    /**
     * Handle a config file that exists but cannot be used.
     *
     * Policy:
     *   - Default: throw. Server refuses to boot with unverified config.
     *   - If FORCE_EMPTY_GOVERNANCE_CONFIG=true (env) OR the CLI flag
     *     --force-empty-config is active (set via the same env var by the
     *     CLI shim): log loudly, stash an audit-chain event for the caller
     *     to emit, and return `{}`.
     */
    handleConfigFailure(filePath, parseErrorMessage) {
        const forceEmptyEnv = process.env.FORCE_EMPTY_GOVERNANCE_CONFIG;
        const forceEmpty = forceEmptyEnv === 'true' || forceEmptyEnv === '1';
        if (!forceEmpty) {
            throw new Error(`Governance config at ${filePath} is malformed: ${parseErrorMessage}. ` +
                `Refusing to boot with unverified config. Remove or repair the file, ` +
                `or set FORCE_EMPTY_GOVERNANCE_CONFIG=true (or pass --force-empty-config) ` +
                `to boot with a known-empty config (this action is audit-logged).`);
        }
        // Loud warning at boot. This is a deliberate deviation from normal
        // operation and must be visible on stderr even in quiet environments.
        // eslint-disable-next-line no-console
        console.warn(`[governance-config] FORCE_EMPTY_GOVERNANCE_CONFIG is set. Starting with empty config. ` +
            `This is NOT a valid production configuration. ` +
            `File: ${filePath}. Parse error: ${parseErrorMessage}.`);
        // Stash the event for the caller to push to the audit chain. The
        // caller is expected to call `consumeForceEmptyOverride()` immediately
        // after construction.
        const source = process.env.FORCE_EMPTY_GOVERNANCE_CONFIG_SOURCE === 'cli' ? 'cli' : 'env';
        GovernanceConfig.pendingForceEmptyOverride = {
            type: 'governance-config-force-empty',
            configPath: filePath,
            parseError: parseErrorMessage,
            source,
            timestamp: new Date().toISOString(),
        };
        return {};
    }
    loadEnvConfig() {
        const config = {};
        const envMap = {
            GOVERNANCE_ROOT_DIR: 'rootDir',
            GOVERNANCE_LOG_DIR: 'logDir',
            GOVERNANCE_PERMISSIONS_DIR: 'permissionsDir',
            GOVERNANCE_STATE_DIR: 'stateDir',
            GOVERNANCE_MEMORY_DIR: 'memoryDir',
            GOVERNANCE_QUEUE_DIR: 'queueDir',
            GOVERNANCE_FILE: 'governanceFile',
            GOVERNANCE_AGENTS_DIR: 'agentsDir',
            GOVERNANCE_ENCRYPTION_KEY: 'encryptionKey',
            GOVERNANCE_SECRET_SEED: 'secretSeed',
            GOVERNANCE_LOG_LEVEL: 'logLevel',
        };
        const numericMap = {
            GOVERNANCE_CYCLE_TIMEOUT_MS: 'cycleTimeoutMs',
            GOVERNANCE_SIGNATURE_WINDOW_MS: 'signatureWindowMs',
        };
        const booleanMap = {
            GOVERNANCE_TAMPER_DETECTION: 'tamperDetection',
        };
        for (const [envKey, configKey] of Object.entries(envMap)) {
            const value = process.env[envKey];
            if (value) {
                config[configKey] = value;
            }
        }
        for (const [envKey, configKey] of Object.entries(numericMap)) {
            const value = process.env[envKey];
            if (value) {
                const num = parseInt(value, 10);
                if (!isNaN(num)) {
                    config[configKey] = num;
                }
            }
        }
        for (const [envKey, configKey] of Object.entries(booleanMap)) {
            const value = process.env[envKey];
            if (value) {
                config[configKey] = value === 'true';
            }
        }
        return config;
    }
    resolve(dir) {
        if (path.isAbsolute(dir))
            return dir;
        return dir; // Keep relative for portability
    }
}
//# sourceMappingURL=governance-config.js.map