import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { dirname, isAbsolute, join, resolve } from 'path';
import { DEFAULT_CONFIG, CONFIG_SCHEMA } from '@pellux/goodvibes-sdk/platform/config/schema';
import { ConfigError } from '@pellux/goodvibes-sdk/platform/types/errors';
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
import { getManagedSettingLock } from '../runtime/settings/control-plane.js';
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
const DEFAULT_CONFIG_SNAPSHOT = structuredClone(DEFAULT_CONFIG);
const PERMISSION_TOOL_KEYS = new Set(Object.keys(DEFAULT_CONFIG.permissions.tools));
function cloneDefaultConfig() {
    return structuredClone(DEFAULT_CONFIG_SNAPSHOT);
}
function sanitizeConfigShape(config) {
    const sanitized = structuredClone(config);
    const lineNumbers = sanitized.display.lineNumbers;
    if (typeof lineNumbers === 'boolean') {
        sanitized.display.lineNumbers = lineNumbers ? 'all' : 'off';
    }
    for (const key of Object.keys(sanitized.permissions.tools)) {
        if (!PERMISSION_TOOL_KEYS.has(key)) {
            delete sanitized.permissions.tools[key];
        }
    }
    return sanitized;
}
function requireAbsoluteOwnedPath(path, name) {
    if (path === undefined)
        return undefined;
    const trimmed = path.trim();
    if (!trimmed) {
        throw new Error(`ConfigManager ${name} must be a non-empty absolute path.`);
    }
    if (!isAbsolute(trimmed)) {
        throw new Error(`ConfigManager ${name} must be an absolute path.`);
    }
    return resolve(trimmed);
}
/**
 * Ensure the shared ~/.goodvibes/goodvibes.json exists (empty object if not).
 * This is reserved for future cross-app use — no TUI settings go here.
 */
function ensureSharedConfig(sharedPath) {
    if (!existsSync(sharedPath)) {
        mkdirSync(dirname(sharedPath), { recursive: true });
        try {
            writeFileSync(sharedPath, '{}\n', 'utf-8');
        }
        catch (err) {
            logger.debug('Could not create shared goodvibes.json (non-fatal)', { error: summarizeError(err) });
        }
    }
}
/**
 * ConfigManager — Layered, mutable, persistent config system.
 *
 * Load order: defaults < global TUI settings < project TUI settings < CLI overrides
 * API keys are never persisted — loaded from env vars only.
 */
export class ConfigManager {
    config;
    configDir;
    configPath;
    projectConfigPath;
    workingDirectory;
    homeDirectory;
    hookDispatcher = null;
    constructor(overrides) {
        const roots = overrides;
        const configDir = requireAbsoluteOwnedPath(roots.configDir, 'configDir');
        const homeDirectory = requireAbsoluteOwnedPath(roots.homeDir, 'homeDir') ?? null;
        const workingDirectory = requireAbsoluteOwnedPath(overrides.workingDir, 'workingDir') ?? null;
        const sharedConfigPath = requireAbsoluteOwnedPath(roots.sharedConfigPath, 'sharedConfigPath');
        const base = configDir ?? join(homeDirectory, '.goodvibes', 'goodvibes');
        this.configDir = base;
        this.configPath = join(base, 'settings.json');
        this.workingDirectory = workingDirectory;
        this.homeDirectory = homeDirectory;
        this.projectConfigPath = this.workingDirectory
            ? join(this.workingDirectory, '.goodvibes', 'goodvibes', 'settings.json')
            : null;
        this.config = cloneDefaultConfig();
        const ownedSharedConfigPath = sharedConfigPath ?? (this.homeDirectory ? join(this.homeDirectory, '.goodvibes', 'goodvibes.json') : null);
        if (ownedSharedConfigPath) {
            ensureSharedConfig(ownedSharedConfigPath);
        }
        this.load();
        // Apply constructor overrides (CLI args, etc.) after load
        if (overrides.model !== undefined) {
            this.config.provider.model = overrides.model;
        }
        if (overrides.provider !== undefined) {
            this.config.provider.provider = overrides.provider;
        }
        if (overrides.autoApprove !== undefined) {
            this.config.behavior.autoApprove = overrides.autoApprove;
        }
        if (overrides.systemPromptFile !== undefined) {
            this.config.provider.systemPromptFile = overrides.systemPromptFile;
        }
    }
    getControlPlaneConfigDir() {
        return this.configDir;
    }
    getWorkingDirectory() {
        return this.workingDirectory;
    }
    getHomeDirectory() {
        return this.homeDirectory;
    }
    attachHookDispatcher(hookDispatcher) {
        this.hookDispatcher = hookDispatcher;
    }
    resolvePath(key) {
        const parts = key.split('.');
        let cursor = this.config;
        for (let i = 0; i < parts.length - 1; i++) {
            const part = parts[i];
            if (cursor == null || typeof cursor !== 'object' || !(part in cursor)) {
                throw new Error(`Invalid config path: section '${parts.slice(0, i + 1).join('.')}' does not exist`);
            }
            cursor = cursor[part];
        }
        if (cursor == null || typeof cursor !== 'object') {
            throw new Error(`Invalid config path: section '${parts.slice(0, -1).join('.')}' does not exist`);
        }
        return {
            parent: cursor,
            field: parts[parts.length - 1],
        };
    }
    /** Get a config value by dot-path key. */
    get(key) {
        const { parent, field } = this.resolvePath(key);
        return parent[field];
    }
    /** Set a config value by dot-path key and auto-save to disk. */
    set(key, value, options = {}) {
        const schema = CONFIG_SCHEMA.find(s => s.key === key);
        if (schema?.validate && !schema.validate(value)) {
            throw new ConfigError(`Invalid value for ${key}: ${String(value)}`);
        }
        if (schema?.type === 'enum' && schema.enumValues && !schema.enumValues.includes(value)) {
            throw new ConfigError(`Invalid value for ${key}: "${String(value)}". Allowed: ${schema.enumValues.join(', ')}`);
        }
        if (!options.bypassManagedLock) {
            const lock = getManagedSettingLock(key, this.configDir);
            if (lock) {
                throw new ConfigError(`Setting ${key} is locked by ${lock.source}: ${lock.reason}`);
            }
        }
        const { parent, field } = this.resolvePath(key);
        const previousValue = parent[field];
        parent[field] = value;
        this.save();
        this.emitConfigHook(key, previousValue, value);
    }
    /**
     * Fire the Change:config hook for a config key change.
     * Best-effort: the hook dispatcher may not be initialised during startup.
     */
    emitConfigHook(key, previousValue, newValue) {
        if (!this.hookDispatcher)
            return;
        try {
            const event = {
                path: `Change:config:${key}`,
                phase: 'Change',
                category: 'config',
                specific: key,
                sessionId: '',
                timestamp: Date.now(),
                payload: { key, value: newValue, previousValue },
            };
            this.hookDispatcher
                .fire(event)
                .catch(() => { });
        }
        catch {
            // Dispatcher not ready during startup — safe to ignore
        }
    }
    /**
     * Set a config value from a validated ConfigKey with unknown value type.
     * Used when iterating schema entries where the value type cannot be statically
     * inferred. Runtime validation is still applied by the underlying set() method.
     */
    setDynamic(key, value, options = {}) {
        this.set(key, value, options);
    }
    /** Return a deep-readonly snapshot of the full config. Nested objects are immutable. */
    getAll() {
        return structuredClone(this.config);
    }
    /** Return a deep-cloned snapshot of a config category. */
    getCategory(category) {
        return structuredClone(this.config[category]);
    }
    /** Return a deep-cloned snapshot of the live config. Safe for read-only external consumers. */
    getRaw() {
        return structuredClone(this.config);
    }
    /** Return the full schema. */
    getSchema() {
        return CONFIG_SCHEMA;
    }
    /** Persist current config to global TUI settings file. */
    save() {
        try {
            mkdirSync(dirname(this.configPath), { recursive: true });
            writeFileSync(this.configPath, JSON.stringify(this.config, null, 2) + '\n', 'utf-8');
        }
        catch (err) {
            logger.debug('Config save failed (non-fatal)', { error: summarizeError(err) });
        }
    }
    /** Persist current config to project-level TUI settings file (.goodvibes/goodvibes/settings.json). */
    saveProject() {
        if (!this.projectConfigPath) {
            throw new Error('ConfigManager.saveProject requires an explicit workingDir.');
        }
        try {
            mkdirSync(dirname(this.projectConfigPath), { recursive: true });
            writeFileSync(this.projectConfigPath, JSON.stringify(this.config, null, 2) + '\n', 'utf-8');
        }
        catch (err) {
            logger.debug('Project config save failed (non-fatal)', { error: summarizeError(err) });
        }
    }
    /** Load config from disk: global then project (project wins). Deep-merges with defaults. */
    load() {
        // Load global settings
        if (existsSync(this.configPath)) {
            try {
                const raw = readFileSync(this.configPath, 'utf-8');
                const parsed = JSON.parse(raw);
                this.config = sanitizeConfigShape(deepMerge(cloneDefaultConfig(), parsed));
            }
            catch (err) {
                logger.debug('Global config load failed (non-fatal, using defaults)', { error: summarizeError(err) });
            }
        }
        // Load project settings and deep-merge on top (project wins)
        if (this.projectConfigPath && existsSync(this.projectConfigPath)) {
            try {
                const raw = readFileSync(this.projectConfigPath, 'utf-8');
                const parsed = JSON.parse(raw);
                this.config = sanitizeConfigShape(deepMerge(this.config, parsed));
            }
            catch (err) {
                logger.debug('Project config load failed (non-fatal)', { error: summarizeError(err) });
            }
        }
    }
    /**
     * Merge a partial patch into a config category and auto-save.
     *
     * This is the correct way to update array or object fields within a category
     * that cannot be expressed as a scalar dot-path key (e.g. notifications.webhookUrls).
     * The patch is shallow-merged into the existing category value.
     */
    mergeCategory(category, patch) {
        const current = this.config[category];
        const patchObj = patch;
        for (const key of Object.keys(patchObj)) {
            if (patchObj[key] !== undefined) {
                current[key] = patchObj[key];
            }
        }
        this.save();
    }
    /**
     * Reset a specific key to its default, or reset all config.
     * Saves to disk after reset.
     */
    reset(key) {
        if (key === undefined) {
            this.config = cloneDefaultConfig();
        }
        else {
            const schema = CONFIG_SCHEMA.find(s => s.key === key);
            if (!schema)
                throw new ConfigError(`Unknown config key: ${key}`);
            const livePath = this.resolvePath(key);
            const defaultPath = resolveArbitraryPath(DEFAULT_CONFIG_SNAPSHOT, key);
            livePath.parent[livePath.field] = structuredClone(defaultPath.parent[defaultPath.field]);
        }
        this.save();
    }
}
/** Deep-merge source into target. Returns a new object. Source non-objects are ignored — target clone is returned.
 * Non-object source values will not overwrite object target values (type-safe merge). */
function deepMerge(target, source) {
    const result = isObject(target)
        ? structuredClone(target)
        : {};
    if (!isObject(source))
        return result;
    for (const key of Object.keys(source)) {
        const sv = source[key];
        const tv = result[key];
        if (isObject(sv) && isObject(tv)) {
            result[key] = deepMerge(tv, sv);
        }
        else if (sv !== undefined && !isObject(tv)) {
            // Only overwrite non-object target values — never replace an object with a scalar.
            // Clone assigned values so config instances never share mutable references.
            result[key] = structuredClone(sv);
        }
    }
    return result;
}
function isObject(val) {
    return val !== null && typeof val === 'object' && !Array.isArray(val);
}
function resolveArbitraryPath(root, key) {
    const parts = key.split('.');
    let cursor = root;
    for (let i = 0; i < parts.length - 1; i++) {
        const part = parts[i];
        if (cursor == null || typeof cursor !== 'object' || !(part in cursor)) {
            throw new Error(`Invalid config path: section '${parts.slice(0, i + 1).join('.')}' does not exist`);
        }
        cursor = cursor[part];
    }
    if (cursor == null || typeof cursor !== 'object') {
        throw new Error(`Invalid config path: section '${parts.slice(0, -1).join('.')}' does not exist`);
    }
    return {
        parent: cursor,
        field: parts[parts.length - 1],
    };
}
