/**
 * @license
 * Copyright 2025 HopCode Team
 * SPDX-License-Identifier: Apache-2.0
 */
import * as fs from 'node:fs';
import { getSettingsSchema } from '../config/settingsSchema.js';
import { t } from '../i18n/index.js';
import { isAutoLanguage } from './languageUtils.js';
function flattenSchema(schema, prefix = '') {
    let result = {};
    for (const key in schema) {
        const newKey = prefix ? `${prefix}.${key}` : key;
        const definition = schema[key];
        result[newKey] = { ...definition, key: newKey };
        if (definition.properties) {
            result = { ...result, ...flattenSchema(definition.properties, newKey) };
        }
    }
    return result;
}
let _FLATTENED_SCHEMA;
/** Returns a flattened schema, the first call is memoized for future requests. */
export function getFlattenedSchema() {
    return (_FLATTENED_SCHEMA ??
        (_FLATTENED_SCHEMA = flattenSchema(getSettingsSchema())));
}
function clearFlattenedSchema() {
    _FLATTENED_SCHEMA = undefined;
}
/**
 * Get all settings grouped by category
 */
export function getSettingsByCategory() {
    const categories = {};
    Object.values(getFlattenedSchema()).forEach((definition) => {
        const category = definition.category;
        if (!categories[category]) {
            categories[category] = [];
        }
        categories[category].push(definition);
    });
    return categories;
}
/**
 * Get a setting definition by key
 */
export function getSettingDefinition(key) {
    return getFlattenedSchema()[key];
}
/**
 * Check if a setting requires restart
 */
export function requiresRestart(key) {
    return getFlattenedSchema()[key]?.requiresRestart ?? false;
}
/**
 * Get the default value for a setting
 */
export function getDefaultValue(key) {
    return getFlattenedSchema()[key]?.default;
}
/**
 * Get all setting keys that require restart
 */
export function getRestartRequiredSettings() {
    return Object.values(getFlattenedSchema())
        .filter((definition) => definition.requiresRestart)
        .map((definition) => definition.key);
}
/**
 * Recursively gets a value from a nested object using a key path array.
 */
export function getNestedValue(obj, path) {
    const [first, ...rest] = path;
    if (!first || !(first in obj)) {
        return undefined;
    }
    const value = obj[first];
    if (rest.length === 0) {
        return value;
    }
    if (value && typeof value === 'object' && value !== null) {
        return getNestedValue(value, rest);
    }
    return undefined;
}
export function getNestedProperty(obj, path) {
    return getNestedValue(obj, path.split('.'));
}
/**
 * Get the effective value for a setting, considering inheritance from higher scopes
 * Always returns a value (never undefined) - falls back to default if not set anywhere
 */
export function getEffectiveValue(key, settings, mergedSettings) {
    const definition = getSettingDefinition(key);
    if (!definition) {
        return undefined;
    }
    const path = key.split('.');
    // Check the current scope's settings first
    let value = getNestedValue(settings, path);
    if (value !== undefined) {
        return value;
    }
    // Check the merged settings for an inherited value
    value = getNestedValue(mergedSettings, path);
    if (value !== undefined) {
        return value;
    }
    // Return default value if no value is set anywhere
    return definition.default;
}
/**
 * Get all setting keys from the schema
 */
export function getAllSettingKeys() {
    return Object.keys(getFlattenedSchema());
}
/**
 * Get settings by type
 */
export function getSettingsByType(type) {
    return Object.values(getFlattenedSchema()).filter((definition) => definition.type === type);
}
/**
 * Get settings that require restart
 */
export function getSettingsRequiringRestart() {
    return Object.values(getFlattenedSchema()).filter((definition) => definition.requiresRestart);
}
/**
 * Validate if a setting key exists in the schema
 */
export function isValidSettingKey(key) {
    return key in getFlattenedSchema();
}
/**
 * Get the category for a setting
 */
export function getSettingCategory(key) {
    return getFlattenedSchema()[key]?.category;
}
/**
 * Check if a setting should be shown in the settings dialog
 */
export function shouldShowInDialog(key) {
    return getFlattenedSchema()[key]?.showInDialog ?? true; // Default to true for backward compatibility
}
/**
 * Get all settings that should be shown in the dialog, grouped by category
 */
export function getDialogSettingsByCategory() {
    const categories = {};
    Object.values(getFlattenedSchema())
        .filter((definition) => definition.showInDialog !== false)
        .forEach((definition) => {
        const category = definition.category;
        if (!categories[category]) {
            categories[category] = [];
        }
        categories[category].push(definition);
    });
    return categories;
}
/**
 * Get settings by type that should be shown in the dialog
 */
export function getDialogSettingsByType(type) {
    return Object.values(getFlattenedSchema()).filter((definition) => definition.type === type && definition.showInDialog !== false);
}
/**
 * Explicit display order for settings shown in the Settings Dialog.
 * Settings are ordered by importance and logical grouping:
 * 1. Workflow control (most impactful)
 * 2. Localization
 * 3. Editor/Shell experience
 * 4. Display preferences
 * 5. Git behavior
 * 6. File filtering
 * 7. System settings (rarely changed)
 *
 * New settings with showInDialog: true that are not listed here
 * will appear at the end of the list.
 */
const SETTINGS_DIALOG_ORDER = [
    // Workflow Control - most impactful setting
    'tools.approvalMode',
    // Localization - users often set this first
    'general.language',
    'general.outputLanguage',
    // Theme
    'ui.theme',
    // Editor/Shell Experience
    'general.vimMode',
    'tools.shell.enableInteractiveShell',
    // Display Preferences
    'general.preferredEditor',
    'ide.enabled',
    'ui.showLineNumbers',
    'ui.hideTips',
    'general.terminalBell',
    'ui.enableWelcomeBack',
    // Git Behavior
    'general.gitCoAuthor.commit',
    'general.gitCoAuthor.pr',
    // File Filtering
    'context.fileFiltering.respectGitIgnore',
    'context.fileFiltering.respectHopCodeIgnore',
    // System Settings - rarely changed
    'general.disableAutoUpdate',
    // Privacy
    'privacy.usageStatisticsEnabled',
];
/**
 * Get all setting keys that should be shown in the dialog, sorted by display order
 */
export function getDialogSettingKeys() {
    const dialogSettings = Object.values(getFlattenedSchema())
        .filter((definition) => definition.showInDialog === true)
        .map((definition) => definition.key);
    // Sort by explicit order; settings not in the order array appear at the end
    return dialogSettings.sort((a, b) => {
        const indexA = SETTINGS_DIALOG_ORDER.indexOf(a);
        const indexB = SETTINGS_DIALOG_ORDER.indexOf(b);
        // If both are in the order array, sort by their position
        if (indexA !== -1 && indexB !== -1) {
            return indexA - indexB;
        }
        // If only one is in the array, prioritize the one in the array
        if (indexA !== -1)
            return -1;
        if (indexB !== -1)
            return 1;
        // If neither is in the array, maintain original order
        return 0;
    });
}
// ============================================================================
// BUSINESS LOGIC UTILITIES (Higher-level utilities for setting operations)
// ============================================================================
/**
 * Get the current value for a setting in a specific scope
 * Always returns a value (never undefined) - falls back to default if not set anywhere
 */
export function getSettingValue(key, settings, mergedSettings) {
    const definition = getSettingDefinition(key);
    if (!definition) {
        return false; // Default fallback for invalid settings
    }
    const value = getEffectiveValue(key, settings, mergedSettings);
    // Ensure we return a boolean value, converting from the more general type
    if (typeof value === 'boolean') {
        return value;
    }
    // Fall back to default value, ensuring it's a boolean
    const defaultValue = definition.default;
    if (typeof defaultValue === 'boolean') {
        return defaultValue;
    }
    return false; // Final fallback
}
/**
 * Check if a setting value is modified from its default
 */
export function isSettingModified(key, value) {
    const defaultValue = getDefaultValue(key);
    // Handle type comparison properly
    if (typeof defaultValue === 'boolean') {
        return value !== defaultValue;
    }
    // If default is not a boolean, consider it modified if value is true
    return value === true;
}
/**
 * Check if a setting exists in the original settings file for a scope
 */
export function settingExistsInScope(key, scopeSettings) {
    const path = key.split('.');
    const value = getNestedValue(scopeSettings, path);
    return value !== undefined;
}
export function setNestedPropertyForce(obj, path, value) {
    const keys = path.split('.');
    const lastKey = keys.pop();
    if (!lastKey)
        return;
    let current = obj;
    for (const key of keys) {
        if (!current[key] || typeof current[key] !== 'object') {
            current[key] = {};
        }
        current = current[key];
    }
    current[lastKey] = value;
}
export function setNestedPropertySafe(obj, path, value) {
    const keys = path.split('.');
    const lastKey = keys.pop();
    if (!lastKey)
        return;
    let current = obj;
    for (const key of keys) {
        if (current[key] === undefined) {
            current[key] = {};
        }
        const next = current[key];
        if (typeof next === 'object' && next !== null) {
            current = next;
        }
        else {
            return;
        }
    }
    current[lastKey] = value;
}
export function deleteNestedPropertySafe(obj, path) {
    const keys = path.split('.');
    const lastKey = keys.pop();
    if (!lastKey)
        return;
    let current = obj;
    for (const key of keys) {
        const next = current[key];
        if (typeof next !== 'object' || next === null) {
            return;
        }
        current = next;
    }
    delete current[lastKey];
}
/**
 * Set a setting value in the pending settings
 */
export function setPendingSettingValue(key, value, pendingSettings) {
    const newSettings = JSON.parse(JSON.stringify(pendingSettings));
    setNestedPropertyForce(newSettings, key, value);
    return newSettings;
}
/**
 * Generic setter: Set a setting value (boolean, number, string, etc.) in the pending settings
 */
export function setPendingSettingValueAny(key, value, pendingSettings) {
    const newSettings = structuredClone(pendingSettings);
    setNestedPropertyForce(newSettings, key, value);
    return newSettings;
}
/**
 * Check if any modified settings require a restart
 */
export function hasRestartRequiredSettings(modifiedSettings) {
    return Array.from(modifiedSettings).some((key) => requiresRestart(key));
}
/**
 * Get the restart required settings from a set of modified settings
 */
export function getRestartRequiredFromModified(modifiedSettings) {
    return Array.from(modifiedSettings).filter((key) => requiresRestart(key));
}
/**
 * Save modified settings to the appropriate scope
 */
export function saveModifiedSettings(modifiedSettings, pendingSettings, loadedSettings, scope) {
    modifiedSettings.forEach((settingKey) => {
        const path = settingKey.split('.');
        const value = getNestedValue(pendingSettings, path);
        const existsInOriginalFile = settingExistsInScope(settingKey, loadedSettings.forScope(scope).settings);
        if (value === undefined) {
            // Treat `undefined` as "unset" when the key exists in the scope file.
            // LoadedSettings.setValue(..., undefined) is used elsewhere in the codebase
            // to remove optional settings from disk.
            if (existsInOriginalFile) {
                loadedSettings.setValue(scope, settingKey, undefined);
            }
            return;
        }
        const isDefaultValue = value === getDefaultValue(settingKey);
        if (existsInOriginalFile || !isDefaultValue) {
            loadedSettings.setValue(scope, settingKey, value);
        }
    });
}
/**
 * Get the display value for a setting, showing current scope value with default change indicator
 */
export function getDisplayValue(key, settings, _mergedSettings, modifiedSettings, pendingSettings) {
    // Prioritize pending changes if user has modified this setting
    const definition = getSettingDefinition(key);
    let value;
    if (pendingSettings && settingExistsInScope(key, pendingSettings)) {
        // Show the value from the pending (unsaved) edits when it exists
        value = getEffectiveValue(key, pendingSettings, {});
    }
    else if (settingExistsInScope(key, settings)) {
        // Show the value defined at the current scope if present
        value = getEffectiveValue(key, settings, {});
    }
    else {
        // Fall back to the schema default when the key is unset in this scope
        value = getDefaultValue(key);
    }
    let valueString = String(value);
    // Special handling for outputLanguage 'auto' value
    if (key === 'general.outputLanguage' && isAutoLanguage(value)) {
        valueString = t('Auto (detect from system)');
    }
    else if (definition?.type === 'enum' && definition.options) {
        const option = definition.options?.find((option) => option.value === value);
        if (option?.label) {
            valueString = t(option.label) || option.label;
        }
        else {
            valueString = `${value}`;
        }
    }
    // Check if value is different from default OR if it's in modified settings OR if there are pending changes
    const defaultValue = getDefaultValue(key);
    const isChangedFromDefault = value !== defaultValue;
    const isInModifiedSettings = modifiedSettings.has(key);
    // Mark as modified if setting exists in current scope OR is in modified settings
    if (settingExistsInScope(key, settings) || isInModifiedSettings) {
        return `${valueString}*`; // * indicates setting is set in current scope
    }
    if (isChangedFromDefault || isInModifiedSettings) {
        return `${valueString}*`; // * indicates changed from default value
    }
    return valueString;
}
/**
 * Check if a setting doesn't exist in current scope (should be greyed out)
 */
export function isDefaultValue(key, settings) {
    return !settingExistsInScope(key, settings);
}
/**
 * Check if a setting value is inherited (not set at current scope)
 */
export function isValueInherited(key, settings, _mergedSettings) {
    return !settingExistsInScope(key, settings);
}
/**
 * Get the effective value for display, considering inheritance
 * Always returns a boolean value (never undefined)
 */
export function getEffectiveDisplayValue(key, settings, mergedSettings) {
    return getSettingValue(key, settings, mergedSettings);
}
/**
 * Backup a settings file before modification.
 * Always creates a fresh backup with `.orig` suffix (overwrites any stale backup).
 * @param filePath - Path to the settings file to backup
 * @returns boolean indicating whether a backup was created
 */
export function backupSettingsFile(filePath) {
    try {
        if (fs.existsSync(filePath)) {
            const backupPath = `${filePath}.orig`;
            fs.copyFileSync(filePath, backupPath);
            return true;
        }
    }
    catch (_e) {
        // Ignore backup errors, proceed without backup
    }
    return false;
}
/**
 * Restore a settings file from its `.orig` backup created by {@link backupSettingsFile}.
 * Removes the backup file after a successful restore.
 * @param filePath - Path to the settings file to restore
 * @returns boolean indicating whether the restore succeeded
 */
export function restoreSettingsFromBackup(filePath) {
    try {
        const backupPath = `${filePath}.orig`;
        if (fs.existsSync(backupPath)) {
            fs.copyFileSync(backupPath, filePath);
            fs.unlinkSync(backupPath);
            return true;
        }
    }
    catch (_e) {
        // Ignore restore errors — caller should handle the failure
    }
    return false;
}
/**
 * Remove the `.orig` backup after a successful operation.
 * @param filePath - Path to the settings file whose backup should be removed
 */
export function cleanupSettingsBackup(filePath) {
    try {
        const backupPath = `${filePath}.orig`;
        if (fs.existsSync(backupPath)) {
            fs.unlinkSync(backupPath);
        }
    }
    catch (_e) {
        // Ignore cleanup errors — non-critical
    }
}
export const TEST_ONLY = { clearFlattenedSchema };
//# sourceMappingURL=settingsUtils.js.map