/**
 * Settings Management Service
 *
 * Handles persistence and validation of Crewly application settings.
 * Settings are stored in ~/.crewly/settings.json.
 *
 * @module services/settings/settings.service
 */
import { promises as fs } from 'fs';
import path from 'path';
import { getDefaultSettings, validateSettings, mergeSettings, resolveApiKey, } from '../../types/settings.types.js';
import { atomicWriteJson, safeReadJson } from '../../utils/file-io.utils.js';
import { CREWLY_AGENT_MANAGED_COMMAND, LEGACY_CREWLY_AGENT_SENTINELS, } from '../../constants.js';
// ============================================================================
// Custom Error Classes
// ============================================================================
/**
 * Error thrown when settings validation fails
 */
export class SettingsValidationError extends Error {
    errors;
    constructor(errors) {
        super(`Settings validation failed: ${errors.join(', ')}`);
        this.name = 'SettingsValidationError';
        this.errors = errors;
    }
}
/**
 * Error thrown when settings file cannot be read or parsed
 */
export class SettingsFileError extends Error {
    cause;
    constructor(message, cause) {
        super(message);
        this.cause = cause;
        this.name = 'SettingsFileError';
    }
}
// ============================================================================
// SettingsService Class
// ============================================================================
/**
 * Service for managing Crewly application settings
 *
 * Settings are stored in ~/.crewly/settings.json by default.
 * Default values are used when no settings file exists.
 *
 * @example
 * ```typescript
 * const service = getSettingsService();
 *
 * // Get current settings
 * const settings = await service.getSettings();
 *
 * // Update settings
 * await service.updateSettings({
 *   general: { verboseLogging: true },
 * });
 *
 * // Reset to defaults
 * await service.resetSettings();
 * ```
 */
export class SettingsService {
    settingsDir;
    settingsFile;
    settingsCache = null;
    /**
     * Create a new SettingsService instance
     *
     * @param options - Configuration options
     * @param options.settingsDir - Directory for settings file
     */
    constructor(options) {
        this.settingsDir = options?.settingsDir ??
            path.join(process.env.HOME || '~', '.crewly');
        this.settingsFile = path.join(this.settingsDir, 'settings.json');
    }
    /**
     * Get current settings, loading from file if not cached
     *
     * If settings file doesn't exist or is invalid, returns default settings.
     *
     * @returns Current settings
     */
    async getSettings() {
        if (this.settingsCache) {
            return this.settingsCache;
        }
        const loaded = await safeReadJson(this.settingsFile, null);
        if (loaded) {
            // Migrate legacy claudeCodeCommand/claudeCodeInitScript → runtimeCommands
            this.migrateSettings(loaded);
            this.settingsCache = mergeSettings(getDefaultSettings(), loaded);
            return this.settingsCache;
        }
        return getDefaultSettings();
    }
    /**
     * Update settings with partial input
     *
     * Merges the input with current settings and validates before saving.
     *
     * @param input - Partial settings to update
     * @returns Updated settings
     * @throws {SettingsValidationError} If validation fails
     */
    async updateSettings(input) {
        const current = await this.getSettings();
        const merged = mergeSettings(current, input);
        const validation = validateSettings(merged);
        if (!validation.valid) {
            throw new SettingsValidationError(validation.errors);
        }
        await this.saveSettings(merged);
        this.settingsCache = merged;
        return merged;
    }
    /**
     * Reset all settings to defaults
     *
     * @returns Default settings
     */
    async resetSettings() {
        const defaults = getDefaultSettings();
        await this.saveSettings(defaults);
        this.settingsCache = defaults;
        return defaults;
    }
    /**
     * Reset a specific settings section to defaults
     *
     * @param section - The section to reset ('general', 'chat', or 'skills')
     * @returns Updated settings
     */
    async resetSection(section) {
        const current = await this.getSettings();
        const defaults = getDefaultSettings();
        const updated = {
            ...current,
            [section]: defaults[section],
        };
        await this.saveSettings(updated);
        this.settingsCache = updated;
        return updated;
    }
    /**
     * Validate settings input without saving
     *
     * Useful for form validation before submission.
     *
     * @param input - Settings to validate
     * @returns Validation result
     */
    async validateSettingsInput(input) {
        const current = await this.getSettings();
        const merged = mergeSettings(current, input);
        return validateSettings(merged);
    }
    /**
     * Export settings to a file
     *
     * @param exportPath - Path to export file
     */
    async exportSettings(exportPath) {
        const settings = await this.getSettings();
        await atomicWriteJson(exportPath, settings);
    }
    /**
     * Import settings from a file
     *
     * Validates imported settings before saving.
     *
     * @param importPath - Path to import file
     * @returns Imported settings
     * @throws {SettingsValidationError} If imported settings are invalid
     * @throws {SettingsFileError} If file cannot be read or parsed
     */
    async importSettings(importPath) {
        let content;
        try {
            content = await fs.readFile(importPath, 'utf-8');
        }
        catch (err) {
            throw new SettingsFileError(`Failed to read settings file: ${importPath}`, err);
        }
        let imported;
        try {
            imported = JSON.parse(content);
        }
        catch (err) {
            throw new SettingsFileError('Failed to parse settings file: invalid JSON', err);
        }
        // Merge with defaults to fill in missing properties
        const defaults = getDefaultSettings();
        const merged = mergeSettings(defaults, imported);
        const validation = validateSettings(merged);
        if (!validation.valid) {
            throw new SettingsValidationError(validation.errors);
        }
        await this.saveSettings(merged);
        this.settingsCache = merged;
        return merged;
    }
    /**
     * Resolve an API key through the override chain: skill → runtime → global → env var
     *
     * @param provider - The API key provider (gemini, anthropic, openai)
     * @param context - Optional context with runtime and/or skill for override resolution
     * @returns The resolved API key or undefined if not configured anywhere
     */
    async getApiKey(provider, context) {
        const settings = await this.getSettings();
        return resolveApiKey(provider, settings.apiKeys, context);
    }
    /**
     * Clear the settings cache
     *
     * Forces the next getSettings call to reload from disk.
     */
    clearCache() {
        this.settingsCache = null;
    }
    /**
     * Get the path to the settings file
     *
     * @returns Absolute path to settings.json
     */
    getSettingsFilePath() {
        return this.settingsFile;
    }
    /**
     * Check if a settings file exists
     *
     * @returns True if settings file exists
     */
    async hasSettingsFile() {
        try {
            await fs.access(this.settingsFile);
            return true;
        }
        catch {
            return false;
        }
    }
    // ============================================================================
    // Private Methods
    // ============================================================================
    /**
     * Migrate legacy settings format to current format
     *
     * Converts old claudeCodeCommand/claudeCodeInitScript fields
     * to the new runtimeCommands map.
     *
     * @param loaded - Partially loaded settings from disk
     */
    migrateSettings(loaded) {
        const general = loaded.general;
        if (!general)
            return;
        const hasLegacyCommand = typeof general['claudeCodeCommand'] === 'string';
        const hasRuntimeCommands = general['runtimeCommands'] !== undefined;
        if (hasLegacyCommand && !hasRuntimeCommands) {
            const defaults = getDefaultSettings();
            general['runtimeCommands'] = {
                'claude-code': general['claudeCodeCommand'],
                'gemini-cli': defaults.general.runtimeCommands['gemini-cli'],
                'codex-cli': defaults.general.runtimeCommands['codex-cli'],
                'crewly-agent': defaults.general.runtimeCommands['crewly-agent'],
            };
        }
        // Keep existing users aligned with the current safer Codex default.
        // Only rewrite when their value is exactly the previous built-in default.
        const runtimeCommands = general['runtimeCommands'];
        if (runtimeCommands?.['codex-cli'] === 'codex --full-auto') {
            runtimeCommands['codex-cli'] = 'codex -a never -s danger-full-access';
        }
        // Self-heal the stale crewly-agent sentinel (issue #693). Nodes that
        // persisted settings before PR #599 carry
        // `runtimeCommands['crewly-agent'] = 'crewly-agent-in-process'`, a value
        // that is not a real executable: it passes the shell-command allow-list
        // (no metacharacters) and gets shelled out via `sh -lc`, failing with
        // exit-127 so the orchestrator never starts. Rewrite any known legacy
        // sentinel to the managed binary so existing nodes recover on restart
        // without a manual settings.json edit. Idempotent — fresh installs already
        // default to the managed command.
        if (runtimeCommands &&
            LEGACY_CREWLY_AGENT_SENTINELS.includes(runtimeCommands['crewly-agent'])) {
            runtimeCommands['crewly-agent'] = CREWLY_AGENT_MANAGED_COMMAND;
        }
        // One-time flip: existing users with proactive compact enabled get switched
        // off per Steve user-position decision (2026-05-04). The Settings UI still
        // allows users to opt back in. The `_proactiveCompactMigrated` marker is
        // idempotent — a user who deliberately re-enables via Settings UI will not
        // be re-flipped on subsequent loads.
        //
        // **Marker persistence note** (per Arch review observation 2 on PR #447):
        // This migration mutates the in-memory `loaded` object only. The marker
        // is not separately written to disk here — it lands on disk via the
        // standard `saveSettings()` flow (e.g. when the user later updates ANY
        // setting through the UI, or via `mergeSettings`/`updateSettings`).
        // Until that next save, the marker exists transiently in memory; if the
        // process restarts before any save, the migration runs again and is
        // still safe (the same `true → false` flip happens, idempotent in
        // user-visible outcome). Future maintainers: a missing-on-disk marker
        // is NOT a bug — it just means the user has not saved settings since
        // first load post-upgrade.
        //
        // Per spec 2026-05-05-compact-fix-AB-followup §A.1
        if (typeof general['enableProactiveCompact'] === 'boolean' &&
            general['enableProactiveCompact'] === true &&
            !general['_proactiveCompactMigrated']) {
            general['enableProactiveCompact'] = false;
            general['_proactiveCompactMigrated'] = true;
        }
        // Clean up legacy fields
        delete general['claudeCodeCommand'];
        delete general['claudeCodeInitScript'];
    }
    /**
     * Save settings to disk
     *
     * @param settings - Settings to save
     */
    async saveSettings(settings) {
        await fs.mkdir(this.settingsDir, { recursive: true });
        await atomicWriteJson(this.settingsFile, settings);
    }
}
// ============================================================================
// Singleton Instance
// ============================================================================
let settingsServiceInstance = null;
/**
 * Get the singleton SettingsService instance
 *
 * @returns The SettingsService instance
 */
export function getSettingsService() {
    if (!settingsServiceInstance) {
        settingsServiceInstance = new SettingsService();
    }
    return settingsServiceInstance;
}
/**
 * Reset the singleton instance (for testing)
 */
export function resetSettingsService() {
    settingsServiceInstance = null;
}
//# sourceMappingURL=settings.service.js.map