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 --build-system native 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 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")
        }

        // 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 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.
            let containerManager = ContainerManager()
            // 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()
            // 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.
            let approvalServerRegistry = ApprovalServerRegistry()
            let store = AppStore(
                database: database, worktrees: worktrees, transcriptsDir: transcriptsDir,
                containerManager: containerManager, 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,
                        conflictCoordinator: conflictCoordinator,
                        approvalServerRegistry: approvalServerRegistry)
                case .grok:
                    // Grok over ACP (`grok agent stdio`): 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). The conflict
                    // coordinator joins grok sessions to the lock system + autoship; on the vsock
                    // control-plane path it execs in its control container with the interceptor wired.
                    return GrokACPBackend(
                        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) }
            store.defaultModel = ModelCatalog.storedDefaultModel
            store.defaultEffort = ModelCatalog.storedDefaultEffort
            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)
                .frame(minWidth: 900, minHeight: 560)
                .task {
                    await store.activateConflictArbitration()
                    await store.loadProjects()
                    await store.loadSessions()
                    await store.loadTodos()
                    await reconcileDefaultModel()
                    store.startQuotaPolling()
                    store.startStatusPolling()
                    store.startAutoArchivePolling()
                    // Anonymous, opt-out, once-a-day DAI heartbeat (docs/CLOUD_INFRA.md §4).
                    Task { await HeartbeatReporter.reportIfDue() }
                }
        }
        .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 goes to the home dashboard — the surface for starting a new
                // chat (its chat bar picks the project, model, and effort).
                Button("New Chat") {
                    store.goHome()
                }
                .keyboardShortcut("n", modifiers: .command)
                // 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
    }
}
