#if targetEnvironment(macCatalyst)
import Combine
import Foundation
import Observation
import UIKit

@MainActor
public final class SybilMacQuickQuestionController: NSObject, SybilQuickQuestionPanelDelegate {
    public static let shared = SybilMacQuickQuestionController()

    private var pluginBundle: Bundle?
    private var bridge: (any SybilQuickQuestionPanelBridging)?
    private var viewModel: SybilViewModel?
    private var observationGeneration = 0
    private var isPresentationRequested = false
    private var activationRequested = false
    private var presentationTask: Task<Void, Never>?
    private var foregroundObservation: AnyCancellable?
    private var notificationCenter = NotificationCenter.default
    private var isApplicationInForeground: @MainActor () -> Bool = {
        UIApplication.shared.applicationState != .background
    }

    private override init() {
        super.init()
    }

    // Inject foreground readiness as well as the panel so tests can exercise
    // windowless activation without activating apps or showing real windows.
    init(
        bridge: any SybilQuickQuestionPanelBridging,
        isApplicationInForeground: @escaping @MainActor () -> Bool = { true },
        notificationCenter: NotificationCenter = NotificationCenter()
    ) {
        self.bridge = bridge
        self.isApplicationInForeground = isApplicationInForeground
        self.notificationCenter = notificationCenter
        super.init()
        _ = bridge.start(delegate: self)
        observeForegroundTransitionsIfNeeded()
    }

    public func start() {
        observeForegroundTransitionsIfNeeded()
        guard bridge == nil else { return }
        guard let url = Bundle.main.builtInPlugInsURL?.appendingPathComponent("SybilMacQuickQuestion.bundle"),
              let bundle = Bundle(url: url)
        else {
            SybilLog.error(SybilLog.app, "The macOS Quick Question bundle is missing.")
            return
        }

        do {
            try bundle.loadAndReturnError()
            guard let pluginClass = bundle.principalClass as? any SybilQuickQuestionPanelBridging.Type else {
                SybilLog.error(SybilLog.app, "The macOS Quick Question bundle has an invalid principal class.")
                return
            }
            let plugin = pluginClass.init()
            pluginBundle = bundle
            bridge = plugin
            let status = plugin.start(delegate: self)
            if status != 0 {
                SybilLog.warning(SybilLog.app, "Option+Space could not be registered (status \(status)). Quick Question remains available in the app menu.")
            }
        } catch {
            SybilLog.error(SybilLog.app, "Could not load the macOS Quick Question panel", error: error)
        }
    }

    func attach(_ viewModel: SybilViewModel) {
        guard self.viewModel !== viewModel else { return }
        // A window becoming active must not replace an in-flight quick question.
        if self.viewModel != nil,
           isPresentationRequested || bridge?.isVisible == true || self.viewModel?.isQuickQuestionSending == true || self.viewModel?.isConvertingQuickQuestion == true {
            return
        }
        self.viewModel = viewModel
        if bridge?.isVisible == true { updateAndObserve() }
    }

    public func toggle() {
        start()
        if isPresentationRequested || bridge?.isVisible == true {
            cancelPresentation()
            bridge?.hide()
        } else {
            show()
        }
    }

    public func show() {
        start()
        isPresentationRequested = true
        schedulePresentationIfNeeded()
    }

    public func stop() {
        cancelPresentation()
        foregroundObservation = nil
        viewModel?.cancelQuickQuestion()
        bridge?.stop()
        bridge = nil
        // Keep the loaded bundle alive for the lifetime of its code.
    }

    private func observeForegroundTransitionsIfNeeded() {
        guard foregroundObservation == nil else { return }
        foregroundObservation = notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification)
            .merge(with: notificationCenter.publisher(for: UIApplication.didBecomeActiveNotification))
            .sink { [weak self] _ in
                Task { @MainActor [weak self] in
                    self?.schedulePresentationIfNeeded()
                }
            }
    }

    private func schedulePresentationIfNeeded() {
        guard isPresentationRequested, bridge?.isVisible != true, presentationTask == nil else { return }
        // Leave the Carbon/NSApplication event callback before touching windows.
        // AppKit activation is asynchronous; an arbitrary delay or NSApp.isActive
        // is not a substitute for checking UIKit's foreground lifecycle state.
        presentationTask = Task { @MainActor [weak self] in
            guard let self, !Task.isCancelled else { return }
            self.presentationTask = nil
            guard self.isPresentationRequested else { return }
            guard self.isApplicationInForeground() else {
                guard !self.activationRequested else { return }
                self.activationRequested = true
                SybilLog.debug(SybilLog.ui, "Waiting for Catalyst foreground before showing Quick Question")
                self.bridge?.activateForQuickQuestion()
                // Also handle an activation that completes synchronously. If it
                // has not completed, subsequent UIKit notifications resume us.
                self.schedulePresentationIfNeeded()
                return
            }
            self.activationRequested = false
            self.updateAndObserve()
            self.bridge?.show()
        }
    }

    private func cancelPresentation() {
        isPresentationRequested = false
        activationRequested = false
        presentationTask?.cancel()
        presentationTask = nil
        observationGeneration += 1
    }

    private func updateAndObserve() {
        guard let bridge else { return }
        observationGeneration += 1
        let generation = observationGeneration
        let state = withObservationTracking {
            viewModel?.macQuickQuestionPanelState ?? SybilQuickQuestionPanelState()
        } onChange: { [weak self] in
            Task { @MainActor [weak self] in
                guard let self, self.observationGeneration == generation, self.bridge?.isVisible == true else { return }
                self.updateAndObserve()
            }
        }
        do {
            bridge.updateState(try JSONEncoder().encode(state))
        } catch {
            SybilLog.error(SybilLog.ui, "Could not update the macOS Quick Question panel", error: error)
        }
    }

    func quickQuestionToggleRequested() {
        toggle()
    }

    func quickQuestionPromptChanged(_ prompt: String) {
        guard let viewModel, !viewModel.isQuickQuestionSending, !viewModel.isConvertingQuickQuestion else { return }
        viewModel.updateQuickQuestionPrompt(prompt)
        updateAndObserve()
    }

    func quickQuestionSubmitRequested() {
        guard let viewModel, viewModel.isAuthenticated, !viewModel.isCheckingSession else { return }
        viewModel.sendQuickQuestion()
        updateAndObserve()
    }

    func quickQuestionProviderChanged(_ provider: String) {
        guard let viewModel, let provider = Provider(rawValue: provider),
              !viewModel.isQuickQuestionSending, !viewModel.isConvertingQuickQuestion else { return }
        viewModel.setQuickQuestionProvider(provider)
        updateAndObserve()
    }

    func quickQuestionModelChanged(_ model: String) {
        guard let viewModel, !viewModel.isQuickQuestionSending, !viewModel.isConvertingQuickQuestion else { return }
        viewModel.setQuickQuestionModel(model)
        updateAndObserve()
    }

    func quickQuestionConvertRequested() {
        guard let viewModel, viewModel.canConvertQuickQuestion else { return }
        Task { @MainActor [weak self] in
            guard let self else { return }
            if await viewModel.convertQuickQuestionToChat() {
                self.cancelPresentation()
                self.openMainWindow()
                self.bridge?.hide()
            } else {
                self.updateAndObserve()
            }
        }
    }

    func quickQuestionOpenAppRequested() {
        cancelPresentation()
        openMainWindow()
        bridge?.hide()
    }

    private func openMainWindow() {
        let session = UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .first?.session
        UIApplication.shared.requestSceneSessionActivation(session, userActivity: nil, options: nil)
        bridge?.activateMainWindow()
    }

    func quickQuestionPanelDismissed() {
        // Toggling out preserves the draft and lets an answer finish. Reopening
        // reads the latest state, without starting a new request.
        cancelPresentation()
    }
}

extension SybilViewModel {
    var macQuickQuestionPanelState: SybilQuickQuestionPanelState {
        SybilQuickQuestionPanelState(
            prompt: quickQuestionPrompt,
            answer: quickQuestionAnswerText,
            toolSummaries: quickQuestionMessages.compactMap { message in
                guard let metadata = message.toolCallMetadata else { return nil }
                return metadata.summary ?? message.content
            },
            provider: quickQuestionProvider.rawValue,
            providers: providerOptions.map { .init(id: $0.rawValue, title: $0.displayName) },
            model: quickQuestionModel,
            models: quickQuestionProviderModelOptions,
            isSending: isQuickQuestionSending,
            isConverting: isConvertingQuickQuestion,
            canSend: isAuthenticated && !isCheckingSession && canSendQuickQuestion,
            canConvert: canConvertQuickQuestion,
            isAuthenticated: isAuthenticated,
            isCheckingSession: isCheckingSession,
            error: quickQuestionError
        )
    }
}
#endif
