//
//  SettingsService.swift
//  Confab
//
//  Settings persistence: SQLite for preferences, Keychain for API keys
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://confab.no>.


import Foundation
import os
import Security

/// Kill switch for the entire Personal Data tools section (Calendar/Reminders/Contacts/
/// Location & Maps). Hides the UI and forces every `*Enabled` getter to return `false`
/// regardless of the persisted DB value — no code deleted, just inert.
enum PersonalDataTools {
    static let isHiddenPendingAppleFix = false
}

@Observable
class SettingsService {
    static let shared = SettingsService()

    // In-memory cache of DB settings for fast reads
    private var cache: [String: String] = [:]

    // Encrypted database keys (for API keys)
    private enum EncryptedKeys {
        static let openrouterAPIKey = "openrouterAPIKey"
        static let anthropicAPIKey = "anthropicAPIKey"
        static let openaiAPIKey = "openaiAPIKey"
        static let googleAPIKey = "googleAPIKey"
        static let googleSearchEngineID = "googleSearchEngineID"
        static let anytypeMcpAPIKey = "anytypeMcpAPIKey"
        static let paperlessAPIToken = "paperlessAPIToken"
        static let jarvisAPIKey = "jarvisAPIKey"
    }

    // Old keychain keys (for migration only)
    private enum KeychainKeys {
        static let openrouterAPIKey = "com.oai.apikey.openrouter"
        static let anthropicAPIKey = "com.oai.apikey.anthropic"
        static let openaiAPIKey = "com.oai.apikey.openai"
        static let googleAPIKey = "com.oai.apikey.google"
        static let googleSearchEngineID = "com.oai.google.searchEngineID"
    }

    private init() {
        // Load all settings from DB into cache
        cache = (try? DatabaseService.shared.loadAllSettings()) ?? [:]
        Log.settings.info("Settings initialized with \(self.cache.count) cached entries")

        // Migrate from UserDefaults on first launch
        migrateFromUserDefaultsIfNeeded()

        // Migrate API keys from Keychain to encrypted database
        migrateFromKeychainIfNeeded()
    }

    // MARK: - Provider Settings

    var defaultProvider: Settings.Provider {
        get {
            if let raw = cache["defaultProvider"],
               let provider = Settings.Provider(rawValue: raw) {
                return provider
            }
            return .openrouter
        }
        set {
            cache["defaultProvider"] = newValue.rawValue
            DatabaseService.shared.setSetting(key: "defaultProvider", value: newValue.rawValue)
        }
    }

    var defaultModel: String? {
        get { cache["defaultModel"] }
        set {
            if let value = newValue {
                cache["defaultModel"] = value
                DatabaseService.shared.setSetting(key: "defaultModel", value: value)
            } else {
                cache.removeValue(forKey: "defaultModel")
                DatabaseService.shared.deleteSetting(key: "defaultModel")
            }
        }
    }

    // MARK: - Model Settings

    var streamEnabled: Bool {
        get { cache["streamEnabled"].map { $0 == "true" } ?? true }
        set {
            cache["streamEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "streamEnabled", value: String(newValue))
        }
    }

    var maxTokens: Int {
        get { cache["maxTokens"].flatMap(Int.init) ?? 0 }
        set {
            cache["maxTokens"] = String(newValue)
            DatabaseService.shared.setSetting(key: "maxTokens", value: String(newValue))
        }
    }

    var temperature: Double {
        get { cache["temperature"].flatMap(Double.init) ?? 0.0 }
        set {
            cache["temperature"] = String(newValue)
            DatabaseService.shared.setSetting(key: "temperature", value: String(newValue))
        }
    }

    var systemPrompt: String? {
        get { cache["systemPrompt"] }
        set {
            if let value = newValue, !value.isEmpty {
                cache["systemPrompt"] = value
                DatabaseService.shared.setSetting(key: "systemPrompt", value: value)
            } else {
                cache.removeValue(forKey: "systemPrompt")
                DatabaseService.shared.deleteSetting(key: "systemPrompt")
            }
        }
    }

    var customPromptMode: Settings.CustomPromptMode {
        get {
            if let rawValue = cache["customPromptMode"],
               let mode = Settings.CustomPromptMode(rawValue: rawValue) {
                return mode
            }
            return .append  // Default to append mode
        }
        set {
            cache["customPromptMode"] = newValue.rawValue
            DatabaseService.shared.setSetting(key: "customPromptMode", value: newValue.rawValue)
        }
    }

    // MARK: - Feature Settings

    var onlineMode: Bool {
        get { cache["onlineMode"] == "true" }
        set {
            cache["onlineMode"] = String(newValue)
            DatabaseService.shared.setSetting(key: "onlineMode", value: String(newValue))
        }
    }

    var memoryEnabled: Bool {
        get { cache["memoryEnabled"].map { $0 == "true" } ?? true }
        set {
            cache["memoryEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "memoryEnabled", value: String(newValue))
        }
    }

    var contextSelectionEnabled: Bool {
        get { cache["contextSelectionEnabled"] == "true" }
        set {
            cache["contextSelectionEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "contextSelectionEnabled", value: String(newValue))
        }
    }

    var contextMaxTokens: Int {
        get { cache["contextMaxTokens"].flatMap(Int.init) ?? 100_000 }
        set {
            cache["contextMaxTokens"] = String(newValue)
            DatabaseService.shared.setSetting(key: "contextMaxTokens", value: String(newValue))
        }
    }

    var embeddingsEnabled: Bool {
        get { cache["embeddingsEnabled"] == "true" }
        set {
            cache["embeddingsEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "embeddingsEnabled", value: String(newValue))
        }
    }

    var embeddingProvider: String {
        get { cache["embeddingProvider"] ?? "openai-small" }
        set {
            cache["embeddingProvider"] = newValue
            DatabaseService.shared.setSetting(key: "embeddingProvider", value: newValue)
        }
    }

    var progressiveSummarizationEnabled: Bool {
        get { cache["progressiveSummarizationEnabled"] == "true" }
        set {
            cache["progressiveSummarizationEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "progressiveSummarizationEnabled", value: String(newValue))
        }
    }

    var summarizationThreshold: Int {
        get { cache["summarizationThreshold"].flatMap(Int.init) ?? 50 }
        set {
            cache["summarizationThreshold"] = String(newValue)
            DatabaseService.shared.setSetting(key: "summarizationThreshold", value: String(newValue))
        }
    }

    var mcpEnabled: Bool {
        get { cache["mcpEnabled"] == "true" }
        set {
            cache["mcpEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "mcpEnabled", value: String(newValue))
        }
    }

    // MARK: - Reasoning Settings

    var reasoningEnabled: Bool {
        get { cache["reasoningEnabled"] == "true" }
        set {
            cache["reasoningEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "reasoningEnabled", value: String(newValue))
        }
    }

    /// "high", "medium", "low", "minimal" — default "medium"
    var reasoningEffort: String {
        get { cache["reasoningEffort"] ?? "medium" }
        set {
            cache["reasoningEffort"] = newValue
            DatabaseService.shared.setSetting(key: "reasoningEffort", value: newValue)
        }
    }

    /// When true, model reasons internally but thinking content is excluded from response
    var reasoningExclude: Bool {
        get { cache["reasoningExclude"] == "true" }
        set {
            cache["reasoningExclude"] = String(newValue)
            DatabaseService.shared.setSetting(key: "reasoningExclude", value: String(newValue))
        }
    }

    // MARK: - Text Size Settings

    /// GUI text size (headers, labels, buttons) — default 13
    var guiTextSize: Double {
        get { cache["guiTextSize"].flatMap(Double.init) ?? 13.0 }
        set {
            cache["guiTextSize"] = String(newValue)
            DatabaseService.shared.setSetting(key: "guiTextSize", value: String(newValue))
        }
    }

    /// Dialog/chat message text size — default 14
    var dialogTextSize: Double {
        get { cache["dialogTextSize"].flatMap(Double.init) ?? 14.0 }
        set {
            cache["dialogTextSize"] = String(newValue)
            DatabaseService.shared.setSetting(key: "dialogTextSize", value: String(newValue))
        }
    }

    /// Input box text size — default 14
    var inputTextSize: Double {
        get { cache["inputTextSize"].flatMap(Double.init) ?? 14.0 }
        set {
            cache["inputTextSize"] = String(newValue)
            DatabaseService.shared.setSetting(key: "inputTextSize", value: String(newValue))
        }
    }

    // MARK: - Toolbar Settings

    /// Toolbar icon size — default 16 (minimum)
    var toolbarIconSize: Double {
        get { cache["toolbarIconSize"].flatMap(Double.init) ?? 16.0 }
        set {
            cache["toolbarIconSize"] = String(newValue)
            DatabaseService.shared.setSetting(key: "toolbarIconSize", value: String(newValue))
        }
    }

    /// Show labels on toolbar icons — default false
    var showToolbarLabels: Bool {
        get { cache["showToolbarLabels"] == "true" }
        set {
            cache["showToolbarLabels"] = String(newValue)
            DatabaseService.shared.setSetting(key: "showToolbarLabels", value: String(newValue))
        }
    }

    /// Input bar height in points — default 80
    var inputBarHeight: Double {
        get { cache["inputBarHeight"].flatMap(Double.init) ?? 80.0 }
        set {
            cache["inputBarHeight"] = String(newValue)
            DatabaseService.shared.setSetting(key: "inputBarHeight", value: String(newValue))
        }
    }

    /// Whether the sidebar is visible — default true
    var sidebarVisible: Bool {
        get { cache["sidebarVisible"] != "false" }
        set {
            cache["sidebarVisible"] = String(newValue)
            DatabaseService.shared.setSetting(key: "sidebarVisible", value: String(newValue))
        }
    }

    // MARK: - MCP Permissions

    var mcpCanWriteFiles: Bool {
        get { cache["mcpCanWriteFiles"] == "true" }
        set {
            cache["mcpCanWriteFiles"] = String(newValue)
            DatabaseService.shared.setSetting(key: "mcpCanWriteFiles", value: String(newValue))
        }
    }

    var mcpCanDeleteFiles: Bool {
        get { cache["mcpCanDeleteFiles"] == "true" }
        set {
            cache["mcpCanDeleteFiles"] = String(newValue)
            DatabaseService.shared.setSetting(key: "mcpCanDeleteFiles", value: String(newValue))
        }
    }

    var mcpCanCreateDirectories: Bool {
        get { cache["mcpCanCreateDirectories"] == "true" }
        set {
            cache["mcpCanCreateDirectories"] = String(newValue)
            DatabaseService.shared.setSetting(key: "mcpCanCreateDirectories", value: String(newValue))
        }
    }

    var mcpCanMoveFiles: Bool {
        get { cache["mcpCanMoveFiles"] == "true" }
        set {
            cache["mcpCanMoveFiles"] = String(newValue)
            DatabaseService.shared.setSetting(key: "mcpCanMoveFiles", value: String(newValue))
        }
    }

    var mcpRespectGitignore: Bool {
        get { cache["mcpRespectGitignore"].map { $0 == "true" } ?? true }
        set {
            cache["mcpRespectGitignore"] = String(newValue)
            DatabaseService.shared.setSetting(key: "mcpRespectGitignore", value: String(newValue))
        }
    }

    // MARK: - MCP Allowed Folders

    var mcpAllowedFolders: [String] {
        get {
            guard let json = cache["mcpAllowedFolders"],
                  let data = json.data(using: .utf8),
                  let folders = try? JSONDecoder().decode([String].self, from: data) else {
                return []
            }
            return folders
        }
        set {
            if let data = try? JSONEncoder().encode(newValue),
               let json = String(data: data, encoding: .utf8) {
                cache["mcpAllowedFolders"] = json
                DatabaseService.shared.setSetting(key: "mcpAllowedFolders", value: json)
            }
        }
    }

    // MARK: - User Shortcuts (prompt template macros)

    var userShortcuts: [Shortcut] {
        get {
            guard let json = cache["userShortcuts"],
                  let data = json.data(using: .utf8) else { return [] }
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .iso8601
            return (try? decoder.decode([Shortcut].self, from: data)) ?? []
        }
        set {
            let encoder = JSONEncoder()
            encoder.dateEncodingStrategy = .iso8601
            if let data = try? encoder.encode(newValue),
               let json = String(data: data, encoding: .utf8) {
                cache["userShortcuts"] = json
                DatabaseService.shared.setSetting(key: "userShortcuts", value: json)
            }
        }
    }

    func addShortcut(_ shortcut: Shortcut) { userShortcuts = userShortcuts + [shortcut] }

    func updateShortcut(_ shortcut: Shortcut) {
        userShortcuts = userShortcuts.map { $0.id == shortcut.id ? shortcut : $0 }
    }

    func deleteShortcut(id: UUID) { userShortcuts = userShortcuts.filter { $0.id != id } }

    // MARK: - Agent Skills (SKILL.md-style behavioral instructions)

    var agentSkills: [AgentSkill] {
        get {
            guard let json = cache["agentSkills"],
                  let data = json.data(using: .utf8) else { return [] }
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .iso8601
            return (try? decoder.decode([AgentSkill].self, from: data)) ?? []
        }
        set {
            let encoder = JSONEncoder()
            encoder.dateEncodingStrategy = .iso8601
            if let data = try? encoder.encode(newValue),
               let json = String(data: data, encoding: .utf8) {
                cache["agentSkills"] = json
                DatabaseService.shared.setSetting(key: "agentSkills", value: json)
            }
        }
    }

    func addAgentSkill(_ skill: AgentSkill) { agentSkills = agentSkills + [skill] }

    func updateAgentSkill(_ skill: AgentSkill) {
        agentSkills = agentSkills.map { $0.id == skill.id ? skill : $0 }
    }

    func deleteAgentSkill(id: UUID) {
        agentSkills = agentSkills.filter { $0.id != id }
        AgentSkillFilesService.shared.deleteAll(for: id)
    }

    func toggleAgentSkill(id: UUID) {
        agentSkills = agentSkills.map { s in
            s.id == id ? AgentSkill(id: s.id, name: s.name, skillDescription: s.skillDescription,
                                    content: s.content, isActive: !s.isActive,
                                    createdAt: s.createdAt, updatedAt: Date()) : s
        }
    }

    // MARK: - External MCP Servers

    var externalMCPServers: [ExternalMCPServer] {
        get {
            guard let json = cache["externalMCPServers"],
                  let data = json.data(using: .utf8) else { return [] }
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .iso8601
            return (try? decoder.decode([ExternalMCPServer].self, from: data)) ?? []
        }
        set {
            let encoder = JSONEncoder()
            encoder.dateEncodingStrategy = .iso8601
            if let data = try? encoder.encode(newValue),
               let json = String(data: data, encoding: .utf8) {
                cache["externalMCPServers"] = json
                DatabaseService.shared.setSetting(key: "externalMCPServers", value: json)
            }
            Task { @MainActor in ExternalMCPManager.shared.reconfigure(servers: newValue) }
        }
    }

    func addExternalMCPServer(_ server: ExternalMCPServer) {
        externalMCPServers = externalMCPServers + [server]
    }

    func updateExternalMCPServer(_ server: ExternalMCPServer) {
        externalMCPServers = externalMCPServers.map { $0.id == server.id ? server : $0 }
    }

    func deleteExternalMCPServer(id: UUID) {
        externalMCPServers = externalMCPServers.filter { $0.id != id }
    }

    func toggleExternalMCPServer(id: UUID) {
        externalMCPServers = externalMCPServers.map { s in
            s.id == id ? ExternalMCPServer(id: s.id, name: s.name, command: s.command,
                                           args: s.args, isEnabled: !s.isEnabled,
                                           timeout: s.timeout, createdAt: s.createdAt) : s
        }
    }

    // MARK: - Favorite Models

    var favoriteModelIds: Set<String> {
        get {
            guard let json = cache["favoriteModelIds"],
                  let data = json.data(using: .utf8),
                  let ids = try? JSONDecoder().decode([String].self, from: data) else { return [] }
            return Set(ids)
        }
        set {
            let sorted = newValue.sorted()
            if let data = try? JSONEncoder().encode(sorted),
               let json = String(data: data, encoding: .utf8) {
                cache["favoriteModelIds"] = json
                DatabaseService.shared.setSetting(key: "favoriteModelIds", value: json)
            }
        }
    }

    // MARK: - Folder Collapse State

    /// IDs of folders currently collapsed in the sidebar/conversation list — persisted so the
    /// app reopens with folders in the same expanded/collapsed state the user left them in.
    var collapsedFolderIds: Set<UUID> {
        get {
            guard let json = cache["collapsedFolderIds"],
                  let data = json.data(using: .utf8),
                  let ids = try? JSONDecoder().decode([String].self, from: data) else { return [] }
            return Set(ids.compactMap { UUID(uuidString: $0) })
        }
        set {
            let sorted = newValue.map { $0.uuidString }.sorted()
            if let data = try? JSONEncoder().encode(sorted),
               let json = String(data: data, encoding: .utf8) {
                cache["collapsedFolderIds"] = json
                DatabaseService.shared.setSetting(key: "collapsedFolderIds", value: json)
            }
        }
    }

    /// ISO8601 timestamp of the last local change to favoriteModelIds — used to
    /// resolve last-write-wins conflicts when syncing favorites across machines.
    var favoriteModelsUpdatedAt: String {
        get { cache["favoriteModelsUpdatedAt"] ?? "" }
        set {
            cache["favoriteModelsUpdatedAt"] = newValue
            DatabaseService.shared.setSetting(key: "favoriteModelsUpdatedAt", value: newValue)
        }
    }

    func toggleFavoriteModel(_ id: String) {
        var favs = favoriteModelIds
        if favs.contains(id) { favs.remove(id) } else { favs.insert(id) }
        favoriteModelIds = favs
        favoriteModelsUpdatedAt = ISO8601DateFormatter().string(from: Date())
        Task { await BackupService.shared.pushFavorites() }
    }

    // MARK: - Automatic Backup

    /// "manual" (default), "daily", or "weekly"
    var autoBackupFrequency: String {
        get { cache["autoBackupFrequency"] ?? "manual" }
        set {
            cache["autoBackupFrequency"] = newValue
            DatabaseService.shared.setSetting(key: "autoBackupFrequency", value: newValue)
        }
    }

    // MARK: - Anytype MCP Settings

    var anytypeMcpEnabled: Bool {
        get { cache["anytypeMcpEnabled"] == "true" }
        set {
            cache["anytypeMcpEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "anytypeMcpEnabled", value: String(newValue))
        }
    }

    var anytypeMcpURL: String {
        get { cache["anytypeMcpURL"] ?? "" }
        set {
            let trimmed = newValue.trimmingCharacters(in: .whitespaces)
            if trimmed.isEmpty {
                cache.removeValue(forKey: "anytypeMcpURL")
                DatabaseService.shared.deleteSetting(key: "anytypeMcpURL")
            } else {
                cache["anytypeMcpURL"] = trimmed
                DatabaseService.shared.setSetting(key: "anytypeMcpURL", value: trimmed)
            }
        }
    }

    var anytypeMcpEffectiveURL: String {
        let url = anytypeMcpURL
        return url.isEmpty ? "http://127.0.0.1:31009" : url
    }

    var anytypeMcpAPIKey: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.anytypeMcpAPIKey) }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.anytypeMcpAPIKey, value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.anytypeMcpAPIKey)
            }
        }
    }

    var anytypeMcpConfigured: Bool {
        guard let key = anytypeMcpAPIKey else { return false }
        return !key.isEmpty
    }

    // MARK: - Jarvis Settings

    var jarvisEnabled: Bool {
        get { cache["jarvisEnabled"] == "true" }
        set {
            cache["jarvisEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "jarvisEnabled", value: String(newValue))
        }
    }

    var jarvisURL: String {
        get { cache["jarvisURL"] ?? "" }
        set {
            let trimmed = newValue.trimmingCharacters(in: .whitespaces)
            if trimmed.isEmpty {
                cache.removeValue(forKey: "jarvisURL")
                DatabaseService.shared.deleteSetting(key: "jarvisURL")
            } else {
                cache["jarvisURL"] = trimmed
                DatabaseService.shared.setSetting(key: "jarvisURL", value: trimmed)
            }
        }
    }

    var jarvisAPIKey: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.jarvisAPIKey) }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.jarvisAPIKey, value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.jarvisAPIKey)
            }
        }
    }

    var jarvisConfigured: Bool {
        guard let key = jarvisAPIKey else { return false }
        return !jarvisURL.isEmpty && !key.isEmpty
    }

    // MARK: - Bash Execution Settings

    var bashEnabled: Bool {
        get { cache["bashEnabled"] == "true" }
        set {
            cache["bashEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "bashEnabled", value: String(newValue))
        }
    }

    var bashRequireApproval: Bool {
        get { cache["bashRequireApproval"].map { $0 == "true" } ?? true }
        set {
            cache["bashRequireApproval"] = String(newValue)
            DatabaseService.shared.setSetting(key: "bashRequireApproval", value: String(newValue))
        }
    }

    // MARK: - Research Agents Settings

    /// When true, the AI can call `spawn_research_agents` to run multiple read-only
    /// sub-agents in parallel. Each sub-agent is its own full chain of model calls, so
    /// this can noticeably increase cost — opt-in, off by default.
    var agentsEnabled: Bool {
        get { cache["agentsEnabled"] == "true" }
        set {
            cache["agentsEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "agentsEnabled", value: String(newValue))
        }
    }

    var maxConcurrentAgents: Int {
        get { cache["maxConcurrentAgents"].flatMap(Int.init) ?? 3 }
        set {
            cache["maxConcurrentAgents"] = String(newValue)
            DatabaseService.shared.setSetting(key: "maxConcurrentAgents", value: String(newValue))
        }
    }

    /// When true (and an active "2nd Brain" Agent Skill is installed), bash commands that
    /// invoke the 2nd Brain helper script skip the approval dialog entirely.
    var trustSecondBrainSkill: Bool {
        get { cache["trustSecondBrainSkill"] == "true" }
        set {
            cache["trustSecondBrainSkill"] = String(newValue)
            DatabaseService.shared.setSetting(key: "trustSecondBrainSkill", value: String(newValue))
        }
    }

    var bashWorkingDirectory: String {
        get { cache["bashWorkingDirectory"] ?? "~" }
        set {
            cache["bashWorkingDirectory"] = newValue
            DatabaseService.shared.setSetting(key: "bashWorkingDirectory", value: newValue)
        }
    }

    var bashTimeout: Int {
        get { cache["bashTimeout"].flatMap(Int.init) ?? 30 }
        set {
            cache["bashTimeout"] = String(newValue)
            DatabaseService.shared.setSetting(key: "bashTimeout", value: String(newValue))
        }
    }

    // MARK: - Personal Data Settings (Calendar/Reminders/Contacts/Location/Maps)

    var calendarEnabled: Bool {
        get { !PersonalDataTools.isHiddenPendingAppleFix && cache["calendarEnabled"] == "true" }
        set {
            cache["calendarEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "calendarEnabled", value: String(newValue))
        }
    }

    var remindersEnabled: Bool {
        get { !PersonalDataTools.isHiddenPendingAppleFix && cache["remindersEnabled"] == "true" }
        set {
            cache["remindersEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "remindersEnabled", value: String(newValue))
        }
    }

    var contactsEnabled: Bool {
        get { !PersonalDataTools.isHiddenPendingAppleFix && cache["contactsEnabled"] == "true" }
        set {
            cache["contactsEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "contactsEnabled", value: String(newValue))
        }
    }

    var locationMapsEnabled: Bool {
        get { !PersonalDataTools.isHiddenPendingAppleFix && cache["locationMapsEnabled"] == "true" }
        set {
            cache["locationMapsEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "locationMapsEnabled", value: String(newValue))
        }
    }

    var personalDataRequireApproval: Bool {
        get { cache["personalDataRequireApproval"].map { $0 == "true" } ?? true }
        set {
            cache["personalDataRequireApproval"] = String(newValue)
            DatabaseService.shared.setSetting(key: "personalDataRequireApproval", value: String(newValue))
        }
    }

    // MARK: - Paperless-NGX Settings

    var paperlessEnabled: Bool {
        get { cache["paperlessEnabled"] == "true" }
        set {
            cache["paperlessEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "paperlessEnabled", value: String(newValue))
        }
    }

    var paperlessURL: String {
        get { cache["paperlessURL"] ?? "" }
        set {
            var trimmed = newValue.trimmingCharacters(in: .whitespaces)
            // Remove trailing slash for consistency
            while trimmed.hasSuffix("/") { trimmed = String(trimmed.dropLast()) }
            if trimmed.isEmpty {
                cache.removeValue(forKey: "paperlessURL")
                DatabaseService.shared.deleteSetting(key: "paperlessURL")
            } else {
                cache["paperlessURL"] = trimmed
                DatabaseService.shared.setSetting(key: "paperlessURL", value: trimmed)
            }
        }
    }

    var paperlessAPIToken: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.paperlessAPIToken) }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.paperlessAPIToken, value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.paperlessAPIToken)
            }
        }
    }

    var paperlessConfigured: Bool {
        guard !paperlessURL.isEmpty else { return false }
        guard let token = paperlessAPIToken else { return false }
        return !token.isEmpty
    }

    // MARK: - Search Settings

    var searchProvider: Settings.SearchProvider {
        get {
            if let raw = cache["searchProvider"],
               let provider = Settings.SearchProvider(rawValue: raw) {
                return provider
            }
            return .duckduckgo
        }
        set {
            cache["searchProvider"] = newValue.rawValue
            DatabaseService.shared.setSetting(key: "searchProvider", value: newValue.rawValue)
        }
    }

    // MARK: - Ollama Settings

    var ollamaBaseURL: String {
        get { cache["ollamaBaseURL"] ?? "" }
        set {
            let trimmed = newValue.trimmingCharacters(in: .whitespaces)
            if trimmed.isEmpty {
                cache.removeValue(forKey: "ollamaBaseURL")
                DatabaseService.shared.deleteSetting(key: "ollamaBaseURL")
            } else {
                cache["ollamaBaseURL"] = trimmed
                DatabaseService.shared.setSetting(key: "ollamaBaseURL", value: trimmed)
            }
        }
    }

    /// Resolved Ollama URL — returns the user value or the default
    var ollamaEffectiveURL: String {
        let url = ollamaBaseURL
        return url.isEmpty ? "http://localhost:11434" : url
    }

    /// Whether the user has explicitly configured an Ollama URL
    var ollamaConfigured: Bool {
        cache["ollamaBaseURL"] != nil && !(cache["ollamaBaseURL"]!.isEmpty)
    }

    // MARK: - API Keys (Encrypted Database)

    var openrouterAPIKey: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.openrouterAPIKey) }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.openrouterAPIKey, value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.openrouterAPIKey)
            }
        }
    }

    var anthropicAPIKey: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.anthropicAPIKey) }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.anthropicAPIKey, value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.anthropicAPIKey)
            }
        }
    }

    var openaiAPIKey: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.openaiAPIKey) }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.openaiAPIKey, value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.openaiAPIKey)
            }
        }
    }

    var googleAPIKey: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.googleAPIKey) }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.googleAPIKey, value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.googleAPIKey)
            }
        }
    }

    var googleSearchEngineID: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.googleSearchEngineID) }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.googleSearchEngineID, value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.googleSearchEngineID)
            }
        }
    }

    // MARK: - Git Sync Settings

    var syncEnabled: Bool {
        get { cache["syncEnabled"] == "true" }
        set {
            cache["syncEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "syncEnabled", value: String(newValue))
        }
    }

    var syncRepoURL: String {
        get { cache["syncRepoURL"] ?? "" }
        set {
            let trimmed = newValue.trimmingCharacters(in: .whitespaces)
            if trimmed.isEmpty {
                cache.removeValue(forKey: "syncRepoURL")
                DatabaseService.shared.deleteSetting(key: "syncRepoURL")
            } else {
                cache["syncRepoURL"] = trimmed
                DatabaseService.shared.setSetting(key: "syncRepoURL", value: trimmed)
            }
        }
    }

    var syncLocalPath: String {
        get { cache["syncLocalPath"] ?? "~/Library/Application Support/oAI/sync" }
        set {
            cache["syncLocalPath"] = newValue
            DatabaseService.shared.setSetting(key: "syncLocalPath", value: newValue)
        }
    }

    var syncAuthMethod: String {
        get { cache["syncAuthMethod"] ?? "token" } // Default to access token
        set {
            cache["syncAuthMethod"] = newValue
            DatabaseService.shared.setSetting(key: "syncAuthMethod", value: newValue)
        }
    }

    // Encrypted sync credentials
    var syncUsername: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: "syncUsername") }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: "syncUsername", value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: "syncUsername")
            }
        }
    }

    var syncPassword: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: "syncPassword") }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: "syncPassword", value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: "syncPassword")
            }
        }
    }

    var syncAccessToken: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: "syncAccessToken") }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: "syncAccessToken", value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: "syncAccessToken")
            }
        }
    }

    var syncAutoExport: Bool {
        get { cache["syncAutoExport"] == "true" }
        set {
            cache["syncAutoExport"] = String(newValue)
            DatabaseService.shared.setSetting(key: "syncAutoExport", value: String(newValue))
        }
    }

    var syncAutoPull: Bool {
        get { cache["syncAutoPull"] == "true" }
        set {
            cache["syncAutoPull"] = String(newValue)
            DatabaseService.shared.setSetting(key: "syncAutoPull", value: String(newValue))
        }
    }

    var syncConfigured: Bool {
        guard !syncRepoURL.isEmpty else { return false }

        switch syncAuthMethod {
        case "ssh":
            return true // SSH uses system keys
        case "password":
            return syncUsername != nil && syncPassword != nil
        case "token":
            return syncAccessToken != nil
        default:
            return false
        }
    }

    // MARK: - Crash-Recovery Draft Settings

    /// How often (in seconds) the in-progress conversation is mirrored to a local
    /// crash-recovery draft, so a force-quit/crash doesn't lose it. 0 = off.
    var draftRecoveryIntervalSeconds: Int {
        get { cache["draftRecoveryIntervalSeconds"].flatMap(Int.init) ?? 10 }
        set {
            cache["draftRecoveryIntervalSeconds"] = String(newValue)
            DatabaseService.shared.setSetting(key: "draftRecoveryIntervalSeconds", value: String(newValue))
        }
    }

    // MARK: - Email Handler Settings

    var emailHandlerEnabled: Bool {
        get { cache["emailHandlerEnabled"] == "true" }
        set {
            cache["emailHandlerEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "emailHandlerEnabled", value: String(newValue))
        }
    }

    var emailHandlerProvider: String {
        get { cache["emailHandlerProvider"] ?? "openrouter" }
        set {
            cache["emailHandlerProvider"] = newValue
            DatabaseService.shared.setSetting(key: "emailHandlerProvider", value: newValue)
        }
    }

    var emailHandlerModel: String {
        get { cache["emailHandlerModel"] ?? "" }
        set {
            cache["emailHandlerModel"] = newValue
            DatabaseService.shared.setSetting(key: "emailHandlerModel", value: newValue)
        }
    }

    // MARK: - CLI Server

    /// Local Unix-socket server (CLIServerService) for one-shot, non-streaming shell access to a
    /// single fixed model — deliberately separate from the chat UI's defaultProvider/defaultModel
    /// so switching models in the GUI never changes shell-tool behavior, same reasoning as the
    /// email handler's own dedicated provider/model pair above.
    var cliServerEnabled: Bool {
        get { cache["cliServerEnabled"] == "true" }
        set {
            cache["cliServerEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "cliServerEnabled", value: String(newValue))
        }
    }

    var cliServerProvider: String {
        get { cache["cliServerProvider"] ?? "openrouter" }
        set {
            cache["cliServerProvider"] = newValue
            DatabaseService.shared.setSetting(key: "cliServerProvider", value: newValue)
        }
    }

    var cliServerModel: String {
        get { cache["cliServerModel"] ?? "" }
        set {
            cache["cliServerModel"] = newValue
            DatabaseService.shared.setSetting(key: "cliServerModel", value: newValue)
        }
    }

    var emailSubjectIdentifier: String {
        get { cache["emailSubjectIdentifier"] ?? "[OAIBOT]" }
        set {
            cache["emailSubjectIdentifier"] = newValue
            DatabaseService.shared.setSetting(key: "emailSubjectIdentifier", value: newValue)
        }
    }

    var emailRateLimitEnabled: Bool {
        get { cache["emailRateLimitEnabled"] == "true" }
        set {
            cache["emailRateLimitEnabled"] = String(newValue)
            DatabaseService.shared.setSetting(key: "emailRateLimitEnabled", value: String(newValue))
        }
    }

    var emailRateLimitPerHour: Int {
        get { cache["emailRateLimitPerHour"].flatMap(Int.init) ?? 10 }
        set {
            cache["emailRateLimitPerHour"] = String(newValue)
            DatabaseService.shared.setSetting(key: "emailRateLimitPerHour", value: String(newValue))
        }
    }

    var emailMaxTokens: Int {
        get { cache["emailMaxTokens"].flatMap(Int.init) ?? 2000 }
        set {
            cache["emailMaxTokens"] = String(newValue)
            DatabaseService.shared.setSetting(key: "emailMaxTokens", value: String(newValue))
        }
    }

    var emailHandlerSystemPrompt: String? {
        get { cache["emailHandlerSystemPrompt"] }
        set {
            if let value = newValue, !value.isEmpty {
                cache["emailHandlerSystemPrompt"] = value
                DatabaseService.shared.setSetting(key: "emailHandlerSystemPrompt", value: value)
            } else {
                cache.removeValue(forKey: "emailHandlerSystemPrompt")
                DatabaseService.shared.deleteSetting(key: "emailHandlerSystemPrompt")
            }
        }
    }

    var emailOnlineMode: Bool {
        get { cache["emailOnlineMode"] == "true" }
        set {
            cache["emailOnlineMode"] = String(newValue)
            DatabaseService.shared.setSetting(key: "emailOnlineMode", value: String(newValue))
        }
    }

    var emailHandlerConfigured: Bool {
        guard emailHandlerEnabled else { return false }
        guard !emailHandlerModel.isEmpty else { return false }
        // Check if email server is configured
        guard emailServerConfigured else { return false }
        return true
    }

    // MARK: - Email Server Settings

    var emailImapHost: String? {
        get { cache["emailImapHost"] }
        set {
            if let value = newValue, !value.isEmpty {
                cache["emailImapHost"] = value
                DatabaseService.shared.setSetting(key: "emailImapHost", value: value)
            } else {
                cache.removeValue(forKey: "emailImapHost")
                DatabaseService.shared.deleteSetting(key: "emailImapHost")
            }
        }
    }

    var emailSmtpHost: String? {
        get { cache["emailSmtpHost"] }
        set {
            if let value = newValue, !value.isEmpty {
                cache["emailSmtpHost"] = value
                DatabaseService.shared.setSetting(key: "emailSmtpHost", value: value)
            } else {
                cache.removeValue(forKey: "emailSmtpHost")
                DatabaseService.shared.deleteSetting(key: "emailSmtpHost")
            }
        }
    }

    var emailUsername: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: "emailUsername") }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: "emailUsername", value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: "emailUsername")
            }
        }
    }

    var emailPassword: String? {
        get { try? DatabaseService.shared.getEncryptedSetting(key: "emailPassword") }
        set {
            if let value = newValue, !value.isEmpty {
                try? DatabaseService.shared.setEncryptedSetting(key: "emailPassword", value: value)
            } else {
                DatabaseService.shared.deleteEncryptedSetting(key: "emailPassword")
            }
        }
    }

    var emailImapPort: Int {
        get { cache["emailImapPort"].flatMap(Int.init) ?? 993 }
        set {
            cache["emailImapPort"] = String(newValue)
            DatabaseService.shared.setSetting(key: "emailImapPort", value: String(newValue))
        }
    }

    var emailSmtpPort: Int {
        get { cache["emailSmtpPort"].flatMap(Int.init) ?? 587 }
        set {
            cache["emailSmtpPort"] = String(newValue)
            DatabaseService.shared.setSetting(key: "emailSmtpPort", value: String(newValue))
        }
    }

    var emailServerConfigured: Bool {
        guard let imapHost = emailImapHost, !imapHost.isEmpty else { return false }
        guard let smtpHost = emailSmtpHost, !smtpHost.isEmpty else { return false }
        guard let username = emailUsername, !username.isEmpty else { return false }
        guard let password = emailPassword, !password.isEmpty else { return false }
        return true
    }

    // MARK: - Cache Reload

    /// Replace the in-memory cache with a fresh read from the database.
    /// Called by BackupService after restoring a backup.
    func reloadFromDatabase() {
        cache = (try? DatabaseService.shared.loadAllSettings()) ?? [:]
        Log.settings.info("Settings cache reloaded (\(self.cache.count) entries)")
    }

    // MARK: - UserDefaults Migration

    private func migrateFromUserDefaultsIfNeeded() {
        // Skip if already migrated
        guard cache["_migrated"] == nil else { return }

        let defaults = UserDefaults.standard
        let migrations: [(udKey: String, dbKey: String)] = [
            ("defaultProvider", "defaultProvider"),
            ("defaultModel", "defaultModel"),
            ("streamEnabled", "streamEnabled"),
            ("maxTokens", "maxTokens"),
            ("temperature", "temperature"),
            ("onlineMode", "onlineMode"),
            ("memoryEnabled", "memoryEnabled"),
            ("mcpEnabled", "mcpEnabled"),
            ("searchProvider", "searchProvider"),
            ("ollamaBaseURL", "ollamaBaseURL"),
        ]

        for (udKey, dbKey) in migrations {
            guard cache[dbKey] == nil else { continue }

            if let stringVal = defaults.string(forKey: udKey) {
                cache[dbKey] = stringVal
                DatabaseService.shared.setSetting(key: dbKey, value: stringVal)
            } else if defaults.object(forKey: udKey) != nil {
                // Handle bool/int/double stored as non-string
                let value: String
                if let boolVal = defaults.object(forKey: udKey) as? Bool {
                    value = String(boolVal)
                } else if defaults.integer(forKey: udKey) != 0 {
                    value = String(defaults.integer(forKey: udKey))
                } else if defaults.double(forKey: udKey) != 0.0 {
                    value = String(defaults.double(forKey: udKey))
                } else {
                    continue
                }
                cache[dbKey] = value
                DatabaseService.shared.setSetting(key: dbKey, value: value)
            }
        }

        // Mark migration complete
        cache["_migrated"] = "true"
        DatabaseService.shared.setSetting(key: "_migrated", value: "true")
    }

    // MARK: - Keychain Migration

    private func migrateFromKeychainIfNeeded() {
        // Skip if already migrated
        guard cache["_keychain_migrated"] == nil else { return }

        Log.settings.info("Migrating API keys from Keychain to encrypted database...")

        let keysToMigrate: [(keychainKey: String, encryptedKey: String)] = [
            (KeychainKeys.openrouterAPIKey, EncryptedKeys.openrouterAPIKey),
            (KeychainKeys.anthropicAPIKey, EncryptedKeys.anthropicAPIKey),
            (KeychainKeys.openaiAPIKey, EncryptedKeys.openaiAPIKey),
            (KeychainKeys.googleAPIKey, EncryptedKeys.googleAPIKey),
            (KeychainKeys.googleSearchEngineID, EncryptedKeys.googleSearchEngineID),
        ]

        var migratedCount = 0
        for (keychainKey, encryptedKey) in keysToMigrate {
            // Read from keychain
            if let value = getKeychainValue(for: keychainKey), !value.isEmpty {
                // Write to encrypted database
                do {
                    try DatabaseService.shared.setEncryptedSetting(key: encryptedKey, value: value)
                    // Delete from keychain after successful migration
                    deleteKeychainValue(for: keychainKey)
                    migratedCount += 1
                    Log.settings.info("Migrated \(encryptedKey) from Keychain to encrypted database")
                } catch {
                    Log.settings.error("Failed to migrate \(encryptedKey): \(error.localizedDescription)")
                }
            }
        }

        Log.settings.info("Keychain migration complete: \(migratedCount) keys migrated")

        // Mark migration complete
        cache["_keychain_migrated"] = "true"
        DatabaseService.shared.setSetting(key: "_keychain_migrated", value: "true")
    }

    // MARK: - Keychain Helpers (for migration only)

    private func getKeychainValue(for key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var dataTypeRef: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)

        guard status == errSecSuccess,
              let data = dataTypeRef as? Data,
              let value = String(data: data, encoding: .utf8) else {
            return nil
        }

        return value
    }

    private func setKeychainValue(_ value: String, for key: String) {
        guard let data = value.data(using: .utf8) else { return }

        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]

        let attributes: [String: Any] = [
            kSecValueData as String: data
        ]

        let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)

        if updateStatus == errSecItemNotFound {
            var newItem = query
            newItem[kSecValueData as String] = data
            SecItemAdd(newItem as CFDictionary, nil)
        }
    }

    private func deleteKeychainValue(for key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]

        SecItemDelete(query as CFDictionary)
    }
}
