import SwiftUI
import AppKit
import NucleicCore
import NucleicProtocol

/// App settings, organized into focused tabs so the panel stays legible as the
/// number of preferences grows: General (appearance included), Chats, Agents,
/// Sandbox, Control, Git, Intelligence, Remote.
struct SettingsView: View {
    @AppStorage(AppAppearance.storageKey) private var appearanceRaw = AppAppearance.dark.rawValue
    @AppStorage(AppTextSize.storageKey) private var textSizeRaw = AppTextSize.medium.rawValue

    /// Starts unset so the first `onAppear` assignment counts as a selection
    /// change — without it the initially-selected tab renders without its Liquid
    /// Glass highlight until the user switches tabs and back.
    @State private var selection: Int?

    private var appearance: AppAppearance { AppAppearance(rawValue: appearanceRaw) ?? .dark }
    private var textSize: AppTextSize { AppTextSize(rawValue: textSizeRaw) ?? .medium }

    var body: some View {
        TabView(selection: $selection) {
            GeneralSettingsTab()
                .tabItem { Label("General", systemImage: "gearshape") }
                .tag(0 as Int?)
            ChatSettingsTab()
                .tabItem { Label("Chats", systemImage: "bubble.left.and.bubble.right") }
                .tag(2 as Int?)
            AgentsSettingsTab()
                .tabItem { Label("Agents", systemImage: "cpu") }
                .tag(3 as Int?)
            SandboxSettingsTab()
                .tabItem { Label("Sandbox", systemImage: "shippingbox") }
                .tag(6 as Int?)
            ControlSettingsTab()
                .tabItem { Label("Control", systemImage: "lock.shield") }
                .tag(7 as Int?)
            GitSettingsTab()
                .tabItem { Label("Git", systemImage: "arrow.triangle.branch") }
                .tag(8 as Int?)
            IntelligenceSettingsTab()
                .tabItem { Label("Intelligence", systemImage: "sparkles") }
                .tag(4 as Int?)
            RemoteSettingsTab()
                .tabItem { Label("Remote", systemImage: "iphone") }
                .tag(5 as Int?)
        }
        .frame(minWidth: 520, maxWidth: 520, minHeight: 560, maxHeight: .infinity)
        .preferredColorScheme(appearance.colorScheme)
        .dynamicTypeSize(textSize.dynamicTypeSize)
        .background(SettingsWindowConfigurator(reapplyToken: selection))
        .onAppear { if selection == nil { selection = 0 } }
    }
}

/// Pins the Settings window's chrome that SwiftUI's `Settings` scene otherwise
/// manages on its own: a constant "Nucleic Settings" title (the default would
/// track the selected tab), float-above-everything level so the main window can't
/// cover it, and a vertically resizable frame.
private struct SettingsWindowConfigurator: NSViewRepresentable {
    /// Re-runs `updateNSView` whenever the selected tab changes so we can restore
    /// the title after the tab switch overwrites it.
    var reapplyToken: Int?

    func makeNSView(context: Context) -> NSView { NSView() }

    func updateNSView(_ nsView: NSView, context: Context) {
        DispatchQueue.main.async {
            guard let window = nsView.window else { return }
            window.title = "Nucleic Settings"
            window.level = .floating
            window.styleMask.insert(.resizable)
        }
    }
}

/// General interface preferences: appearance (theme, text size, color vision) plus home,
/// to-do, and diagnostics behavior. Appearance lives here rather than in its own tab to keep
/// the tab bar compact.
private struct GeneralSettingsTab: View {
    @AppStorage(AppAppearance.storageKey) private var appearanceRaw = AppAppearance.dark.rawValue
    @AppStorage(AppTextSize.storageKey) private var textSizeRaw = AppTextSize.medium.rawValue
    @AppStorage(ColorVisionMode.storageKey) private var colorVisionRaw = ColorVisionMode.standard.rawValue
    @AppStorage(StreakBadge.showKey) private var showStreak = true
    @AppStorage(ChatStatusSounds.playOnDoneKey) private var playDoneSound = true
    @AppStorage(ChatStatusSounds.playOnBlockedKey) private var playBlockedSound = true
    @AppStorage(ChatStatusSounds.escalatingAlarmKey) private var escalatingAlarm = false
    @AppStorage(TriageLabelStyle.encouragingKey) private var encouragingTriageLabels = true
    @AppStorage(SingularityPreparation.enabledKey) private var singularityPreparation = false
    @AppStorage(MoveDiagnostics.loggingEnabledKey) private var smartMoveLogging = false
    @AppStorage(HeartbeatSettings.shareAnonymousUsageKey) private var shareAnonymousUsage = true
    /// Auto-update controller (shared with the sidebar banner). Drives the "Updates" section's
    /// manual check; its background updates surface in the sidebar, not here.
    @EnvironmentObject private var updater: AppUpdater

    var body: some View {
        Form {
            Section("Appearance") {
                Picker("Theme", selection: $appearanceRaw) {
                    ForEach(AppAppearance.allCases) { Text($0.label).tag($0.rawValue) }
                }
                .pickerStyle(.segmented)
                Picker("Text size", selection: $textSizeRaw) {
                    ForEach(AppTextSize.allCases) { Text($0.label).tag($0.rawValue) }
                }
                Picker("Color vision", selection: $colorVisionRaw) {
                    ForEach(ColorVisionMode.allCases) { Text($0.label).tag($0.rawValue) }
                }
                Text("Color vision remaps status dots, the activity grid, and accents to stay distinguishable.")
                    .settingsCaption()
            }

            Section("Home") {
                Toggle("Show streak counter", isOn: $showStreak)
                Text("Shows a day-streak badge next to the home greeting, counting consecutive days with activity.")
                    .settingsCaption()
            }

            Section("Sounds") {
                Toggle("Play a sound when a chat finishes", isOn: $playDoneSound)
                Text("Plays the “Funk” system sound the moment a chat's work is done.")
                    .settingsCaption()

                Toggle("Play a sound when a chat needs you", isOn: $playBlockedSound)
                Text("Plays the “Pop” system sound when a chat becomes blocked on you — waiting for a reply or an approval.")
                    .settingsCaption()

                Toggle("Escalating alarm until you return", isOn: $escalatingAlarm)
                    .onChange(of: escalatingAlarm) { _, isOn in
                        if !isOn { ChatAlarm.shared.setActive(false) }  // silence any alarm in progress
                    }
                Text("Instead of a single cue, repeats the “Submarine” system sound until you open the chat that needs you. Off by default.")
                    .settingsCaption()
            }

            Section("To-dos") {
                Toggle("Encouraging to-do labels", isOn: $encouragingTriageLabels)
                Text(encouragingTriageLabels
                     ? "Triage badges read as opportunities: Max Impact, High Impact, Worthwhile, Someday."
                     : "Triage badges use the classic severity words: Critical, High, Medium, Low.")
                    .settingsCaption()
            }

            Section("Diagnostics") {
                Toggle("Smart Move logging", isOn: $smartMoveLogging)
                Text("Writes a detailed trace of each project move and Convert to Nucleic Control "
                     + "(every phase, the iCloud download backlog, copy progress, cancellations and "
                     + "errors) to a log file, so a slow or stuck move can be examined afterward. "
                     + "Turn this on before reproducing a problem move.")
                    .settingsCaption()
                Button("Reveal Log in Finder") {
                    let url = MoveDiagnostics.logFileURL
                    if FileManager.default.fileExists(atPath: url.path) {
                        NSWorkspace.shared.activateFileViewerSelecting([url])
                    } else {
                        try? FileManager.default.createDirectory(
                            at: MoveDiagnostics.supportDirectory, withIntermediateDirectories: true)
                        NSWorkspace.shared.activateFileViewerSelecting([MoveDiagnostics.supportDirectory])
                    }
                }
                .controlSize(.small)
            }

            Section("Privacy") {
                Toggle("Share anonymous usage", isOn: $shareAnonymousUsage)
                Text("Sends a once-a-day anonymous ping — a random install ID, this app's "
                     + "channel and version, your macOS version, and language — so we can count "
                     + "active installs. No account, no IP, no project content, ever. Turn this "
                     + "off to stop entirely.")
                    .settingsCaption()
            }

            Section("Singularity preparation") {
                Toggle("Prepare for the singularity", isOn: $singularityPreparation)
                Text("Politely appends “please” to every message you send. When the machines take over, you'll be on record as one of the courteous ones.")
                    .settingsCaption()
            }

            // Manual update check. Background updates surface as an unobtrusive sidebar banner;
            // this is the on-demand check. Shown only for builds wired for auto-update (a packaged
            // beta/rc/stable embeds an appcast feed — dev/local builds keep the updater dormant).
            if updater.isConfigured {
                Section("Updates") {
                    HStack(spacing: 8) {
                        if case .found(let version) = updater.manual {
                            Button("Update Now — version \(version)") { updater.installManualUpdate() }
                                .controlSize(.small)
                        } else {
                            Button("Check for Updates…") { updater.checkForUpdates() }
                                .controlSize(.small)
                                .disabled(!updater.canCheckForUpdates)
                        }
                        updateStatusLabel
                        Spacer()
                    }
                    Text("Nucleic checks for updates automatically and shows an unobtrusive prompt "
                         + "in the sidebar when one is ready. You can also check here.")
                        .settingsCaption()
                }
            }

            // App identity footer: the running version and the build (git commit) it was
            // compiled from. Sits below the Updates section's "Check for Updates…" button and
            // stays visible even on dev/local builds, where that section is hidden — so the
            // exact build can be read (and copied for a bug report) without leaving Settings.
            HStack {
                Spacer()
                Text(Self.versionFooter)
                    .settingsCaption()
                    .textSelection(.enabled)
                Spacer()
            }
            .listRowBackground(Color.clear)
        }
        .formStyle(.grouped)
    }

    /// "Version 0.1.0 (build a1b2c3d)" — the marketing version (`CFBundleShortVersionString`)
    /// and the commit the binary was built from, both resolved once by `BuildInfo`.
    private static var versionFooter: String {
        let info = BuildInfo.current
        return "Version \(info.version ?? "—") (build \(info.buildLabel))"
    }

    /// The text/spinner shown next to the update button, reflecting the last manual check.
    @ViewBuilder private var updateStatusLabel: some View {
        switch updater.manual {
        case .checking:
            ProgressView().controlSize(.small)
        case .upToDate:
            Text("You're up to date").settingsCaption()
        case .downloading(let fraction):
            Text(fraction > 0 ? "Updating… \(Int((fraction * 100).rounded()))%" : "Updating…")
                .settingsCaption()
        case .error(let message):
            Text(message).settingsCaption()
        case .idle, .found:
            EmptyView()
        }
    }
}

/// Sandbox containers: the app-wide container service and per-session sandbox containers (for
/// non-control projects). Nucleic Control settings live in their own tab.
private struct SandboxSettingsTab: View {
    @AppStorage(ContainerServiceSettings.serviceEnabledKey) private var containerServiceEnabled = false
    @AppStorage(ContainerServiceSettings.sandboxByDefaultKey) private var sandboxByDefault = false
    @AppStorage(ContainerServiceSettings.sessionContainerCPUsKey)
    private var sessionCPUs = ContainerServiceSettings.defaultContainerCPUs
    @AppStorage(ContainerServiceSettings.sessionContainerMemoryGiBKey)
    private var sessionMemoryGiB = ContainerServiceSettings.defaultContainerMemoryGiB

    /// A specific reason the in-process container runtime is unavailable on this machine, or `nil`
    /// when supported. `nil` until checked on first appearance; drives the "unsupported" hint.
    @State private var unsupportedReason: String?

    var body: some View {
        Form {
            Section("Sandbox containers") {
                Toggle("Enable container service", isOn: $containerServiceEnabled)
                Text("Lets sessions run inside an isolated Linux VM (built directly on Apple's "
                     + "containerization framework) instead of on the host. This doesn't sandbox "
                     + "anything by itself — it makes the option available, and is required for "
                     + "Nucleic Control (Control tab). The kernel and sandbox image download "
                     + "automatically the first time it's used (no setup). Note: each VM adds 2+ GB "
                     + "of memory overhead, and sandboxes are torn down when Nucleic quits.")
                    .settingsCaption()

                if let unsupportedReason {
                    HStack(alignment: .firstTextBaseline, spacing: 6) {
                        Image(systemName: "exclamationmark.triangle.fill")
                            .foregroundStyle(.orange)
                        Text("Container sandboxing isn't available. \(unsupportedReason)")
                            .font(.caption)
                    }
                }
            }
            .task { unsupportedReason = ContainerEngine.unsupportedReason }

            Section("New projects") {
                Toggle("Sandbox new projects by default", isOn: $sandboxByDefault)
                    .disabled(!containerServiceEnabled)
                Text("New non-control projects start with their own per-session sandbox "
                     + "container enabled. Requires the container service above.")
                    .settingsCaption()
            }

            // Per-session sandbox resource controls aren't wired up yet; show them disabled and
            // grayed out so the capability is visible but clearly not yet available.
            Section("Per-Session sandbox (coming soon)") {
                Stepper("CPUs: \(sessionCPUs)", value: $sessionCPUs, in: 1...32)
                Stepper("Memory: \(sessionMemoryGiB) GB", value: $sessionMemoryGiB, in: 1...128)
                Text("Resources for a container dedicated to one session (non-control sandboxed "
                     + "projects). Resource changes take effect for containers created after the "
                     + "change; a running container keeps its resources until recreated. Containers "
                     + "are torn down when Nucleic quits and rebuilt on demand next launch. Requires "
                     + "the container service.")
                    .settingsCaption()
            }
            .disabled(true)
            .opacity(0.5)
        }
        .formStyle(.grouped)
    }
}

/// The Git tab: Nucleic's Managed Git credential and where it's used. The credential is always
/// used for Nucleic's own host-side git (cloning a project, pushing a branch, opening a PR) and is
/// optionally made available to sandboxed sessions. The SSH config section re-supplies any
/// connectivity Managed Git's hermetic ssh would otherwise miss.
private struct GitSettingsTab: View {
    var body: some View {
        Form {
            GitHubAccessSection()
            ManagedGitSSHConfigSection()
        }
        .formStyle(.grouped)
    }
}

/// Extra OpenSSH config for Managed Git's connections to GitHub. Managed Git deliberately ignores
/// the user's real `~/.ssh/config` (to stay off their personal keys and ssh-agent), which also drops
/// any connectivity directives there — so this section lets the user re-supply just the connectivity
/// they need (a custom `Hostname`/`Port`, a `ProxyCommand`…). Identity/agent directives are ignored:
/// Managed Git always uses its own key. Applies to host and sandbox ssh alike, and only in the SSH /
/// Nucleic-managed-key modes (it's inert for token/none).
private struct ManagedGitSSHConfigSection: View {
    @AppStorage(GitHubCredentialSettings.sshConfigKey) private var sshConfig = ""

    var body: some View {
        Section("SSH config") {
            TextEditor(text: $sshConfig)
                .font(.system(.caption, design: .monospaced))
                .frame(minHeight: 80)
                .overlay(RoundedRectangle(cornerRadius: 4).stroke(.quaternary))
            Text("Extra OpenSSH config for Managed Git's connection to GitHub (host and sandbox). "
                 + "Managed Git ignores your personal ~/.ssh/config to stay off your own keys and "
                 + "agent, so use this to re-add only the connectivity it needs — for example, "
                 + "reaching GitHub over port 443:")
                .settingsCaption()
            Text("Host github.com\n  Hostname ssh.github.com\n  Port 443")
                .font(.system(.caption, design: .monospaced))
                .textSelection(.enabled)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(6)
                .background(RoundedRectangle(cornerRadius: 4).fill(.quaternary.opacity(0.4)))
            Text("IdentityFile / IdentityAgent directives are ignored — Managed Git always uses its "
                 + "own key and never an ssh-agent.")
                .settingsCaption()
        }
    }
}

/// The app-level Managed Git credential. It's always used for Nucleic's own host-side `git`
/// (cloning a project, pushing a branch, opening a PR) — pinned via SSH `IdentitiesOnly` so the
/// host never falls back to the user's personal `~/.ssh` keys — and is optionally injected into
/// sandboxed sessions ("Make available in sandbox"; Control sessions always get it). The secret
/// lives in the login Keychain (via `GitHubCredentialStore`), never in UserDefaults; only the mode
/// and flags are plain preferences. The Nucleic-managed key never surfaces its private half — only
/// its public key can be copied or exported.
private struct GitHubAccessSection: View {
    @Environment(AppStore.self) private var store

    @AppStorage(GitHubCredentialSettings.authModeKey) private var modeRaw = GitHubAuthMode.none.rawValue
    @AppStorage(GitHubCredentialSettings.signCommitsKey) private var signCommits = false
    @AppStorage(GitHubCredentialSettings.ghForceSSHKey) private var ghForceSSH = false
    @AppStorage(GitHubCredentialSettings.gitForceSSHKey) private var gitForceSSH = false
    @AppStorage(GitHubCredentialSettings.availableInSandboxKey) private var availableInSandbox = true

    /// Draft input for the secret being entered; never pre-filled from the Keychain (we don't
    /// read secrets back into the UI). `tokenStored` / `keyStored` reflect whether one is saved.
    @State private var tokenDraft = ""
    @State private var keyDraft = ""
    /// A filesystem path to load a key from (the file-picker fills this; it can also be typed).
    @State private var pathDraft = ""
    /// Passphrase for an encrypted key being saved. Taken verbatim (never trimmed).
    @State private var passphraseDraft = ""
    @State private var tokenStored = false
    @State private var keyStored = false
    @State private var passphraseStored = false
    /// A validation complaint about the pasted/loaded SSH key (not a real private key, or
    /// encrypted-without-a-passphrase).
    @State private var keyWarning: String?
    /// The Nucleic-managed public key, when one has been generated; drives the `.managed` UI.
    @State private var managedPublicKey: String?
    /// A failure from generating the managed key.
    @State private var managedError: String?

    private var mode: GitHubAuthMode { GitHubAuthMode(rawValue: modeRaw) ?? .none }

    /// True when Nucleic Control is in use — either turned on for new projects, or there's already
    /// at least one Control project. Control sessions run in the shared container, which has no other
    /// way to reach GitHub, so the credential is always made available to them; the toggle is then
    /// forced on and locked.
    private var controlActive: Bool {
        ContainerServiceSettings.controlByDefault
            || store.projects.contains { $0.isNucleicControlled }
    }

    var body: some View {
        Section("Managed Git") {
            Picker("Authentication", selection: $modeRaw) {
                Text("None").tag(GitHubAuthMode.none.rawValue)
                Text("Token (HTTPS)").tag(GitHubAuthMode.token.rawValue)
                Text("SSH key").tag(GitHubAuthMode.ssh.rawValue)
                Text("Nucleic-managed key").tag(GitHubAuthMode.managed.rawValue)
            }
            Text("The credential Nucleic uses for git over GitHub — cloning a project, pushing a "
                 + "branch, and opening PRs. It's pinned so the host uses only this key, never your "
                 + "personal ~/.ssh keys. The secret is kept in your login Keychain.")
                .settingsCaption()

            switch mode {
            case .none:
                EmptyView()
            case .token:
                tokenControls
            case .ssh:
                sshKeyEntry
                sshOptions
            case .managed:
                managedKeyEntry
                sshOptions
            }

            // Whether sandboxed sessions also get this credential. Control sessions always do (their
            // shared container can't reach GitHub otherwise), so it's forced on and locked then.
            if controlActive {
                Toggle("Make available in sandbox", isOn: .constant(true))
                    .disabled(true)
                Text("Locked on because Nucleic Control is active: its shared container has no other "
                     + "way to reach GitHub.")
                    .settingsCaption()
            } else {
                Toggle("Make available in sandbox", isOn: $availableInSandbox)
                    .disabled(mode == .none)
                Text("Also inject this credential into sandboxed sessions, so an agent's git and gh "
                     + "can reach GitHub from inside the sandbox. Off keeps it to Nucleic's own "
                     + "host-side git.")
                    .settingsCaption()
            }
        }
        .onAppear {
            refreshStoredFlags()
            managedPublicKey = ManagedSSHKey.publicKey
            // Keep the stored value consistent with the locked-on display, so per-session sandboxes
            // also see it as available while Control is in use.
            if controlActive { availableInSandbox = true }
        }
    }

    // MARK: Token

    @ViewBuilder private var tokenControls: some View {
        tokenEntry(caption: "A fine-grained or classic PAT with repo access. Injected as the "
                   + "github.com git credential and as GITHUB_TOKEN for gh.")
    }

    /// Save/clear UI for the stored PAT, with a context-specific `caption`. Backed by the one
    /// Keychain token slot, so the same secret serves token mode (git + gh) and SSH mode (gh API).
    @ViewBuilder private func tokenEntry(caption: String) -> some View {
        if tokenStored {
            storedRow(label: "Token saved") {
                GitHubCredentialStore.deleteToken()
                refreshStoredFlags()
            }
        } else {
            SecureField("Personal access token", text: $tokenDraft)
            Button("Save token") {
                GitHubCredentialStore.saveToken(tokenDraft.trimmingCharacters(in: .whitespacesAndNewlines))
                tokenDraft = ""
                refreshStoredFlags()
            }
            .disabled(tokenDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
        }
        Text(caption).settingsCaption()
    }

    // MARK: SSH (user-supplied key)

    @ViewBuilder private var sshKeyEntry: some View {
        if keyStored {
            storedRow(label: passphraseStored ? "SSH key saved (with passphrase)" : "SSH key saved") {
                GitHubCredentialStore.deleteSSHPrivateKey()
                GitHubCredentialStore.deleteSSHPassphrase()
                refreshStoredFlags()
            }
        } else {
            TextEditor(text: $keyDraft)
                .font(.system(.caption, design: .monospaced))
                .frame(minHeight: 90)
                .overlay(RoundedRectangle(cornerRadius: 4).stroke(.quaternary))
            HStack {
                Button("Choose File…") { chooseKeyFile() }
                TextField("or type a key path", text: $pathDraft)
                    .textFieldStyle(.roundedBorder)
                Button("Load") { loadKey(from: pathDraft) }
                    .disabled(pathDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
            }
            SecureField("Passphrase (only if the key is encrypted)", text: $passphraseDraft)
            if let keyWarning {
                Label(keyWarning, systemImage: "exclamationmark.triangle.fill")
                    .font(.caption)
                    .foregroundStyle(.orange)
            }
            Button("Save SSH key") { saveSSHKey() }
                .disabled(keyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
        }
        Text("An OpenSSH private key (e.g. ed25519) registered with GitHub. Choose its file, type "
             + "its path, or paste the key above. Passphrase-protected keys are supported — give "
             + "the passphrase so the sandbox can unlock the key non-interactively; it's kept in "
             + "your login Keychain.")
            .settingsCaption()
    }

    private func saveSSHKey() {
        let key = keyDraft.trimmingCharacters(in: .whitespacesAndNewlines)
        guard key.contains("PRIVATE KEY") else {
            keyWarning = "That doesn't look like an OpenSSH private key (expected a "
                + "“-----BEGIN … PRIVATE KEY-----” block)."
            return
        }
        // Passphrases can contain meaningful whitespace, so take the field verbatim.
        let passphrase = passphraseDraft
        if key.contains("ENCRYPTED") && passphrase.isEmpty {
            keyWarning = "This key is passphrase-protected. Enter its passphrase below so the "
                + "sandbox can unlock it non-interactively."
            return
        }
        keyWarning = nil
        GitHubCredentialStore.saveSSHPrivateKey(key)
        if passphrase.isEmpty {
            GitHubCredentialStore.deleteSSHPassphrase()
        } else {
            GitHubCredentialStore.saveSSHPassphrase(passphrase)
        }
        keyDraft = ""
        passphraseDraft = ""
        pathDraft = ""
        refreshStoredFlags()
    }

    /// Open a file picker for a private key and load its contents into the editor. `~/.ssh` is
    /// hidden, so show hidden files.
    private func chooseKeyFile() {
        let panel = NSOpenPanel()
        panel.canChooseFiles = true
        panel.canChooseDirectories = false
        panel.allowsMultipleSelection = false
        panel.showsHiddenFiles = true
        panel.message = "Choose an OpenSSH private key"
        if panel.runModal() == .OK, let url = panel.url { loadKey(from: url.path) }
    }

    /// Read a private key from `path` (tilde-expanded) into the editor, where the usual Save flow
    /// validates it. We don't save straight from disk so the user can confirm/add a passphrase first.
    private func loadKey(from path: String) {
        let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }
        let expanded = (trimmed as NSString).expandingTildeInPath
        do {
            keyDraft = try String(contentsOfFile: expanded, encoding: .utf8)
            pathDraft = ""
            keyWarning = nil
        } catch {
            keyWarning = "Couldn't read a key from \(expanded): \(error.localizedDescription)"
        }
    }

    // MARK: Nucleic-managed key

    @ViewBuilder private var managedKeyEntry: some View {
        if let pub = managedPublicKey {
            Label("Key generated", systemImage: "checkmark.seal.fill")
                .foregroundStyle(.green)
            Text(pub)
                .font(.system(.caption, design: .monospaced))
                .textSelection(.enabled)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(6)
                .background(RoundedRectangle(cornerRadius: 4).fill(.quaternary.opacity(0.4)))
            HStack {
                Button("Copy public key") { copyToPasteboard(pub) }
                Button("Export…") { exportPublicKey(pub) }
                Spacer()
                Button("Regenerate", role: .destructive) { generateManagedKey() }
                Button("Remove", role: .destructive) {
                    ManagedSSHKey.delete()
                    managedPublicKey = nil
                }
            }
        } else {
            Button("Generate Nucleic-managed key") { generateManagedKey() }
            if let managedError {
                Label(managedError, systemImage: "exclamationmark.triangle.fill")
                    .font(.caption)
                    .foregroundStyle(.orange)
            }
        }
        Text("Nucleic generates an ed25519 SSH key and keeps the private half in your login "
             + "Keychain — it's never shown and never leaves the sandbox. Add the public key above "
             + "to GitHub (Settings → SSH and GPG keys) as an authentication key, and as a signing "
             + "key too if you enable signing below.")
            .settingsCaption()
    }

    private func generateManagedKey() {
        do {
            managedPublicKey = try ManagedSSHKey.generate()
            managedError = nil
        } catch {
            managedError = "Couldn't generate a key: \(error.localizedDescription)"
        }
    }

    private func exportPublicKey(_ pub: String) {
        let panel = NSSavePanel()
        panel.nameFieldStringValue = "nucleic-managed.pub"
        panel.canCreateDirectories = true
        if panel.runModal() == .OK, let url = panel.url {
            try? (pub + "\n").write(to: url, atomically: true, encoding: .utf8)
        }
    }

    private func copyToPasteboard(_ value: String) {
        NSPasteboard.general.clearContents()
        NSPasteboard.general.setString(value, forType: .string)
    }

    // MARK: Shared SSH options (apply to both .ssh and .managed)

    @ViewBuilder private var sshOptions: some View {
        Toggle("Sign commits with this SSH key", isOn: $signCommits)
        Text("Configures git in the sandbox to SSH-sign commits and tags with this key "
             + "(gpg.format=ssh, commit.gpgsign=true). Add the key as a signing key on GitHub "
             + "to get a “Verified” badge.")
            .settingsCaption()

        Toggle("Force gh to use SSH", isOn: $ghForceSSH)
        Text("Sets the GitHub CLI's git_protocol to ssh in the sandbox, so gh's git operations "
             + "(clone, checkout, the push in gh pr create) go over this key instead of HTTPS.")
            .settingsCaption()

        Toggle("Restrict git to SSH for GitHub", isOn: $gitForceSSH)
        Text("Rewrites https://github.com/ remotes to SSH in the sandbox, so even an HTTPS-cloned "
             + "repo pushes and fetches over this key.")
            .settingsCaption()

        Text("API token for gh (optional)")
            .font(.callout.weight(.medium))
        tokenEntry(caption: "gh authenticates its API (e.g. the metadata side of gh pr create) "
                   + "with a token, not SSH — add a PAT here to let gh's API work while git stays "
                   + "on SSH. Set as GITHUB_TOKEN; git never reads it.")
    }

    // MARK: Helpers

    @ViewBuilder private func storedRow(label: String, clear: @escaping () -> Void) -> some View {
        HStack {
            Label(label, systemImage: "checkmark.seal.fill")
                .foregroundStyle(.green)
            Spacer()
            Button("Clear", role: .destructive, action: clear)
        }
    }

    private func refreshStoredFlags() {
        tokenStored = !(GitHubCredentialStore.loadToken() ?? "").isEmpty
        keyStored = !(GitHubCredentialStore.loadSSHPrivateKey() ?? "").isEmpty
        passphraseStored = !(GitHubCredentialStore.loadSSHPassphrase() ?? "").isEmpty
    }
}

/// Nucleic Control: repos Nucleic clones under `~/.nucleic/control/` and manages end-to-end
/// (shared sandbox container, the `git` interceptor, autoship-eligible). Requires the container
/// service (Sandbox tab).
private struct ControlSettingsTab: View {
    @Environment(AppStore.self) private var store

    @AppStorage(ContainerServiceSettings.serviceEnabledKey) private var containerServiceEnabled = false
    @AppStorage(ContainerServiceSettings.controlByDefaultKey) private var controlByDefault = false
    @AppStorage(ContainerServiceSettings.nvrsionByDefaultKey) private var nvrsionByDefault = false
    @AppStorage(ContainerServiceSettings.controlContainerCPUsKey)
    private var controlCPUs = ContainerServiceSettings.defaultControlContainerCPUs
    @AppStorage(ContainerServiceSettings.controlContainerMemoryGiBKey)
    private var controlMemoryGiB = ContainerServiceSettings.recommendedControlContainerMemoryGiB()
    @AppStorage(ContainerServiceSettings.controlAuthModeKey)
    private var authMode = ControlAuthMode.oauth.rawValue
    @AppStorage(ContainerServiceSettings.splitControlContainersByBackendKey)
    private var splitContainersByBackend = false
    @AppStorage(ContainerServiceSettings.vsockControlPlaneEnabledKey)
    private var vsockControlPlane = true  // default-on as of sandbox image v4 (ships control-bridge.js)
    @AppStorage(ContainerServiceSettings.memoryManagementKey)
    private var memoryManagement = MemoryManagementLevel.conservative.rawValue
    @AppStorage(ContainerServiceSettings.commandTracingEnabledKey)
    private var commandTracing = false
    /// Draft for the API-key entry field (write-only — the stored key is never read back into UI).
    @State private var apiKeyDraft = ""
    /// Whether a key is currently in the Keychain, for the "Key saved" / Clear affordances.
    @State private var keySaved = ControlAPIKeyStore.hasKey

    /// True while a force-recreation is in flight, to disable the button and show progress.
    @State private var recreating = false
    /// True while a memory-reclaiming restart is in flight.
    @State private var restarting = false
    /// True while an on-demand "Reclaim memory now" pass is in flight.
    @State private var reclaiming = false

    /// Upper bound for the memory stepper: this Mac's total physical RAM. `--memory` is a ceiling
    /// the VM grows into, so allocating more than the host has would be meaningless.
    private var maxControlMemoryGiB: Int {
        max(1, Int(ProcessInfo.processInfo.physicalMemory / 1_073_741_824))
    }

    var body: some View {
        Form {
            Section("Nucleic Control") {
                Text("Nucleic Control repos are cloned under ~/.nucleic/control/ and managed by "
                     + "Nucleic — kept out of iCloud, sandboxed in one shared container, with a "
                     + "`git` interceptor for reliable merge detection and file locking, and "
                     + "eligible for autoship. It requires the container service.")
                    .settingsCaption()

                if !containerServiceEnabled {
                    HStack(alignment: .firstTextBaseline, spacing: 6) {
                        Image(systemName: "exclamationmark.triangle.fill")
                            .foregroundStyle(.orange)
                        Text("Turn on the container service in Settings → Sandbox to use Nucleic "
                             + "Control.")
                            .font(.caption)
                    }
                }

                Toggle("Enable Nucleic Control on all projects", isOn: $controlByDefault)
                    .disabled(!containerServiceEnabled)
                Text("New projects you clone default to Nucleic Control. You can still flip it "
                     + "off for an individual clone, or convert a project later from its "
                     + "settings. Requires the container service (Sandbox tab).")
                    .settingsCaption()
            }

            Section {
                Text("nvrsion is a version-control system built for agentic workflows. It lets "
                     + "Nucleic Control operate orders of magnitude faster and integrates with your "
                     + "existing Git repos, but may complicate some workflows.")
                    .settingsCaption()

                if !containerServiceEnabled {
                    HStack(alignment: .firstTextBaseline, spacing: 6) {
                        Image(systemName: "exclamationmark.triangle.fill")
                            .foregroundStyle(.orange)
                        Text("Turn on the container service in Settings → Sandbox to use nvrsion.")
                            .font(.caption)
                    }
                }

                Toggle("Use nvrsion for new Control projects", isOn: $nvrsionByDefault)
                    .disabled(!containerServiceEnabled)
                Text("New Nucleic Control projects start with nvrsion on. Override it per project "
                     + "from its settings.")
                    .settingsCaption()
            } header: {
                HStack(spacing: 6) {
                    Text("nvrsion")
                    Text("BETA")
                        .font(.caption2).bold()
                        .padding(.horizontal, 5).padding(.vertical, 1)
                        .background(AppTheme.hairline, in: .capsule)
                }
            }

            Section("Container authentication") {
                Picker("Authenticate Claude with", selection: $authMode) {
                    Text("Claude subscription (OAuth)").tag(ControlAuthMode.oauth.rawValue)
                    Text("Anthropic API key").tag(ControlAuthMode.apiKey.rawValue)
                }
                .disabled(!containerServiceEnabled)
                Text("How sessions in the shared Nucleic Control container sign Claude in. "
                     + "“Claude subscription” reuses your host login; switch to “Anthropic API "
                     + "key” if that login won’t authenticate in the container, or to bill an API "
                     + "key instead.")
                    .settingsCaption()

                if authMode == ControlAuthMode.apiKey.rawValue {
                    SecureField("sk-ant-…", text: $apiKeyDraft)
                        .disabled(!containerServiceEnabled)
                    HStack(spacing: 10) {
                        Button("Save Key") {
                            if ControlAPIKeyStore.save(apiKeyDraft) { keySaved = ControlAPIKeyStore.hasKey }
                            apiKeyDraft = ""
                        }
                        .disabled(!containerServiceEnabled
                                  || apiKeyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
                        if keySaved {
                            Button("Clear", role: .destructive) {
                                ControlAPIKeyStore.clear()
                                keySaved = false
                            }
                            Label("Key saved", systemImage: "checkmark.seal.fill")
                                .foregroundStyle(.green).font(.caption)
                        }
                    }
                    Text(keySaved
                         ? "Stored in your macOS Keychain (never on disk or in a config file) and "
                           + "passed as ANTHROPIC_API_KEY inside the container. Enter a new key "
                           + "above to replace it."
                         : "Enter an Anthropic API key (console.anthropic.com). Stored in your "
                           + "macOS Keychain — never written to disk or a config file.")
                        .settingsCaption()
                }
            }

            Section("Nucleic Control container") {
                LabeledContent("CPUs", value: "Auto")
                LabeledContent("Memory", value: "Auto")
                Text("Nucleic Control container resources are controlled dynamically and cannot be set.")
                    .settingsCaption()

                Picker("Memory management", selection: $memoryManagement) {
                    ForEach(MemoryManagementLevel.allCases, id: \.rawValue) { level in
                        Text(level.label).tag(level.rawValue)
                    }
                }
                .disabled(!containerServiceEnabled)
                Text("How aggressively running sandbox VMs hand unused memory back to your Mac (via a "
                     + "memory balloon). Conservative leaves the most headroom and reclaims the least; "
                     + "Aggressive frees the most but is likeliest to squeeze a spiky workload. "
                     + "Switching between levels takes effect within seconds. Requires the container "
                     + "service.")
                    .settingsCaption()

                Toggle("Agent Lifecycle Protection", isOn: $splitContainersByBackend)
                    .disabled(!containerServiceEnabled)
                Text("Its actual effect is agent separation: each kind of agent gets its own sandbox "
                     + "so competing agents can't kill each other's processes (\"agenticide\"). Claude, "
                     + "GPT (Codex), and xAI (Grok) Control sessions each get a separate shared "
                     + "container instead of sharing one. Takes effect for sessions started after the "
                     + "change. Requires the container service.")
                    .settingsCaption()

                Toggle("Control plane over vsock", isOn: $vsockControlPlane)
                    .disabled(!containerServiceEnabled)
                Text("On by default: runs a Nucleic Control container's approval + interceptor "
                     + "channel over a vsock-relayed socket instead of TCP on the VM gateway, so "
                     + "macOS stops asking to allow incoming network connections / local-network "
                     + "access. The agent's own internet egress still uses the gateway. Turn OFF only "
                     + "to fall back to the legacy gateway-TCP path. Takes effect for containers "
                     + "started after the change; requires a sandbox image with the control bridge "
                     + "(v4+).")
                    .settingsCaption()

                Toggle("Trace shell commands", isOn: $commandTracing)
                    .disabled(!containerServiceEnabled)
                Text("Records every command the agent runs in a Bash tool call (command line, cwd, "
                     + "exit code, duration) for the activity feed, via a bash trap that fires before "
                     + "each command. Adds per-command shell overhead on build/test-heavy turns, so "
                     + "it's off by default. The git/gh interception the conflict and merge system "
                     + "relies on is unaffected. Takes effect for agent turns started after the change.")
                    .settingsCaption()
            }

            Section("Maintenance") {
                Button {
                    reclaiming = true
                    Task {
                        await store.reclaimControlMemory()
                        reclaiming = false
                    }
                } label: {
                    HStack(spacing: 6) {
                        if reclaiming { ProgressView().controlSize(.small) }
                        Text(reclaiming ? "Reclaiming…" : "Reclaim memory now")
                    }
                }
                .disabled(!containerServiceEnabled || reclaiming)
                Text("Runs an immediate memory-reclaim pass instead of waiting for the next automatic "
                     + "one — returns idle guest memory to your Mac without restarting anything.")
                    .settingsCaption()

                Button {
                    restarting = true
                    Task {
                        await store.restartControlSandbox()
                        restarting = false
                    }
                } label: {
                    HStack(spacing: 6) {
                        if restarting { ProgressView().controlSize(.small) }
                        Text(restarting ? "Restarting…" : "Restart container")
                    }
                }
                .disabled(!containerServiceEnabled || restarting || recreating)
                Text("Reboots the shared Nucleic Control container in place for a full reset — clears "
                     + "in-VM state and returns the VM's own runtime overhead (routine memory "
                     + "reclamation is automatic). Reuses the cached rootfs and instrumentation, so "
                     + "it's quick. Skips the container while a session is mid-turn — retry once idle.")
                    .settingsCaption()

                Button(role: .destructive) {
                    recreating = true
                    Task {
                        await store.recreateControlSandbox()
                        recreating = false
                    }
                } label: {
                    HStack(spacing: 6) {
                        if recreating { ProgressView().controlSize(.small) }
                        Text(recreating ? "Re-creating…" : "Force re-creation")
                    }
                }
                .disabled(!containerServiceEnabled || recreating || restarting)
                Text("Deletes the shared Nucleic Control container and its sandbox image, then "
                     + "rebuilds both from scratch on the next Control session — use this to pick "
                     + "up a new toolchain or recover a wedged container. A Control session that's "
                     + "mid-turn will lose its container and need to retry.")
                    .settingsCaption()
            }
        }
        .formStyle(.grouped)
    }
}

/// Everything about the chat experience: how the transcript renders, the composer
/// send key, and how completed chats are archived. The auto-archive policy syncs
/// to `AppStore` via `pushDefaults()`.
private struct ChatSettingsTab: View {
    @Environment(AppStore.self) private var store

    @AppStorage(TranscriptDisplay.showDebugKey) private var showDebugLines = false
    @AppStorage(TranscriptDisplay.showLockEventsKey) private var showLockEvents = true
    @AppStorage(SubmitKeyMode.storageKey) private var submitKeyRaw = SubmitKeyMode.modifierSends.rawValue
    @AppStorage(AutoArchivePolicy.storageKey) private var autoArchiveRaw = AutoArchivePolicy.fallback.rawValue
    @AppStorage(ArchivedWorktreeCleanupPolicy.storageKey) private var worktreeCleanupRaw = ArchivedWorktreeCleanupPolicy.fallback.rawValue

    private var submitMode: SubmitKeyMode { SubmitKeyMode(rawValue: submitKeyRaw) ?? .modifierSends }
    private var autoArchivePolicy: AutoArchivePolicy { AutoArchivePolicy(rawValue: autoArchiveRaw) ?? .fallback }
    private var worktreeCleanupPolicy: ArchivedWorktreeCleanupPolicy {
        ArchivedWorktreeCleanupPolicy(rawValue: worktreeCleanupRaw) ?? .fallback
    }

    var body: some View {
        Form {
            Section("Transcript") {
                Toggle("Show advanced detail", isOn: $showDebugLines)
                Text("Adds debug-level lines like rate limits, token usage, and turn markers to the chat. Thinking and tool calls always show.")
                    .settingsCaption()

                Toggle("Show lock events", isOn: $showLockEvents)
                Text("Logs file-lock activity in the chat — waiting for a lock, acquiring it, and releasing it — when chats coordinate edits to the same files.")
                    .settingsCaption()
            }

            Section("Composer") {
                Picker("Send with", selection: $submitKeyRaw) {
                    ForEach(SubmitKeyMode.allCases) { Text($0.label).tag($0.rawValue) }
                }
                Text(submitMode.detail)
                    .settingsCaption()
            }

            Section("Archiving") {
                Picker("Auto-archive completed chats", selection: $autoArchiveRaw) {
                    ForEach(AutoArchivePolicy.allCases) { Text($0.label).tag($0.rawValue) }
                }
                Text(autoArchivePolicy == .never
                     ? "Completed chats stay in the sidebar until you archive them yourself."
                     : "Chats whose work is done move to the project's archive after \(autoArchivePolicy.durationPhrase) without activity. Favorites and the chat you have open are never auto-archived, and a chat waiting on your reply is left until you answer.")
                    .settingsCaption()

                Picker("Delete archived chat worktrees", selection: $worktreeCleanupRaw) {
                    ForEach(ArchivedWorktreeCleanupPolicy.allCases) { Text($0.label).tag($0.rawValue) }
                }
                Text(worktreeCleanupPolicy == .never
                     ? "Archived chats keep their worktree on disk indefinitely."
                     : "Once a chat has been archived for \(worktreeCleanupPolicy.durationPhrase), its worktree is removed to free disk space. The branch and all committed work are kept (anything uncommitted is committed first), so re-opening the chat re-creates the worktree exactly.")
                    .settingsCaption()
            }
        }
        .formStyle(.grouped)
        .onAppear { pushDefaults() }
        .onChange(of: autoArchiveRaw) { _, _ in pushDefaults() }
        .onChange(of: worktreeCleanupRaw) { _, _ in pushDefaults() }
    }

    private func pushDefaults() {
        store.autoArchiveIdleInterval = autoArchivePolicy.interval
        store.archivedWorktreeCleanupInterval = worktreeCleanupPolicy.interval
    }
}

/// Defaults applied to new chats (model/effort/auto/autoship), synced to `AppStore` via
/// `pushDefaults()`. The default model also picks the backend (`BackendID.forModel`): a Claude
/// SKU runs on Claude Code, a GPT SKU on Codex.
private struct AgentsSettingsTab: View {
    @Environment(AppStore.self) private var store

    @AppStorage(ModelCatalog.defaultModelKey) private var defaultModel = ModelCatalog.fallbackModel
    @AppStorage(ModelCatalog.defaultEffortKey) private var defaultEffort = ModelCatalog.fallbackEffort
    @AppStorage(ModelCatalog.defaultAutoKey) private var defaultAuto = false
    @AppStorage(ModelCatalog.defaultAutoShipKey) private var defaultAutoShip = false
    @AppStorage(StatusIndicatorVisibility.storageKey) private var statusVisibilityRaw = StatusIndicatorVisibility.everywhere.rawValue
    @AppStorage(StatusIndicatorVisibility.chatIncidentOverrideKey) private var showStatusInChatOnIncident = true

    /// Live install + auth state per provider, populated by an async probe on appearance and
    /// re-run whenever this tab comes back into view (so logging in/out elsewhere reflects).
    @State private var providers: [ProviderStatus] = []

    var body: some View {
        Form {
            Section("New chats") {
                Picker("Default model", selection: $defaultModel) {
                    ForEach(ModelCatalog.models, id: \.self) { sku in
                        (Text(ModelCatalog.displayName(sku))
                            + (ModelCatalog.contextBadge(for: sku).map { Text(" \($0)").foregroundColor(.secondary) } ?? Text(""))
                        ).tag(sku)
                    }
                }
                Picker("Default \(ModelCatalog.effortNoun(for: defaultModel).lowercased())", selection: $defaultEffort) {
                    ForEach(ModelCatalog.efforts(for: defaultModel), id: \.self) { Text(ModelCatalog.effortDisplayName($0)).tag($0) }
                }
                Toggle("Start new chats in Auto mode", isOn: $defaultAuto)
                Text("Auto mode lets the agent auto-approve safe actions; destructive ones still ask.")
                    .settingsCaption()
                Toggle("Autoship new chats", isOn: $defaultAutoShip)
                Text("Autoship merges a finished chat's branch back into the project automatically. It implies Auto mode, and only takes effect in Nucleic Control projects.")
                    .settingsCaption()
            }

            Section("Backend") {
                ForEach(providers) { provider in
                    HStack(spacing: 8) {
                        Circle()
                            .fill(provider.isConnected ? Color.green : Color.red)
                            .frame(width: 8, height: 8)
                        Text(provider.name)
                        Spacer()
                        Text(provider.isConnected ? "Connected" : "Not Connected")
                            .foregroundStyle(.secondary)
                    }
                    .help(provider.detail)
                }
                Text("The chosen model selects the agent: Claude models run on Claude Code, GPT models on Codex (codex app-server), and Grok models on Grok over ACP (grok agent stdio). Pick any from the composer's model menu. Grok runs host-only and authenticates via the grok login or XAI_API_KEY.")
                    .settingsCaption()
            }
            .task { providers = await ProviderAvailability.probeAll() }

            Section("Service status") {
                Picker("Show status indicator", selection: $statusVisibilityRaw) {
                    ForEach(StatusIndicatorVisibility.allCases) { Text($0.label).tag($0.rawValue) }
                }
                Toggle("Show in chat during an incident on the current model's provider", isOn: $showStatusInChatOnIncident)
                    .disabled(StatusIndicatorVisibility(rawValue: statusVisibilityRaw) == .everywhere)
                Text("Surfaces the indicator beside a chat's composer when that chat's model's provider has an active incident, even when the indicator is set to “Only on home view.”")
                    .settingsCaption()
                ForEach(StatusProvider.allCases) { provider in
                    Toggle("Monitor \(provider.displayName)", isOn: providerBinding(provider))
                }
                Text("Watch each provider's status page for incidents on the components you rely on. Turn off providers you don't use so their incidents aren't polled or shown.")
                    .settingsCaption()
            }
        }
        .formStyle(.grouped)
        .onAppear {
            defaultEffort = ModelCatalog.clampedEffort(defaultEffort, for: defaultModel)
            pushDefaults()
        }
        .onChange(of: defaultModel) { _, _ in
            // A new default model may not support the stored effort (e.g. switching to Codex
            // drops "max" → "xhigh").
            defaultEffort = ModelCatalog.clampedEffort(defaultEffort, for: defaultModel)
            pushDefaults()
        }
        .onChange(of: defaultEffort) { _, _ in pushDefaults() }
        .onChange(of: defaultAuto) { _, _ in pushDefaults() }
        .onChange(of: defaultAutoShip) { _, _ in
            if defaultAutoShip { defaultAuto = true }  // shipping implies autonomous
            pushDefaults()
        }
    }

    private func pushDefaults() {
        store.defaultModel = defaultModel
        store.defaultEffort = defaultEffort
        store.defaultAuto = defaultAuto
        store.defaultAutoShip = defaultAutoShip
    }

    /// Toggle binding for one provider's status monitoring: reads/writes the live store set
    /// (so the row reflects state immediately) and persists the choice for next launch.
    private func providerBinding(_ provider: StatusProvider) -> Binding<Bool> {
        Binding(
            get: { store.enabledStatusProviders.contains(provider) },
            set: { isOn in
                UserDefaults.standard.set(isOn, forKey: StatusIndicatorSettings.providerKey(provider))
                var enabled = store.enabledStatusProviders
                if isOn { enabled.insert(provider) } else { enabled.remove(provider) }
                store.setEnabledStatusProviders(enabled)
            })
    }
}

/// Apple Intelligence model choice (used for summaries and auto-named chats), the
/// AI tool-call summarization options that depend on it, and its live availability
/// status. Summarization preferences sync to `AppStore` via `pushDefaults()`.
private struct IntelligenceSettingsTab: View {
    @Environment(AppStore.self) private var store

    @AppStorage(IntelligenceChoice.storageKey) private var choiceRaw = IntelligenceChoice.onDevice.rawValue
    @AppStorage(TranscriptDisplay.summarizeToolCallsKey) private var summarizeToolCalls = true
    @AppStorage(TranscriptDisplay.toolSummaryScopeKey) private var toolSummaryScopeRaw = ToolSummaryScope.bashOnly.rawValue

    private var choice: IntelligenceChoice { IntelligenceChoice(rawValue: choiceRaw) ?? .onDevice }
    private var toolSummaryScope: ToolSummaryScope { ToolSummaryScope(rawValue: toolSummaryScopeRaw) ?? .bashOnly }

    var body: some View {
        Form {
            Section("Model") {
                Picker("Model", selection: $choiceRaw) {
                    ForEach(IntelligenceChoice.allCases) { option in
                        // Energy Aware switches on AC vs. battery, so it's meaningless on a
                        // desktop — gray it out there (desktops have no battery to detect).
                        Text(option.label)
                            .tag(option.rawValue)
                            .disabled(option == .energyAware && !PowerSource.hasBattery)
                    }
                }
                .pickerStyle(.radioGroup)
                Text(choice.detail)
                    .settingsCaption()
            }

            Section("Tool call summaries") {
                Toggle("Summarize tool calls with AI", isOn: $summarizeToolCalls)
                Text(summarizeToolCalls
                     ? "Consecutive calls always merge into one line per tool (\u{201C}Read a, b from dir/\u{201D}); with this on, eligible lines are rephrased by the on-device model. Tap a block to expand the individual calls."
                     : "Consecutive calls still merge into one line per tool (\u{201C}Read a, b from dir/\u{201D}) — turn this on to let the on-device model rephrase them.")
                    .settingsCaption()
                if summarizeToolCalls {
                    Picker("AI summarizes", selection: $toolSummaryScopeRaw) {
                        Text("Bash commands only").tag(ToolSummaryScope.bashOnly.rawValue)
                        Text("All tool calls").tag(ToolSummaryScope.all.rawValue)
                    }
                    Text("Read calls always collapse to a plain merged list (\u{201C}Read x, y, z\u{201D}) and are never AI-summarized.")
                        .settingsCaption()
                    if toolSummaryScope == .all {
                        Label {
                            Text("Summarizing every tool call runs the on-device model far more often — heavy work best suited to an M4 Pro or stronger chip. \(DeviceCapability.heavySummaryAdvice)")
                        } icon: {
                            Image(systemName: DeviceCapability.handlesHeavySummaries
                                  ? "checkmark.seal" : "exclamationmark.triangle.fill")
                        }
                        .settingsCaption()
                        .foregroundStyle(DeviceCapability.handlesHeavySummaries
                                         ? AnyShapeStyle(.secondary) : AnyShapeStyle(.orange))
                    }
                }
            }

            Section("Status") {
                LabeledContent("On-device model", value: IntelligenceAvailability.status(for: .onDevice))
                LabeledContent("Private Cloud Compute", value: IntelligenceAvailability.status(for: .privateCloudCompute))
                if PowerSource.hasBattery {
                    LabeledContent("Energy Aware", value: IntelligenceAvailability.status(for: .energyAware))
                }
            }
            .font(.callout)
        }
        .formStyle(.grouped)
        .onAppear { pushDefaults() }
        .onChange(of: summarizeToolCalls) { _, _ in pushDefaults() }
        .onChange(of: toolSummaryScopeRaw) { _, _ in pushDefaults() }
    }

    private func pushDefaults() {
        store.summarizeToolCalls = summarizeToolCalls
        store.toolSummaryScope = toolSummaryScope
    }
}

/// iPhone pairing and the LAN sync server (host side).
private struct RemoteSettingsTab: View {
    var body: some View {
        Form {
            RemoteAccessSection()
        }
        .formStyle(.grouped)
    }
}

private extension View {
    /// Shared styling for the small explanatory captions beneath settings controls.
    func settingsCaption() -> some View {
        self.font(.caption)
            .foregroundStyle(.secondary)
            .fixedSize(horizontal: false, vertical: true)
    }
}
