/**
 * Unified Config Loader
 *
 * Loads and saves the unified YAML configuration.
 * Provides fallback to legacy JSON format for backward compatibility.
 */

import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import * as yaml from 'js-yaml';
import { getCcsDir } from '../utils/config-manager';
import {
  isUnifiedConfig,
  createEmptyUnifiedConfig,
  UNIFIED_CONFIG_VERSION,
  DEFAULT_COPILOT_CONFIG,
  DEFAULT_CURSOR_CONFIG,
  DEFAULT_GLOBAL_ENV,
  DEFAULT_CLIPROXY_SERVER_CONFIG,
  DEFAULT_CLIPROXY_SAFETY_CONFIG,
  DEFAULT_OPENAI_COMPAT_PROXY_CONFIG,
  DEFAULT_QUOTA_MANAGEMENT_CONFIG,
  DEFAULT_THINKING_CONFIG,
  DEFAULT_OFFICIAL_CHANNELS_CONFIG,
  DEFAULT_DASHBOARD_AUTH_CONFIG,
  DEFAULT_BROWSER_CONFIG,
  DEFAULT_IMAGE_ANALYSIS_CONFIG,
  DEFAULT_LOGGING_CONFIG,
} from './unified-config-types';
import type {
  UnifiedConfig,
  CLIProxySafetyConfig,
  GlobalEnvConfig,
  ThinkingConfig,
  OfficialChannelsConfig,
  OfficialChannelId,
  DashboardAuthConfig,
  BrowserConfig,
  BrowserEvalMode,
  BrowserToolPolicy,
  ImageAnalysisConfig,
  LoggingConfig,
  CursorConfig,
  ContinuityConfig,
} from './unified-config-types';
import { validateCompositeTiers } from '../cliproxy/composite-validator';
import { isUnifiedConfigEnabled } from './feature-flags';
import {
  isOfficialChannelId,
  normalizeOfficialChannelIds,
  resolveLegacyDiscordSelection,
} from '../channels/official-channels-runtime';
import { getRecommendedBrowserUserDataDir } from '../utils/browser/browser-settings';
import { canonicalizeImageAnalysisConfig } from '../utils/hooks/image-analysis-backend-resolver';
import { normalizeSearxngBaseUrl } from '../utils/websearch/types';

const CONFIG_YAML = 'config.yaml';
const CONFIG_JSON = 'config.json';
const CONFIG_LOCK = 'config.yaml.lock';
const LOCK_STALE_MS = 5000; // Lock is stale after 5 seconds

function normalizeBrowserDevtoolsPort(value: number | undefined): number {
  if (!Number.isFinite(value)) {
    return DEFAULT_BROWSER_CONFIG.claude.devtools_port;
  }

  const port = Math.floor(value as number);
  if (port < 1 || port > 65535) {
    return DEFAULT_BROWSER_CONFIG.claude.devtools_port;
  }

  return port;
}

function normalizeBrowserPolicy(value: string | undefined): BrowserToolPolicy {
  return value === 'auto' || value === 'manual' ? value : DEFAULT_BROWSER_CONFIG.claude.policy;
}

function normalizeBrowserEvalMode(value: string | undefined): BrowserEvalMode {
  if (value === 'disabled' || value === 'readonly' || value === 'readwrite') {
    return value;
  }

  return DEFAULT_BROWSER_CONFIG.claude.eval_mode ?? 'readonly';
}

function canonicalizeBrowserConfig(
  config?: BrowserConfig,
  fallback: BrowserConfig = DEFAULT_BROWSER_CONFIG
): BrowserConfig {
  const claudeUserDataDir =
    config?.claude?.user_data_dir === undefined
      ? fallback.claude.user_data_dir || getRecommendedBrowserUserDataDir()
      : config.claude.user_data_dir.trim() || getRecommendedBrowserUserDataDir();

  return {
    claude: {
      enabled: config?.claude?.enabled ?? fallback.claude.enabled,
      policy: normalizeBrowserPolicy(config?.claude?.policy ?? fallback.claude.policy),
      user_data_dir: claudeUserDataDir,
      devtools_port: normalizeBrowserDevtoolsPort(
        config?.claude?.devtools_port ?? fallback.claude.devtools_port
      ),
      eval_mode: normalizeBrowserEvalMode(config?.claude?.eval_mode ?? fallback.claude.eval_mode),
    },
    codex: {
      enabled: config?.codex?.enabled ?? fallback.codex.enabled,
      policy: normalizeBrowserPolicy(config?.codex?.policy ?? fallback.codex.policy),
      eval_mode: normalizeBrowserEvalMode(config?.codex?.eval_mode ?? fallback.codex.eval_mode),
    },
  };
}

/**
 * Get path to unified config.yaml
 */
export function getConfigYamlPath(): string {
  return path.join(getCcsDir(), CONFIG_YAML);
}

/**
 * Get path to legacy config.json
 */
export function getConfigJsonPath(): string {
  return path.join(getCcsDir(), CONFIG_JSON);
}

/**
 * Get path to config lockfile
 */
function getLockFilePath(): string {
  return path.join(getCcsDir(), CONFIG_LOCK);
}

/**
 * Acquire lockfile for config write operations.
 * Returns a lock token if acquired, null if already locked by another process.
 * Cleans up stale locks (older than LOCK_STALE_MS).
 */

function acquireLock(): string | null {
  const lockPath = getLockFilePath();
  const lockDir = path.dirname(lockPath);
  const lockToken = crypto.randomUUID();
  const lockData = `${process.pid}\n${Date.now()}\n${lockToken}`;

  try {
    if (!fs.existsSync(lockDir)) {
      fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
    }

    // Check if lock exists
    if (fs.existsSync(lockPath)) {
      const content = fs.readFileSync(lockPath, 'utf8');
      const [pidStr, timestampStr] = content.trim().split('\n');
      const pid = Number.parseInt(pidStr, 10);
      const timestamp = Number.parseInt(timestampStr, 10);
      const hasLiveOwner = Number.isInteger(pid) && pid > 0 && processExists(pid);
      const isStale = !Number.isFinite(timestamp) || Date.now() - timestamp > LOCK_STALE_MS;

      if (hasLiveOwner) {
        return null;
      }

      if (isStale || !hasLiveOwner) {
        fs.unlinkSync(lockPath);
      }
    }

    // Acquire lock
    fs.writeFileSync(lockPath, lockData, { flag: 'wx', mode: 0o600 });
    return lockToken;
  } catch (error) {
    // EEXIST means another process acquired the lock between our check and write
    if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
      return null;
    }
    return null;
  }
}

/**
 * Release lockfile after config write operation.
 */
function releaseLock(lockToken: string): void {
  const lockPath = getLockFilePath();
  try {
    if (fs.existsSync(lockPath)) {
      const content = fs.readFileSync(lockPath, 'utf8');
      const fileToken = content.trim().split('\n')[2];
      if (fileToken === lockToken) {
        fs.unlinkSync(lockPath);
      }
    }
  } catch {
    // Ignore cleanup errors
  }
}

function processExists(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

/**
 * Check if unified config.yaml exists
 */
export function hasUnifiedConfig(): boolean {
  return fs.existsSync(getConfigYamlPath());
}

/**
 * Check if legacy config.json exists
 */
export function hasLegacyConfig(): boolean {
  return fs.existsSync(getConfigJsonPath());
}

/**
 * Determine which config format is active.
 * Returns 'yaml' if unified config exists or is enabled,
 * 'json' if only legacy config exists,
 * 'none' if no config exists.
 */
export function getConfigFormat(): 'yaml' | 'json' | 'none' {
  if (hasUnifiedConfig()) return 'yaml';
  if (isUnifiedConfigEnabled()) return 'yaml';
  if (hasLegacyConfig()) return 'json';
  return 'none';
}

/**
 * Load unified config from YAML file.
 * Returns null if file doesn't exist.
 * Auto-upgrades config if version is outdated (regenerates comments).
 */
export function loadUnifiedConfig(): UnifiedConfig | null {
  const yamlPath = getConfigYamlPath();

  // If file doesn't exist, return null
  if (!fs.existsSync(yamlPath)) {
    return null;
  }

  try {
    const content = fs.readFileSync(yamlPath, 'utf8');
    const parsed = yaml.load(content);

    if (!isUnifiedConfig(parsed)) {
      throw new Error(`Invalid config format in ${yamlPath}`);
    }

    // Auto-upgrade if version is outdated (regenerates YAML with new comments and fields)
    if ((parsed.version ?? 1) < UNIFIED_CONFIG_VERSION) {
      // Merge with defaults to add new fields (e.g., model for websearch providers)
      const upgraded = mergeWithDefaults(parsed);
      upgraded.version = UNIFIED_CONFIG_VERSION;
      try {
        saveUnifiedConfig(upgraded);
        if (process.env.CCS_DEBUG) {
          console.error(`[i] Config upgraded to v${UNIFIED_CONFIG_VERSION}`);
        }
        return upgraded;
      } catch (saveError) {
        console.error('[!] Config upgrade failed to save:', (saveError as Error).message);
        // Continue using the upgraded version in-memory even if save fails
      }
    }

    return parsed;
  } catch (err) {
    // U3: Provide better context for YAML syntax errors
    if (err instanceof yaml.YAMLException) {
      const mark = err.mark;
      console.error(`[X] YAML syntax error in ${yamlPath}:`);
      console.error(
        `    Line ${(mark?.line ?? 0) + 1}, Column ${(mark?.column ?? 0) + 1}: ${err.reason || 'Invalid syntax'}`
      );
      if (mark?.snippet) {
        console.error(`    ${mark.snippet}`);
      }
      console.error(
        `    Tip: Check for missing colons, incorrect indentation, or unquoted special characters.`
      );
    } else {
      const error = err instanceof Error ? err.message : 'Unknown error';
      console.error(`[X] Failed to load config: ${error}`);
    }
    throw err;
  }
}

/**
 * Validate composite variant provider strings.
 * Warns about invalid providers in composite variant configurations.
 */
function validateCompositeVariants(config: UnifiedConfig): void {
  const variants = config.cliproxy?.variants;
  if (!variants) return;

  for (const [name, variant] of Object.entries(variants)) {
    if ('type' in variant && variant.type === 'composite') {
      const error = validateCompositeTiers(variant.tiers, {
        defaultTier: variant.default_tier,
        requireAllTiers: true,
      });
      if (error) {
        console.warn(`[!] Variant '${name}': invalid composite config (${error})`);
      }
    }
  }
}

/**
 * Normalize continuity inheritance mapping payload.
 * Keeps only non-empty string keys and values.
 */
function normalizeContinuityInheritanceMap(value: unknown): Record<string, string> | undefined {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    return undefined;
  }

  const normalized: Record<string, string> = {};
  for (const [profileName, accountName] of Object.entries(value as Record<string, unknown>)) {
    const normalizedProfile = profileName.trim();
    const normalizedAccount = typeof accountName === 'string' ? accountName.trim() : '';

    if (!normalizedProfile || !normalizedAccount) {
      continue;
    }

    normalized[normalizedProfile] = normalizedAccount;
  }

  return Object.keys(normalized).length > 0 ? normalized : undefined;
}

/**
 * Normalize continuity section.
 * Supports legacy root key: continuity_inherit_from_account.
 */
function normalizeContinuityConfig(partial: Partial<UnifiedConfig>): ContinuityConfig | undefined {
  const legacyMap = normalizeContinuityInheritanceMap(
    (partial as Partial<UnifiedConfig> & { continuity_inherit_from_account?: unknown })
      .continuity_inherit_from_account
  );
  const continuityMap = normalizeContinuityInheritanceMap(partial.continuity?.inherit_from_account);

  if (!legacyMap && !continuityMap) {
    return undefined;
  }

  return {
    inherit_from_account: {
      ...(legacyMap ?? {}),
      ...(continuityMap ?? {}),
    },
  };
}

interface LegacyDiscordChannelsConfig {
  enabled?: boolean;
  unattended?: boolean;
}

function normalizeOfficialChannelsConfig(
  partial: Partial<UnifiedConfig> & { discord_channels?: LegacyDiscordChannelsConfig }
): OfficialChannelsConfig {
  const hasCanonicalChannelsSection = partial.channels !== undefined;
  const hasExplicitSelectedField =
    hasCanonicalChannelsSection &&
    Object.prototype.hasOwnProperty.call(partial.channels, 'selected');
  const rawSelected =
    hasExplicitSelectedField && Array.isArray(partial.channels?.selected)
      ? partial.channels.selected.filter((value): value is OfficialChannelId =>
          isOfficialChannelId(value)
        )
      : [];

  return {
    selected: hasCanonicalChannelsSection
      ? normalizeOfficialChannelIds(rawSelected)
      : resolveLegacyDiscordSelection(partial.discord_channels?.enabled),
    unattended:
      partial.channels?.unattended ??
      partial.discord_channels?.unattended ??
      DEFAULT_OFFICIAL_CHANNELS_CONFIG.unattended,
  };
}

/**
 * Merge partial config with defaults.
 * Preserves existing data while filling in missing sections.
 */
function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
  const defaults = createEmptyUnifiedConfig();
  const continuity = normalizeContinuityConfig(partial);
  return {
    version: partial.version ?? defaults.version,
    setup_completed: partial.setup_completed,
    default: partial.default ?? defaults.default,
    accounts: partial.accounts ?? defaults.accounts,
    profiles: partial.profiles ?? defaults.profiles,
    cliproxy: {
      ...partial.cliproxy,
      oauth_accounts: partial.cliproxy?.oauth_accounts ?? defaults.cliproxy.oauth_accounts,
      providers: defaults.cliproxy.providers, // Always use defaults for providers
      variants: partial.cliproxy?.variants ?? defaults.cliproxy.variants,
      logging: {
        enabled: partial.cliproxy?.logging?.enabled ?? defaults.cliproxy.logging?.enabled ?? false,
        request_log:
          partial.cliproxy?.logging?.request_log ?? defaults.cliproxy.logging?.request_log ?? false,
      },
      safety: {
        antigravity_ack_bypass:
          partial.cliproxy?.safety?.antigravity_ack_bypass ??
          DEFAULT_CLIPROXY_SAFETY_CONFIG.antigravity_ack_bypass,
      },
      // Kiro browser behavior setting (optional)
      kiro_no_incognito: partial.cliproxy?.kiro_no_incognito,
      // Auth config - preserve user values, no defaults (uses constants as fallback)
      auth: partial.cliproxy?.auth,
      // Background token refresh config (optional)
      token_refresh: partial.cliproxy?.token_refresh,
      // Backend selection - validate and preserve user choice (original vs plus)
      backend:
        partial.cliproxy?.backend === 'original' || partial.cliproxy?.backend === 'plus'
          ? partial.cliproxy.backend
          : undefined, // Invalid values become undefined (defaults to 'original' at runtime)
      // Auto-sync - default to true
      auto_sync: partial.cliproxy?.auto_sync ?? defaults.cliproxy.auto_sync ?? true,
      routing: {
        strategy:
          partial.cliproxy?.routing?.strategy === 'fill-first' ||
          partial.cliproxy?.routing?.strategy === 'round-robin'
            ? partial.cliproxy.routing.strategy
            : defaults.cliproxy.routing?.strategy,
      },
    },
    proxy: {
      port: partial.proxy?.port ?? DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port,
      profile_ports: partial.proxy?.profile_ports ?? {
        ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports,
      },
      routing: {
        default: partial.proxy?.routing?.default ?? defaults.proxy?.routing?.default,
        background: partial.proxy?.routing?.background ?? defaults.proxy?.routing?.background,
        think: partial.proxy?.routing?.think ?? defaults.proxy?.routing?.think,
        longContext: partial.proxy?.routing?.longContext ?? defaults.proxy?.routing?.longContext,
        webSearch: partial.proxy?.routing?.webSearch ?? defaults.proxy?.routing?.webSearch,
        longContextThreshold:
          partial.proxy?.routing?.longContextThreshold ??
          defaults.proxy?.routing?.longContextThreshold,
      },
    },
    logging: {
      enabled: partial.logging?.enabled ?? DEFAULT_LOGGING_CONFIG.enabled,
      level: partial.logging?.level ?? DEFAULT_LOGGING_CONFIG.level,
      rotate_mb: partial.logging?.rotate_mb ?? DEFAULT_LOGGING_CONFIG.rotate_mb,
      retain_days: partial.logging?.retain_days ?? DEFAULT_LOGGING_CONFIG.retain_days,
      redact: partial.logging?.redact ?? DEFAULT_LOGGING_CONFIG.redact,
      live_buffer_size:
        partial.logging?.live_buffer_size ?? DEFAULT_LOGGING_CONFIG.live_buffer_size,
    },
    preferences: {
      ...defaults.preferences,
      ...partial.preferences,
    },
    websearch: {
      enabled: partial.websearch?.enabled ?? defaults.websearch?.enabled ?? true,
      providers: {
        exa: {
          enabled: partial.websearch?.providers?.exa?.enabled ?? false,
          max_results: partial.websearch?.providers?.exa?.max_results ?? 5,
        },
        tavily: {
          enabled: partial.websearch?.providers?.tavily?.enabled ?? false,
          max_results: partial.websearch?.providers?.tavily?.max_results ?? 5,
        },
        brave: {
          enabled: partial.websearch?.providers?.brave?.enabled ?? false,
          max_results: partial.websearch?.providers?.brave?.max_results ?? 5,
        },
        searxng: {
          enabled: partial.websearch?.providers?.searxng?.enabled ?? false,
          url: normalizeSearxngBaseUrl(partial.websearch?.providers?.searxng?.url) ?? '',
          max_results: partial.websearch?.providers?.searxng?.max_results ?? 5,
        },
        duckduckgo: {
          enabled: partial.websearch?.providers?.duckduckgo?.enabled ?? true,
          max_results: partial.websearch?.providers?.duckduckgo?.max_results ?? 5,
        },
        gemini: {
          enabled:
            partial.websearch?.providers?.gemini?.enabled ??
            partial.websearch?.gemini?.enabled ?? // Legacy fallback
            false,
          model: partial.websearch?.providers?.gemini?.model ?? 'gemini-2.5-flash',
          timeout:
            partial.websearch?.providers?.gemini?.timeout ??
            partial.websearch?.gemini?.timeout ?? // Legacy fallback
            55,
        },
        opencode: {
          enabled: partial.websearch?.providers?.opencode?.enabled ?? false,
          model: partial.websearch?.providers?.opencode?.model ?? 'opencode/grok-code',
          timeout: partial.websearch?.providers?.opencode?.timeout ?? 90,
        },
        grok: {
          enabled: partial.websearch?.providers?.grok?.enabled ?? false,
          timeout: partial.websearch?.providers?.grok?.timeout ?? 55,
        },
      },
      // Legacy fields (keep for backwards compatibility during read)
      gemini: partial.websearch?.gemini,
    },
    // Copilot config - strictly opt-in, merge with defaults
    copilot: {
      enabled: partial.copilot?.enabled ?? DEFAULT_COPILOT_CONFIG.enabled,
      auto_start: partial.copilot?.auto_start ?? DEFAULT_COPILOT_CONFIG.auto_start,
      port: partial.copilot?.port ?? DEFAULT_COPILOT_CONFIG.port,
      account_type: partial.copilot?.account_type ?? DEFAULT_COPILOT_CONFIG.account_type,
      rate_limit: partial.copilot?.rate_limit ?? DEFAULT_COPILOT_CONFIG.rate_limit,
      wait_on_limit: partial.copilot?.wait_on_limit ?? DEFAULT_COPILOT_CONFIG.wait_on_limit,
      model: partial.copilot?.model ?? DEFAULT_COPILOT_CONFIG.model,
    },
    // Cursor config - disabled by default, merge with defaults
    cursor: {
      enabled: partial.cursor?.enabled ?? DEFAULT_CURSOR_CONFIG.enabled,
      port: partial.cursor?.port ?? DEFAULT_CURSOR_CONFIG.port,
      auto_start: partial.cursor?.auto_start ?? DEFAULT_CURSOR_CONFIG.auto_start,
      ghost_mode: partial.cursor?.ghost_mode ?? DEFAULT_CURSOR_CONFIG.ghost_mode,
      model: partial.cursor?.model ?? DEFAULT_CURSOR_CONFIG.model,
      opus_model: partial.cursor?.opus_model,
      sonnet_model: partial.cursor?.sonnet_model,
      haiku_model: partial.cursor?.haiku_model,
    },
    // Global env - injected into all non-Claude subscription profiles
    global_env: {
      enabled: partial.global_env?.enabled ?? true,
      env: partial.global_env?.env ?? { ...DEFAULT_GLOBAL_ENV },
    },
    continuity,
    // CLIProxy server config - remote/local CLIProxyAPI settings
    cliproxy_server: {
      remote: {
        enabled:
          partial.cliproxy_server?.remote?.enabled ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.enabled,
        host: partial.cliproxy_server?.remote?.host ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.host,
        // Port is optional - undefined means use protocol default (443 for HTTPS, 8317 for HTTP)
        port: partial.cliproxy_server?.remote?.port,
        protocol:
          partial.cliproxy_server?.remote?.protocol ??
          DEFAULT_CLIPROXY_SERVER_CONFIG.remote.protocol,
        auth_token:
          partial.cliproxy_server?.remote?.auth_token ??
          DEFAULT_CLIPROXY_SERVER_CONFIG.remote.auth_token,
        // management_key is optional - falls back to auth_token when not set
        management_key: partial.cliproxy_server?.remote?.management_key,
      },
      fallback: {
        enabled:
          partial.cliproxy_server?.fallback?.enabled ??
          DEFAULT_CLIPROXY_SERVER_CONFIG.fallback.enabled,
        auto_start:
          partial.cliproxy_server?.fallback?.auto_start ??
          DEFAULT_CLIPROXY_SERVER_CONFIG.fallback.auto_start,
      },
      local: {
        port: partial.cliproxy_server?.local?.port ?? DEFAULT_CLIPROXY_SERVER_CONFIG.local.port,
        auto_start:
          partial.cliproxy_server?.local?.auto_start ??
          DEFAULT_CLIPROXY_SERVER_CONFIG.local.auto_start,
      },
    },
    // Quota management config - hybrid auto+manual account selection
    quota_management: {
      mode: partial.quota_management?.mode ?? DEFAULT_QUOTA_MANAGEMENT_CONFIG.mode,
      auto: {
        preflight_check:
          partial.quota_management?.auto?.preflight_check ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.preflight_check,
        exhaustion_threshold:
          partial.quota_management?.auto?.exhaustion_threshold ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.exhaustion_threshold,
        tier_priority:
          partial.quota_management?.auto?.tier_priority ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.tier_priority,
        cooldown_minutes:
          partial.quota_management?.auto?.cooldown_minutes ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.cooldown_minutes,
      },
      manual: {
        paused_accounts:
          partial.quota_management?.manual?.paused_accounts ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.paused_accounts,
        forced_default:
          partial.quota_management?.manual?.forced_default ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.forced_default,
        tier_lock:
          partial.quota_management?.manual?.tier_lock ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.tier_lock,
      },
      runtime_monitor: {
        enabled:
          partial.quota_management?.runtime_monitor?.enabled ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.enabled,
        normal_interval_seconds:
          partial.quota_management?.runtime_monitor?.normal_interval_seconds ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.normal_interval_seconds,
        critical_interval_seconds:
          partial.quota_management?.runtime_monitor?.critical_interval_seconds ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.critical_interval_seconds,
        warn_threshold:
          partial.quota_management?.runtime_monitor?.warn_threshold ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.warn_threshold,
        exhaustion_threshold:
          partial.quota_management?.runtime_monitor?.exhaustion_threshold ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.exhaustion_threshold,
        cooldown_minutes:
          partial.quota_management?.runtime_monitor?.cooldown_minutes ??
          DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.cooldown_minutes,
      },
    },
    // Thinking config - auto/manual/off control for reasoning budget
    thinking: {
      mode: partial.thinking?.mode ?? DEFAULT_THINKING_CONFIG.mode,
      override: partial.thinking?.override,
      tier_defaults: {
        opus: partial.thinking?.tier_defaults?.opus ?? DEFAULT_THINKING_CONFIG.tier_defaults.opus,
        sonnet:
          partial.thinking?.tier_defaults?.sonnet ?? DEFAULT_THINKING_CONFIG.tier_defaults.sonnet,
        haiku:
          partial.thinking?.tier_defaults?.haiku ?? DEFAULT_THINKING_CONFIG.tier_defaults.haiku,
      },
      provider_overrides: partial.thinking?.provider_overrides,
      show_warnings: partial.thinking?.show_warnings ?? DEFAULT_THINKING_CONFIG.show_warnings,
    },
    channels: normalizeOfficialChannelsConfig(
      partial as Partial<UnifiedConfig> & { discord_channels?: LegacyDiscordChannelsConfig }
    ),
    // Dashboard auth config - disabled by default
    dashboard_auth: {
      enabled: partial.dashboard_auth?.enabled ?? DEFAULT_DASHBOARD_AUTH_CONFIG.enabled,
      username: partial.dashboard_auth?.username ?? DEFAULT_DASHBOARD_AUTH_CONFIG.username,
      password_hash:
        partial.dashboard_auth?.password_hash ?? DEFAULT_DASHBOARD_AUTH_CONFIG.password_hash,
      session_timeout_hours:
        partial.dashboard_auth?.session_timeout_hours ??
        DEFAULT_DASHBOARD_AUTH_CONFIG.session_timeout_hours,
    },
    browser: canonicalizeBrowserConfig(partial.browser),
    // Image analysis config - enabled by default for CLIProxy providers
    image_analysis: canonicalizeImageAnalysisConfig({
      enabled: partial.image_analysis?.enabled ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.enabled,
      timeout: partial.image_analysis?.timeout ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.timeout,
      provider_models:
        partial.image_analysis?.provider_models ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.provider_models,
      fallback_backend:
        partial.image_analysis?.fallback_backend ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.fallback_backend,
      profile_backends:
        partial.image_analysis?.profile_backends ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.profile_backends,
    }),
  };
}

/**
 * Load config, preferring YAML if available, falling back to creating empty config.
 * Merges with defaults to ensure all sections exist.
 */
export function loadOrCreateUnifiedConfig(): UnifiedConfig {
  const existing = loadUnifiedConfig();
  if (existing) {
    // Merge with defaults to fill any missing sections
    const merged = mergeWithDefaults(existing);
    // Validate composite variant provider strings
    validateCompositeVariants(merged);
    return merged;
  }

  // Create empty config
  const config = createEmptyUnifiedConfig();
  return config;
}

/**
 * Generate YAML header with helpful comments.
 */
function generateYamlHeader(): string {
  return `# CCS Unified Configuration
# Docs: https://github.com/kaitranntt/ccs
`;
}

/**
 * Generate YAML content with section comments for better readability.
 */
function generateYamlWithComments(config: UnifiedConfig): string {
  const lines: string[] = [];

  // Version
  lines.push(`version: ${config.version}`);
  if (config.setup_completed !== undefined) {
    lines.push(`setup_completed: ${config.setup_completed}`);
  }
  lines.push('');

  // Default
  if (config.default) {
    lines.push(`# Default profile used when running 'ccs' without arguments`);
    lines.push(`default: "${config.default}"`);
    lines.push('');
  }

  // Accounts section
  lines.push('# ----------------------------------------------------------------------------');
  lines.push('# Accounts: Isolated Claude instances (each with separate auth/sessions)');
  lines.push('# Manage with: ccs auth add <name>, ccs auth list, ccs auth remove <name>');
  lines.push('# ----------------------------------------------------------------------------');
  lines.push(
    yaml.dump({ accounts: config.accounts }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
  );
  lines.push('');

  // Profiles section
  lines.push('# ----------------------------------------------------------------------------');
  lines.push('# Profiles: API-based providers (GLM, Kimi, custom endpoints)');
  lines.push('# Each profile points to a *.settings.json file containing env vars.');
  lines.push('# Edit the settings file directly to customize (ANTHROPIC_MAX_TOKENS, etc.)');
  lines.push('# ----------------------------------------------------------------------------');
  lines.push(
    yaml.dump({ profiles: config.profiles }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
  );
  lines.push('');

  // CLIProxy section
  lines.push('# ----------------------------------------------------------------------------');
  lines.push('# CLIProxy: OAuth-based providers (gemini, codex, agy, qwen, iflow)');
  lines.push('# Each variant can reference a *.settings.json file for custom env vars.');
  lines.push('# Edit the settings file directly to customize model or other settings.');
  lines.push('# ----------------------------------------------------------------------------');
  lines.push(
    yaml.dump({ cliproxy: config.cliproxy }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
  );
  lines.push('');

  if (config.proxy?.routing) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Proxy Routing: OpenAI-compatible local proxy model selection rules');
    lines.push('# Use profile:model selectors to force a target profile and upstream model.');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml.dump({ proxy: config.proxy }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
    );
    lines.push('');
  }

  if (config.logging) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Logging: CCS-owned structured runtime logs');
    lines.push('# Current file: ~/.ccs/logs/current.jsonl');
    lines.push('# Archives rotate automatically and are pruned by retain_days.');
    lines.push('# This is separate from cliproxy.logging, which controls CLIProxy runtime files.');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml.dump({ logging: config.logging }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
    );
    lines.push('');
  }

  // CLIProxy Server section (remote proxy configuration) - placed right after cliproxy
  if (config.cliproxy_server) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# CLIProxy Server: Remote proxy connection settings');
    lines.push('# Configure via Dashboard (`ccs config`) > Proxy tab.');
    lines.push('#');
    lines.push('# remote: Connect to a remote CLIProxyAPI instance');
    lines.push('# fallback: Use local proxy if remote is unreachable');
    lines.push('# local: Local proxy settings (port, auto-start)');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml
        .dump(
          { cliproxy_server: config.cliproxy_server },
          { indent: 2, lineWidth: -1, quotingType: '"' }
        )
        .trim()
    );
    lines.push('');
  }

  // Preferences section
  lines.push('# ----------------------------------------------------------------------------');
  lines.push('# Preferences: User settings');
  lines.push('# ----------------------------------------------------------------------------');
  lines.push(
    yaml
      .dump({ preferences: config.preferences }, { indent: 2, lineWidth: -1, quotingType: '"' })
      .trim()
  );
  lines.push('');

  // WebSearch section
  if (config.websearch) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# WebSearch: real search backends for third-party profiles');
    lines.push('# Dashboard (`ccs config`) is the source of truth for provider selection.');
    lines.push('#');
    lines.push('# Third-party providers (gemini, codex, agy, etc.) do not have access to');
    lines.push("# Anthropic's WebSearch tool. CCS intercepts that tool and runs local search.");
    lines.push('#');
    lines.push(
      '# Priority: Exa -> Tavily -> Brave -> DuckDuckGo -> optional legacy AI CLI fallbacks'
    );
    lines.push('#');
    lines.push('# Exa requires EXA_API_KEY in your environment.');
    lines.push('# Tavily requires TAVILY_API_KEY in your environment.');
    lines.push('# Brave requires BRAVE_API_KEY in your environment.');
    lines.push('# DuckDuckGo works with zero extra setup and is enabled by default.');
    lines.push('#');
    lines.push('# Legacy LLM fallbacks remain optional if you still want them:');
    lines.push('#   gemini: npm i -g @google/gemini-cli');
    lines.push('#   opencode: curl -fsSL https://opencode.ai/install | bash');
    lines.push('#   grok: npm i -g @vibe-kit/grok-cli');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml
        .dump({ websearch: config.websearch }, { indent: 2, lineWidth: -1, quotingType: '"' })
        .trim()
    );
    lines.push('');
  }

  // Copilot section (GitHub Copilot proxy)
  if (config.copilot) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Copilot: GitHub Copilot API proxy (via copilot-api)');
    lines.push('# Uses your existing GitHub Copilot subscription with Claude Code.');
    lines.push('#');
    lines.push('# !! DISCLAIMER - USE AT YOUR OWN RISK !!');
    lines.push('# This uses an UNOFFICIAL reverse-engineered API.');
    lines.push('# Excessive usage may trigger GitHub account restrictions.');
    lines.push('# CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for consequences.');
    lines.push('#');
    lines.push('# Setup: npx copilot-api auth (authenticate with GitHub)');
    lines.push('# Usage: ccs copilot (switch to copilot profile)');
    lines.push('#');
    lines.push('# Models: claude-sonnet-4.5, claude-opus-4.5, gpt-5.1, gemini-2.5-pro');
    lines.push('# Account types: individual, business, enterprise');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml.dump({ copilot: config.copilot }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
    );
    lines.push('');
  }

  // Cursor section (Cursor IDE proxy daemon)
  if (config.cursor) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Cursor: Cursor IDE proxy daemon');
    lines.push('# Enables Cursor IDE integration via local proxy daemon.');
    lines.push('#');
    lines.push('# enabled: Enable/disable Cursor integration (default: false)');
    lines.push('# port: Port for cursor proxy daemon (default: 20129)');
    lines.push('# auto_start: Auto-start daemon when CCS starts (default: false)');
    lines.push('# ghost_mode: Disable telemetry for privacy (default: true)');
    lines.push('# model: Default model ID (used for ANTHROPIC_MODEL)');
    lines.push('# opus_model/sonnet_model/haiku_model: Optional tier model mapping');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml.dump({ cursor: config.cursor }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
    );
    lines.push('');
  }

  // Global env section
  if (config.global_env) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      '# Global Environment Variables: Injected into all non-Claude subscription profiles'
    );
    lines.push('# These env vars disable telemetry/reporting for third-party providers.');
    lines.push('# Configure via Dashboard (`ccs config`) > Global Env tab.');
    lines.push('#');
    lines.push('# Default variables:');
    lines.push('#   DISABLE_BUG_COMMAND: Disables /bug command (not supported by proxy)');
    lines.push('#   DISABLE_ERROR_REPORTING: Disables error reporting to Anthropic');
    lines.push('#   DISABLE_TELEMETRY: Disables usage telemetry');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml
        .dump({ global_env: config.global_env }, { indent: 2, lineWidth: -1, quotingType: '"' })
        .trim()
    );
    lines.push('');
  }

  // Continuity inheritance section
  if (config.continuity?.inherit_from_account) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Continuity Inheritance: Reuse account continuity artifacts across profiles');
    lines.push('# Map execution profile names to source account profiles (CLAUDE_CONFIG_DIR).');
    lines.push('# Applies to Claude target only; credentials remain profile-specific.');
    lines.push('# Example: continuity.inherit_from_account.glm: pro');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml
        .dump({ continuity: config.continuity }, { indent: 2, lineWidth: -1, quotingType: '"' })
        .trim()
    );
    lines.push('');
  }

  // Thinking section (extended thinking/reasoning configuration)
  if (config.thinking) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Thinking: Extended thinking/reasoning budget configuration');
    lines.push('# Controls reasoning depth for supported providers (agy, gemini, codex).');
    lines.push('#');
    lines.push(
      '# Modes: auto (use tier_defaults), off (disable), manual (--thinking/--effort flags)'
    );
    lines.push(
      '# Levels: minimal (512), low (1K), medium (8K), high (24K), xhigh (32K), max (adaptive ceiling), auto'
    );
    lines.push('# Override: Set global override value (number or level name)');
    lines.push('# Provider overrides: Per-provider tier defaults');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml
        .dump({ thinking: config.thinking }, { indent: 2, lineWidth: -1, quotingType: '"' })
        .trim()
    );
    lines.push('');
  }

  // Official Channels section
  if (config.channels) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Official Channels: Runtime auto-enable for Anthropic official channel plugins');
    lines.push('# Supported channels: telegram, discord, imessage');
    lines.push('# Runtime-only: CCS injects --channels at launch for compatible Claude sessions.');
    lines.push('# Bot tokens live in Claude channel env files, not in config.yaml.');
    lines.push('# Use selected: [telegram, discord, imessage] to choose channels.');
    lines.push(
      '# unattended adds --dangerously-skip-permissions only when channel auto-enable is active.'
    );
    lines.push('# Compatible sessions: native Claude default/account profiles only.');
    lines.push('# Configure via: ccs config channels or the Settings > Channels dashboard tab.');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml
        .dump({ channels: config.channels }, { indent: 2, lineWidth: -1, quotingType: '"' })
        .trim()
    );
    lines.push('');
  }

  // Dashboard auth section (only if configured)
  if (config.dashboard_auth?.enabled) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Dashboard Auth: Optional login protection for CCS dashboard');
    lines.push('# Generate password hash: npx bcrypt-cli hash "your-password"');
    lines.push(
      '# ENV override: CCS_DASHBOARD_AUTH_ENABLED, CCS_DASHBOARD_USERNAME, CCS_DASHBOARD_PASSWORD_HASH'
    );
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml
        .dump(
          { dashboard_auth: config.dashboard_auth },
          { indent: 2, lineWidth: -1, quotingType: '"' }
        )
        .trim()
    );
    lines.push('');
  }

  // Browser automation section
  if (config.browser) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Browser Automation: Claude browser attach and Codex browser tooling');
    lines.push('# Claude attach reuses a running Chrome/Chromium session with remote debugging.');
    lines.push('# Codex tooling controls whether CCS injects Playwright MCP overrides.');
    lines.push('#');
    lines.push('# claude.user_data_dir should point at the Chrome user-data directory for the');
    lines.push('# dedicated attach session. claude.devtools_port is the expected debugging port.');
    lines.push('# Configure via: Settings > Browser or `ccs browser ...`.');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml.dump({ browser: config.browser }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
    );
    lines.push('');
  }

  // Image analysis section
  if (config.image_analysis) {
    lines.push('# ----------------------------------------------------------------------------');
    lines.push('# Image Analysis: Vision-based analysis for images and PDFs');
    lines.push('# Routes Read tool requests for images/PDFs through CLIProxy vision API.');
    lines.push('#');
    lines.push('# When enabled: Image files trigger vision analysis instead of raw file read');
    lines.push('# Provider models: Vision model used for each CLIProxy provider');
    lines.push('# Timeout: Maximum seconds to wait for analysis (10-600)');
    lines.push('#');
    lines.push('# Supported formats: .jpg, .jpeg, .png, .gif, .webp, .heic, .bmp, .tiff, .pdf');
    lines.push('# Configure via: ccs config image-analysis');
    lines.push('# ----------------------------------------------------------------------------');
    lines.push(
      yaml
        .dump(
          { image_analysis: config.image_analysis },
          { indent: 2, lineWidth: -1, quotingType: '"' }
        )
        .trim()
    );
    lines.push('');
  }

  return lines.join('\n');
}

/**
 * Sync sleep helper for lock retry loops.
 * Uses Atomics.wait when available to avoid CPU-intensive busy-wait.
 */
function sleepSync(ms: number): void {
  try {
    Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
  } catch {
    const end = Date.now() + ms;
    while (Date.now() < end) {
      /* busy-wait */
    }
  }
}

/**
 * Execute a callback while holding the config lock.
 */
function withConfigWriteLock<T>(callback: () => T): T {
  // Acquire lock (retry for up to 1 second)
  const maxRetries = 10;
  const retryDelayMs = 100;
  let lockToken: string | null = null;
  for (let i = 0; i < maxRetries; i++) {
    const acquiredToken = acquireLock();
    if (acquiredToken) {
      lockToken = acquiredToken;
      break;
    }
    sleepSync(retryDelayMs);
  }

  if (!lockToken) {
    throw new Error('Config file is locked by another process. Wait a moment and try again.');
  }

  try {
    return callback();
  } finally {
    // Always release lock
    releaseLock(lockToken);
  }
}

/**
 * Load unified config directly from disk while lock is already held.
 * Falls back to empty config when file doesn't exist.
 */
function loadUnifiedConfigWithLockHeld(): UnifiedConfig {
  const yamlPath = getConfigYamlPath();
  if (!fs.existsSync(yamlPath)) {
    return createEmptyUnifiedConfig();
  }

  const content = fs.readFileSync(yamlPath, 'utf8');
  const parsed = yaml.load(content);

  if (!isUnifiedConfig(parsed)) {
    throw new Error(`Invalid config format in ${yamlPath}`);
  }

  const merged = mergeWithDefaults(parsed);
  validateCompositeVariants(merged);
  return merged;
}

/**
 * Write unified config to disk while lock is already held.
 */
function writeUnifiedConfigWithLockHeld(config: UnifiedConfig): void {
  const yamlPath = getConfigYamlPath();
  const dir = path.dirname(yamlPath);

  // Ensure directory exists
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
  }

  // Ensure version is set
  config.version = UNIFIED_CONFIG_VERSION;

  // Generate YAML with section comments
  const yamlContent = generateYamlWithComments(config);
  const content = generateYamlHeader() + yamlContent;

  // Atomic write: write to temp file, then rename
  const tempPath = `${yamlPath}.tmp.${process.pid}`;

  try {
    fs.writeFileSync(tempPath, content, { mode: 0o600 });
    fs.renameSync(tempPath, yamlPath);
  } catch (error) {
    // Clean up temp file on error
    if (fs.existsSync(tempPath)) {
      try {
        fs.unlinkSync(tempPath);
      } catch {
        // Ignore cleanup errors
      }
    }
    // Classify filesystem errors
    const err = error as NodeJS.ErrnoException;
    if (err.code === 'ENOSPC') {
      throw new Error('Disk full - cannot save config. Free up space and try again.');
    } else if (err.code === 'EROFS' || err.code === 'EACCES') {
      throw new Error(`Cannot write config - check file permissions: ${err.message}`);
    }
    throw error;
  }
}

/**
 * Save unified config to YAML file.
 * Uses atomic write (temp file + rename) to prevent corruption.
 * Uses lockfile to prevent concurrent writes.
 */
export function saveUnifiedConfig(config: UnifiedConfig): void {
  withConfigWriteLock(() => {
    writeUnifiedConfigWithLockHeld(config);
  });
}

/**
 * Atomically mutate unified config with lock held across read-modify-write.
 * Prevents stale writes from overwriting concurrent updates.
 */
export function mutateUnifiedConfig(mutator: (config: UnifiedConfig) => void): UnifiedConfig {
  return withConfigWriteLock(() => {
    const current = loadUnifiedConfigWithLockHeld();
    const previousBrowser = current.browser
      ? canonicalizeBrowserConfig(current.browser)
      : undefined;
    mutator(current);
    if (current.browser) {
      current.browser = canonicalizeBrowserConfig(current.browser, previousBrowser);
    }
    writeUnifiedConfigWithLockHeld(current);
    return current;
  });
}

/**
 * Update unified config with partial data.
 * Loads existing config, merges changes, and saves.
 */
export function updateUnifiedConfig(updates: Partial<UnifiedConfig>): UnifiedConfig {
  return mutateUnifiedConfig((config) => {
    Object.assign(config, updates);
  });
}

/**
 * Check if unified config mode is active.
 * Returns true if config.yaml exists OR CCS_UNIFIED_CONFIG=1.
 *
 * Use this centralized function instead of duplicating the logic.
 */
export function isUnifiedMode(): boolean {
  return hasUnifiedConfig() || isUnifiedConfigEnabled();
}

/**
 * Get or set default profile name.
 */
export function getDefaultProfile(): string | undefined {
  const config = loadUnifiedConfig();
  return config?.default;
}

export function setDefaultProfile(name: string): void {
  updateUnifiedConfig({ default: name });
}

/**
 * Gemini CLI WebSearch configuration
 */
export interface GeminiWebSearchInfo {
  enabled: boolean;
  model: string;
  timeout: number;
}

/**
 * Get websearch configuration.
 * Returns defaults if not configured.
 * Supports deterministic providers and optional Gemini/OpenCode/Grok CLI fallbacks.
 */
export function getWebSearchConfig(): {
  enabled: boolean;
  providers?: {
    exa?: { enabled?: boolean; max_results?: number };
    tavily?: { enabled?: boolean; max_results?: number };
    brave?: { enabled?: boolean; max_results?: number };
    searxng?: { enabled?: boolean; url?: string; max_results?: number };
    duckduckgo?: { enabled?: boolean; max_results?: number };
    gemini?: GeminiWebSearchInfo;
    opencode?: { enabled?: boolean; model?: string; timeout?: number };
    grok?: { enabled?: boolean; timeout?: number };
  };
  // Legacy fields (deprecated)
  gemini?: { enabled?: boolean; timeout?: number };
} {
  const config = loadOrCreateUnifiedConfig();

  // Build provider configs
  const exaConfig = {
    enabled: config.websearch?.providers?.exa?.enabled ?? false,
    max_results: config.websearch?.providers?.exa?.max_results ?? 5,
  };

  const tavilyConfig = {
    enabled: config.websearch?.providers?.tavily?.enabled ?? false,
    max_results: config.websearch?.providers?.tavily?.max_results ?? 5,
  };

  const duckDuckGoConfig = {
    enabled: config.websearch?.providers?.duckduckgo?.enabled ?? true,
    max_results: config.websearch?.providers?.duckduckgo?.max_results ?? 5,
  };

  const braveConfig = {
    enabled: config.websearch?.providers?.brave?.enabled ?? false,
    max_results: config.websearch?.providers?.brave?.max_results ?? 5,
  };

  const searxngConfig = {
    enabled: config.websearch?.providers?.searxng?.enabled ?? false,
    url: normalizeSearxngBaseUrl(config.websearch?.providers?.searxng?.url) ?? '',
    max_results: config.websearch?.providers?.searxng?.max_results ?? 5,
  };

  const geminiConfig: GeminiWebSearchInfo = {
    enabled:
      config.websearch?.providers?.gemini?.enabled ?? config.websearch?.gemini?.enabled ?? false,
    model: config.websearch?.providers?.gemini?.model ?? 'gemini-2.5-flash',
    timeout:
      config.websearch?.providers?.gemini?.timeout ?? config.websearch?.gemini?.timeout ?? 55,
  };

  const opencodeConfig = {
    enabled: config.websearch?.providers?.opencode?.enabled ?? false,
    model: config.websearch?.providers?.opencode?.model ?? 'opencode/grok-code',
    timeout: config.websearch?.providers?.opencode?.timeout ?? 90,
  };

  const grokConfig = {
    enabled: config.websearch?.providers?.grok?.enabled ?? false,
    timeout: config.websearch?.providers?.grok?.timeout ?? 55,
  };

  // Auto-enable master switch if ANY provider is enabled
  const anyProviderEnabled =
    exaConfig.enabled ||
    tavilyConfig.enabled ||
    braveConfig.enabled ||
    searxngConfig.enabled ||
    duckDuckGoConfig.enabled ||
    geminiConfig.enabled ||
    opencodeConfig.enabled ||
    grokConfig.enabled;
  const enabled = anyProviderEnabled && (config.websearch?.enabled ?? true);

  return {
    enabled,
    providers: {
      exa: exaConfig,
      tavily: tavilyConfig,
      brave: braveConfig,
      searxng: searxngConfig,
      duckduckgo: duckDuckGoConfig,
      gemini: geminiConfig,
      opencode: opencodeConfig,
      grok: grokConfig,
    },
    // Legacy field for backwards compatibility
    gemini: config.websearch?.gemini,
  };
}

/**
 * Get global_env configuration.
 * Returns defaults if not configured.
 */
export function getGlobalEnvConfig(): GlobalEnvConfig {
  const config = loadOrCreateUnifiedConfig();
  return {
    enabled: config.global_env?.enabled ?? true,
    env: config.global_env?.env ?? { ...DEFAULT_GLOBAL_ENV },
  };
}

/**
 * Get continuity inheritance mapping.
 * Returns empty mapping when not configured.
 */
export function getContinuityInheritanceMap(): Record<string, string> {
  const config = loadOrCreateUnifiedConfig();
  return config.continuity?.inherit_from_account ?? {};
}

/**
 * Get cliproxy safety configuration.
 * Returns defaults if not configured.
 */
export function getCliproxySafetyConfig(): CLIProxySafetyConfig {
  const config = loadOrCreateUnifiedConfig();
  return {
    antigravity_ack_bypass:
      config.cliproxy?.safety?.antigravity_ack_bypass ??
      DEFAULT_CLIPROXY_SAFETY_CONFIG.antigravity_ack_bypass,
  };
}

/**
 * Get thinking configuration.
 * Returns defaults if not configured.
 */
export function getThinkingConfig(): ThinkingConfig {
  const config = loadOrCreateUnifiedConfig();

  // W2: Check for invalid thinking config (e.g., thinking: true instead of object)
  if (config.thinking !== undefined && typeof config.thinking !== 'object') {
    console.warn(
      `[!] Invalid thinking config: expected object, got ${typeof config.thinking}. Using defaults.`
    );
    console.warn(`    Tip: Use 'thinking: { mode: auto }' instead of 'thinking: true'`);
    return DEFAULT_THINKING_CONFIG;
  }

  return {
    mode: config.thinking?.mode ?? DEFAULT_THINKING_CONFIG.mode,
    override: config.thinking?.override,
    tier_defaults: {
      opus: config.thinking?.tier_defaults?.opus ?? DEFAULT_THINKING_CONFIG.tier_defaults.opus,
      sonnet:
        config.thinking?.tier_defaults?.sonnet ?? DEFAULT_THINKING_CONFIG.tier_defaults.sonnet,
      haiku: config.thinking?.tier_defaults?.haiku ?? DEFAULT_THINKING_CONFIG.tier_defaults.haiku,
    },
    provider_overrides: config.thinking?.provider_overrides,
    show_warnings: config.thinking?.show_warnings ?? DEFAULT_THINKING_CONFIG.show_warnings,
  };
}

/**
 * Get Official Channels configuration.
 * Returns defaults if not configured.
 */
export function getOfficialChannelsConfig(): OfficialChannelsConfig {
  const config = loadOrCreateUnifiedConfig();

  return {
    selected:
      config.channels?.selected && config.channels.selected.length > 0
        ? normalizeOfficialChannelIds(config.channels.selected)
        : DEFAULT_OFFICIAL_CHANNELS_CONFIG.selected,
    unattended: config.channels?.unattended ?? DEFAULT_OFFICIAL_CHANNELS_CONFIG.unattended,
  };
}

/**
 * Get dashboard_auth configuration with ENV var override.
 * Priority: ENV vars > config.yaml > defaults
 */
export function isDashboardAuthEnabled(): boolean {
  const envEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED;

  if (envEnabled !== undefined) {
    return envEnabled === 'true' || envEnabled === '1';
  }

  const config = loadOrCreateUnifiedConfig();
  return config.dashboard_auth?.enabled ?? false;
}

/**
 * Get dashboard_auth configuration with ENV var override.
 * Priority: ENV vars > config.yaml > defaults
 */
export function getDashboardAuthConfig(): DashboardAuthConfig {
  const config = loadOrCreateUnifiedConfig();

  // ENV vars take precedence
  const envEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED;
  const envUsername = process.env.CCS_DASHBOARD_USERNAME;
  const envPasswordHash = process.env.CCS_DASHBOARD_PASSWORD_HASH;

  return {
    enabled:
      envEnabled !== undefined
        ? envEnabled === 'true' || envEnabled === '1'
        : (config.dashboard_auth?.enabled ?? false),
    username: envUsername ?? config.dashboard_auth?.username ?? '',
    password_hash: envPasswordHash ?? config.dashboard_auth?.password_hash ?? '',
    session_timeout_hours: config.dashboard_auth?.session_timeout_hours ?? 24,
  };
}

/**
 * Get browser automation configuration.
 * Returns canonicalized defaults if not configured.
 */
export function getBrowserConfig(): BrowserConfig {
  const config = loadOrCreateUnifiedConfig();
  return canonicalizeBrowserConfig(config.browser);
}

/**
 * Get image_analysis configuration.
 * Returns defaults if not configured.
 */
export function getImageAnalysisConfig(): ImageAnalysisConfig {
  const config = loadOrCreateUnifiedConfig();

  return canonicalizeImageAnalysisConfig({
    enabled: config.image_analysis?.enabled ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.enabled,
    timeout: config.image_analysis?.timeout ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.timeout,
    provider_models:
      config.image_analysis?.provider_models ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.provider_models,
    fallback_backend:
      config.image_analysis?.fallback_backend ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.fallback_backend,
    profile_backends:
      config.image_analysis?.profile_backends ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.profile_backends,
  });
}

export function getLoggingConfig(): LoggingConfig {
  const config = loadOrCreateUnifiedConfig();

  return {
    enabled: config.logging?.enabled ?? DEFAULT_LOGGING_CONFIG.enabled,
    level: config.logging?.level ?? DEFAULT_LOGGING_CONFIG.level,
    rotate_mb: config.logging?.rotate_mb ?? DEFAULT_LOGGING_CONFIG.rotate_mb,
    retain_days: config.logging?.retain_days ?? DEFAULT_LOGGING_CONFIG.retain_days,
    redact: config.logging?.redact ?? DEFAULT_LOGGING_CONFIG.redact,
    live_buffer_size: config.logging?.live_buffer_size ?? DEFAULT_LOGGING_CONFIG.live_buffer_size,
  };
}

/**
 * Get cursor configuration.
 * Returns defaults if not configured.
 */
export function getCursorConfig(): CursorConfig {
  const config = loadOrCreateUnifiedConfig();
  return config.cursor ?? { ...DEFAULT_CURSOR_CONFIG };
}
