import SwiftUI
import NucleicCore

/// The macOS cockpit (PLAN "macOS UI"). A thin SwiftUI shell over `AppStore` — it
/// holds no canonical state, just observes the store and sends intents (RUNTIME §3).
///
/// Runs as a SwiftPM executable; launch with
/// `swift run NucleicApp`. The store persists under
/// `~/Library/Application Support/Nucleic`.
@main
struct NucleicApp: App {
    @State private var store: AppStore
    // In-app auto-update for direct (non-App-Store) distribution. Dormant unless the build
    // embeds an appcast feed (packaged canary/beta/rc/stable); see AppUpdater.
    @StateObject private var updater = AppUpdater()

    init() {
        // A GUI-launched app inherits the launchd environment, which here carries a *blank*
        // ANTHROPIC_API_KEY. Claude Code prefers ANTHROPIC_API_KEY over the OAuth login, so every
        // claude session — host-spawned (which inherits this whole environment) and sandboxed
        // alike — would attempt empty API-key auth and 401. Purge a blank key before anything
        // spawns; a real key is left intact for users who actually authenticate that way.
        if SessionController.isBlankAPIKey(ProcessInfo.processInfo.environment["ANTHROPIC_API_KEY"]) {
            unsetenv("ANTHROPIC_API_KEY")
        }
        // Same hazard for Grok Build, which prefers XAI_API_KEY over the `grok` login: a blank
        // key inherited from the launchd environment would make every grok session attempt
        // empty-key auth and 401. Purge a blank key; a real one is left intact (GROK_ADAPTER §4).
        if SessionController.isBlankAPIKey(ProcessInfo.processInfo.environment["XAI_API_KEY"]) {
            unsetenv("XAI_API_KEY")
        }
        // Same for the other ACP wrapper agents that prefer an API-key env var over their CLI
        // login (OpenCode/Cursor); purge any blank inherited key so a login isn't shadowed.
        for key in ["OPENCODE_API_KEY", "CURSOR_API_KEY", "HERMES_API_KEY"] {
            if SessionController.isBlankAPIKey(ProcessInfo.processInfo.environment[key]) {
                unsetenv(key)
            }
        }

        // Show a Dock icon + foreground window when launched from the terminal.
        NSApplication.shared.setActivationPolicy(.regular)

        let support = Self.supportDirectory()
        do {
            let database = try GRDBMetadataStore(
                path: support.appendingPathComponent("nucleic.sqlite").path)
            let worktrees = GitWorktreeManager()
            let transcriptsDir = support.appendingPathComponent("sessions", isDirectory: true)
            // Shared approval-server registry: sessions that share a Nucleic Control container share
            // one token-multiplexed approval server (so they share that container's single relayed
            // control socket). Handed to every Claude backend; host/per-session runs ignore it.
            // Created before the container manager so the latter can release a container's server the
            // moment that container is permanently removed.
            let approvalServerRegistry = ApprovalServerRegistry()
            // Shared sandbox orchestrator: opt-in per project, dormant unless a session's
            // project enables it. The same instance is handed to every backend so container
            // lifecycle (idle stop, teardown, reconcile) stays consistent. When a shared control
            // container is permanently removed, drop its approval server + control socket so the next
            // session in a freshly-created container binds a clean one.
            let containerManager = ContainerManager(
                onSharedContainerRemoved: { name in
                    await approvalServerRegistry.teardown(containerName: name)
                })
            // macOS-guest VM orchestrator (Apple `Virtualization`): one shared instance backs both the
            // `mac_vm_exec` tool (per-session isolated Macs for Xcode/simulator work) and the Virtual
            // Machines settings panel. Dormant unless the macOS-VM service is enabled (docs/MACOS_VM.md).
            let macVMManager = MacVMManager()
            // Shared conflict coordinator: every backend consults it from the `check_conflict`
            // MCP tool; the store registers itself as its arbiter at startup (see `.task`).
            let conflictCoordinator = ConflictCoordinator()
            let store = AppStore(
                database: database, worktrees: worktrees, transcriptsDir: transcriptsDir,
                containerManager: containerManager, macVMManager: macVMManager,
                conflictCoordinator: conflictCoordinator
            ) { session in
                switch session.backend {
                case .codex:
                    // Codex app-server backend: interactive approvals over JSON-RPC (ADAPTERS §2).
                    // Joins the lock/arbitration system via the conflict coordinator (file-change
                    // approvals acquire locks; the interceptor releases on merge); execs in its
                    // control container on the vsock control-plane path. (host_exec stays Claude-only.)
                    return CodexAppServerBackend(
                        containerManager: containerManager,
                        conflictCoordinator: conflictCoordinator,
                        approvalServerRegistry: approvalServerRegistry)
                case .codexExec:
                    // Unattended Codex (codex exec --json), fail-closed (ADAPTERS §3). Execs in its
                    // control container on the vsock control-plane path; `--ask-for-approval never`
                    // means no acquire seam, so it joins the lock system on observe/release only.
                    return CodexExecBackend(
                        containerManager: containerManager,
                        conflictCoordinator: conflictCoordinator,
                        approvalServerRegistry: approvalServerRegistry)
                case .claudeCode:
                    // Each chat turn is a single-shot run (stdin closed), with follow-ups
                    // resumed via --resume. This is the only reliable way to use the
                    // approval server: Claude's streaming-input mode hangs alongside
                    // --permission-prompt-tool. Approval server is authoritative
                    // (--permission-mode default); child is hermetic (--strict-mcp-config).
                    return ClaudeCodeBackend(
                        configuration: .init(closeStdinAfterPrompt: true),
                        containerManager: containerManager,
                        macVMManager: macVMManager,
                        conflictCoordinator: conflictCoordinator,
                        approvalServerRegistry: approvalServerRegistry)
                case .grok, .opencode, .openclaw, .hermes, .cursorAgent, .acp:
                    // ACP agents (`grok agent stdio`, `opencode acp`, `openclaw acp`, `hermes acp`,
                    // `cursor-agent acp`): standards JSON-RPC over stdio with a native
                    // `session/request_permission` approval request, suspending on the same
                    // ApprovalCoordinator as Claude/Codex (GROK_ADAPTER §3–4, ADAPTERS §5). One
                    // generic `ACPBackend` drives them all — only the per-agent profile differs.
                    // The conflict coordinator joins these sessions to the lock system + autoship;
                    // on the vsock control-plane path they exec in their control container with the
                    // interceptor wired. A non-ACP backend here is unreachable (compiler-checked).
                    let agent = ACPAgent.forBackend(session.backend) ?? .grok
                    return ACPBackend(
                        configuration: .init(agent: agent),
                        conflictCoordinator: conflictCoordinator,
                        containerManager: containerManager,
                        approvalServerRegistry: approvalServerRegistry)
                }
            }
            store.intelligence = AppleIntelligenceProvider()
            // Play a macOS system sound when a chat finishes (Funk) or starts needing the
            // user (Pop). The handler honors the per-cue toggles in Settings ▸ Chats; the
            // store fires only on live transitions, so launch/resume stay silent.
            store.chatStatusSoundHook = { ChatStatusSounds.play($0) }
            // Drive the escalating alarm: the store flips this true while a chat sits blocked
            // and unviewed, false once the user opens it (or it resolves). `ChatAlarm` honors
            // the off-by-default Settings switch and owns the repeating "Submarine" pulse.
            store.chatAlarmHook = { ChatAlarm.shared.setActive($0) }
            // Bounce the Dock icon once a chat has needed the user for a full minute, calling
            // them back until they foreground the app. Same blocked-and-unviewed edge as the
            // alarm; `DockBounce` owns the one-minute delay and the on-by-default switch.
            store.chatDockBounceHook = { DockBounce.shared.setPending($0) }
            store.defaultModel = ModelCatalog.storedDefaultModel
            store.defaultEffort = ModelCatalog.storedDefaultEffort
            store.defaultSupervisorModel = ModelCatalog.storedDefaultSupervisorModel
            store.defaultOrchestraWorkerModel = ModelCatalog.storedDefaultOrchestraWorkerModel
            store.defaultOrchestraMaxConcurrentWorkers = ModelCatalog.storedDefaultOrchestraMaxWorkers
            store.defaultAuto = ModelCatalog.storedDefaultAuto
            store.defaultAutoShip = ModelCatalog.storedDefaultAutoShip
            store.summarizeToolCalls = TranscriptDisplay.storedSummarizeToolCalls
            store.toolSummaryScope = TranscriptDisplay.storedToolSummaryScope
            store.autoArchiveIdleInterval = AutoArchivePolicy.stored.interval
            store.archivedWorktreeCleanupInterval = ArchivedWorktreeCleanupPolicy.stored.interval
            store.setEnabledStatusProviders(StatusIndicatorSettings.storedEnabledProviders)
            // Hand the sync host the model/effort catalog so paired phones render the same
            // pickers (SYNC §5.2). Static data, injected once since ModelCatalog lives here.
            store.setSyncModelCatalog(ModelCatalog.wireCatalog)
            _store = State(initialValue: store)
        } catch {
            fatalError("Could not open the Nucleic store at \(support.path): \(error)")
        }
    }

    /// Settle the new-chat default on a model the user can actually run. `init` seeds the stored
    /// choice (else Opus) synchronously so the picker shows instantly; once the async provider
    /// probes resolve, if that seed's backend isn't connected (installed + authenticated) we swap
    /// to the highest-priority *connected* provider's default — Claude → Codex → Grok — and persist
    /// it so Settings and new chats stay in step. (Status-driven failover still layers on top later.)
    @MainActor
    private func reconcileDefaultModel() async {
        let connected = Set(
            await ProviderAvailability.probeAll()
                .filter(\.isConnected)
                .map(\.backend))
        let resolved = ModelCatalog.resolvedDefault(connected: connected)
        guard resolved.model != store.defaultModel || resolved.effort != store.defaultEffort
        else { return }
        store.defaultModel = resolved.model
        store.defaultEffort = resolved.effort
        UserDefaults.standard.set(resolved.model, forKey: ModelCatalog.defaultModelKey)
        UserDefaults.standard.set(resolved.effort, forKey: ModelCatalog.defaultEffortKey)
    }

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(store)
                .environmentObject(updater)
                // Keep this floor low enough that the window can always satisfy its layout on a
                // narrow or transiently low-res display (e.g. a macOS VM that presents a ~1024×768
                // fallback framebuffer before negotiating its full mode). Above ~900 the split
                // view can't fit sidebar + detail at their minimums, so it overlaps them and the
                // composer's text wraps a character per line — the garbled render seen in VMs.
                // Below this floor the existing responsive machinery (the sidebar collapses; the
                // home dashboard stacks its cards past its 600pt threshold) reflows cleanly, so a
                // squeezed window degrades gracefully instead of breaking.
                .frame(minWidth: 720, minHeight: 480)
                .task {
                    // Re-arm the sleep-blocking assertion if the user last left "Block sleep" on
                    // (Settings ▸ General ▸ Power). The assertion is in-memory only, so a relaunch
                    // otherwise drops it silently.
                    SleepBlocker.shared.applyStoredSetting()
                    // Wire the host-side computer-use surface (VZVirtualMachineView framebuffer capture
                    // + HID) so macOS-VM computer use runs SIP-free through the guest's virtual display
                    // (docs/MACOS_VM.md §12). Lives in the app layer because it needs AppKit.
                    #if arch(arm64)
                    await store.setMacVMSurfaceHost(MacVMComputerSurface())
                    #endif
                    await store.activateConflictArbitration()
                    // loadProjects() also reconciles the container-service lock: it pins the
                    // service on when a Control project exists and, on a fresh install, adopts any
                    // leftover controlled repos under ~/.nucleic/control/.
                    await store.loadProjects()
                    await store.loadSessions()
                    // Reclaim ephemeral scratch dirs left orphaned by a crash / unclean close — after
                    // loadSessions has reconstructed every persisted scratch chat into a controller.
                    store.sweepOrphanedScratchDirs()
                    // Reconcile any transfer a prior crash/quit left mid-flight (mesh P5): clear
                    // abandoned locks now; a tombstoned source's commit finishes when its peer
                    // reconnects (via `meshPeersChanged`).
                    await store.recoverInterruptedTransfers()
                    await store.loadTodos()
                    await reconcileDefaultModel()
                    store.startQuotaPolling()
                    store.startStatusPolling()
                    store.startAutoArchivePolling()
                    // Restore remote access if the user last had it on. An update relaunches the
                    // process, and the sync server's running state is otherwise in-memory only, so
                    // without this the toggle comes back off and paired devices never reconnect. Off
                    // the startup path — a tailnet start can block on a browser login for minutes.
                    Task { await store.resumeRemoteAccessIfEnabled() }
                    // Anonymous, opt-out, once-a-day DAI heartbeat (docs/CLOUD_INFRA.md §4).
                    Task { await HeartbeatReporter.reportIfDue() }
                    // If an app update changed a VM base's baked-in recipe (provisioning script,
                    // in-guest agent, default packages), the on-disk base is stale — force a
                    // re-provision so the updated recipe takes effect. Off the launch path (it re-runs
                    // provisioning when it fires) and after the surface host is wired at line above, so
                    // the macOS re-provision can drive the guest. No-op when every base is current.
                    Task { await store.reprovisionStaleMacVMBasesIfNeeded() }
                }
        }
        .windowToolbarStyle(.unified)
        .commands {
            // "Check for Updates…" lives in Settings ▸ General now (see GeneralSettingsTab), not
            // the app menu — background updates surface as an unobtrusive sidebar banner instead.
            CommandGroup(replacing: .newItem) {
                // Cmd-N floats the quick-launch composer and, on start, leaves you on your
                // current view — the new chat runs in the background.
                Button("New Chat in Background") {
                    store.presentNewChatComposer(background: true)
                }
                .keyboardShortcut("n", modifiers: .command)
                // Cmd-Shift-N floats the same composer but jumps to the new session once it starts.
                Button("New Chat…") {
                    store.presentNewChatComposer(background: false)
                }
                .keyboardShortcut("n", modifiers: [.command, .shift])
                // Opt-Cmd-N opens the composer straight in ephemeral (Scratch) chat mode — a
                // throwaway, out-of-project chat that runs in its own popup window.
                Button("New Scratch Chat") {
                    store.presentNewChatComposer(background: true, ephemeral: true)
                }
                .keyboardShortcut("n", modifiers: [.command, .option])
                // Cmd-T captures a quick idea into the home to-do inbox from anywhere.
                Button("New To-Do") {
                    store.presentQuickTodo()
                }
                .keyboardShortcut("t", modifiers: .command)
            }
        }
        Settings {
            SettingsView().environment(store).environmentObject(updater)
        }
    }

    private static func supportDirectory() -> URL {
        let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
        let dir = base.appendingPathComponent("Nucleic", isDirectory: true)
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        return dir
    }
}
