//
//  oAIApp.swift
//  Confab
//
//  Main app entry point
//
// 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 SwiftUI
#if os(macOS)
import AppKit

/// Intercepts Quit so an unsaved conversation gets the standard "Do you want to save?" prompt
/// (via `ChatViewModel.confirmDiscardIfNeeded`) with a real chance to cancel the quit.
final class AppDelegate: NSObject, NSApplicationDelegate {
    // `NSApplication.shared.delegate as? AppDelegate` (formerly used in ContentView.onAppear to
    // wire `chatViewModel`) ALWAYS fails: `@NSApplicationDelegateAdaptor` registers an internal
    // `SwiftUI.AppDelegate` wrapper as the real `NSApp.delegate`, which forwards protocol methods
    // (like `applicationShouldTerminate`) to this instance — but querying `NSApp.delegate` returns
    // that SwiftUI wrapper, a same-named-but-different type, not this class. Confirmed via logging
    // ("delegate is Optional(<SwiftUI.AppDelegate: ...>)"). Track our own instance directly instead.
    static var shared: AppDelegate?

    var chatViewModel: ChatViewModel?

    override init() {
        super.init()
        AppDelegate.shared = self
    }

    func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
        guard let chatViewModel, chatViewModel.hasUnsavedChanges else { return .terminateNow }
        var shouldTerminate = false
        chatViewModel.confirmDiscardIfNeeded(
            then: { shouldTerminate = true },
            onCancel: { shouldTerminate = false }
        )
        return shouldTerminate ? .terminateNow : .terminateCancel
    }

    // `chatViewModel` is wired from `ContentView.onAppear`, not from `oAIApp.init()` — reading
    // the `@State private var chatViewModel` there returns a throwaway instance distinct from
    // the one SwiftUI actually renders (confirmed via ObjectIdentifier logging: two different
    // addresses), which silently broke crash-recovery restore. `ContentView.onAppear` reads the
    // same environment-injected instance the view tree observes, so it's guaranteed correct.
}
#endif

@main
struct oAIApp: App {
    @State private var chatViewModel = ChatViewModel()
    @State private var showAbout = false
    #if os(macOS)
    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    #endif

    init() {
        // Start email handler on app launch
        EmailHandlerService.shared.start()

        // Start the local CLI server (Settings > Advanced > CLI Access) — no-op if disabled
        CLIServerService.shared.start()

        // Start external MCP servers
        Task { @MainActor in ExternalMCPManager.shared.startAll() }

        // Sync Git changes on app launch (pull + import)
        Task {
            await GitSyncService.shared.syncOnStartup()
        }

        // Reconcile starred models with the iCloud copy (cross-machine favorites sync)
        Task {
            await BackupService.shared.syncFavoritesOnLaunch()
        }

        // Check for updates in the background
        UpdateCheckService.shared.checkForUpdates()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(chatViewModel)
                .preferredColorScheme(.dark)
                .sheet(isPresented: $showAbout) {
                    AboutView()
                }
                #if os(macOS)
                .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
                    Task {
                        await BackupService.shared.syncFavoritesOnLaunch()
                    }
                }
                .onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in
                    Task { @MainActor in ExternalMCPManager.shared.stopAll() }
                    CLIServerService.shared.stop()
                }
                #endif
        }
        #if os(macOS)
        .windowStyle(.hiddenTitleBar)
        .windowToolbarStyle(.unified)
        .defaultSize(width: 1024, height: 800)
        .windowResizability(.contentMinSize)
        .commands {
            // ── Apple menu ────────────────────────────────────────────────
            CommandGroup(replacing: .appInfo) {
                Button("About Confab") { showAbout = true }
            }

            CommandGroup(replacing: .appSettings) {
                Button("Settings…") { chatViewModel.showSettings = true }
                    .keyboardShortcut(",", modifiers: .command)
            }

            // ── File menu ─────────────────────────────────────────────────
            // Replacing .newItem removes the auto-added "New Window" entry
            CommandGroup(replacing: .newItem) {
                Button("New Chat") { chatViewModel.newConversation() }
                    .keyboardShortcut("n", modifiers: .command)
                Button("Clear Chat") { chatViewModel.clearChat() }
                    .keyboardShortcut("k", modifiers: .command)
                    .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
            }

            CommandGroup(after: .newItem) {
                Button("Open Chat…") { chatViewModel.showConversations = true }
                    .keyboardShortcut("o", modifiers: .command)
                Button("Search Conversations") { chatViewModel.showConversations = true }
                    .keyboardShortcut("l", modifiers: .command)
            }

            CommandGroup(replacing: .saveItem) {
                Button("Save Chat") { chatViewModel.saveFromMenu() }
                    .keyboardShortcut("s", modifiers: .command)
                    .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
                Button("Save Chat As…") { chatViewModel.saveAsFromMenu() }
                    .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
                Button("Stats") { chatViewModel.showStats = true }
                    .keyboardShortcut("s", modifiers: [.command, .shift])
            }

            CommandGroup(after: .importExport) {
                Button("Export as Markdown…") {
                    let name = chatViewModel.currentConversationName ?? "conversation"
                    let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
                    chatViewModel.exportConversationWithSavePanel(format: "md", defaultFilename: "\(safe).md")
                }
                .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)

                Button("Export as HTML…") {
                    let name = chatViewModel.currentConversationName ?? "conversation"
                    let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
                    chatViewModel.exportConversationWithSavePanel(format: "html", defaultFilename: "\(safe).html")
                }
                .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)

                Button("Export as PDF…") {
                    let name = chatViewModel.currentConversationName ?? "conversation"
                    let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
                    chatViewModel.exportConversationWithSavePanel(format: "pdf", defaultFilename: "\(safe).pdf")
                }
                .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
            }

            // ── Chat menu ─────────────────────────────────────────────────
            // Named "Chat" (not "View") to avoid colliding with the "View" menu
            // macOS auto-adds for NavigationSplitView (Enter Full Screen, etc.) —
            // two menus both titled "View" would otherwise appear in the menu bar.
            CommandMenu("Chat") {
                Button("Select Model") { chatViewModel.showModelSelector = true }
                    .keyboardShortcut("m", modifiers: .command)

                Button("Model Info") {
                    chatViewModel.modelInfoTarget = chatViewModel.selectedModel
                }
                .keyboardShortcut("i", modifiers: .command)
                .disabled(chatViewModel.selectedModel == nil)

                Divider()

                Button("Command History") { chatViewModel.showHistory = true }
                    .keyboardShortcut("h", modifiers: [.command, .shift])

                Button("Credits") { chatViewModel.showCredits = true }

                Divider()

                Button(chatViewModel.onlineMode ? "Online Mode: On" : "Online Mode: Off") {
                    chatViewModel.onlineMode.toggle()
                }
                .keyboardShortcut("o", modifiers: [.command, .shift])
            }

            // ── Help menu ─────────────────────────────────────────────────
            CommandGroup(replacing: .help) {
                Button("Confab Help") { Self.openHelpBook() }
                    .keyboardShortcut("?", modifiers: .command)
                Divider()
                Button("Read Release Notes") {
                    let current = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
                    chatViewModel.releaseNotesRequest = ReleaseNotesRequest(versionTag: "v\(current)", isCurrentlyInstalled: true)
                }
                Button(UpdateCheckService.shared.isCheckingManually ? "Checking…" : "Check for Updates…") {
                    UpdateCheckService.shared.checkForUpdatesManually()
                }
                .disabled(UpdateCheckService.shared.isCheckingManually)
            }
        }
        #endif
    }

    #if os(macOS)
    /// Opens the Help Book's index.html directly in the default browser rather than through
    /// NSHelpManager/Help Viewer — see CLAUDE.md's macOS 27 beta note for why (Apple's Tips.app
    /// replacement for Help Viewer can't resolve anchors on this beta). Revisit once macOS 27
    /// reaches RC. No anchor/fragment support: NSWorkspace.shared.open() silently drops #fragments
    /// for file:// URLs before handing off to the browser (confirmed via location.hash coming back
    /// empty in the opened page) — deep links into a specific section aren't reliable through this
    /// API, so callers needing to point at specific content should show it in-app instead (see
    /// GitSyncManualFixSheet for an example) rather than trying to anchor into this Help Book.
    nonisolated static func openHelpBook() {
        guard let helpBookURL = Bundle.main.url(forResource: "Confab.help", withExtension: nil) else { return }
        let url = helpBookURL.appendingPathComponent("Contents/Resources/en.lproj/index.html")
        NSWorkspace.shared.open(url)
    }
    #endif
}
