import Cocoa
import AVFoundation
import Speech
import SwiftUI
import DynamicNotchKit
import UserNotifications

/// 应用主控制器，协调 Fn 键（长按录音）和 Control 键（双击切换模式）
final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDelegate {

    /// 防止 ARC 优化释放（NSApplication.delegate 是 weak）
    private static var retainedInstance: AppDelegate?

    private let keyMonitor = KeyMonitor()
    private let speechRecognizer = SpeechRecognizer()
    private let audioMonitor = AudioLevelMonitor()
    private let recordingPanel = RecordingPanel()
    private let menuBarController = MenuBarController()
    private let llmRefiner = LLMRefiner()
    private let speechSynthesizer = NSSpeechSynthesizer()
    /// 当前正在朗读的模式，用于朗读完成后关闭对应弹窗
    private var speakingMode: LLMMode? = nil
    /// 朗读完成后的延迟关闭任务，重新输入时取消
    private var speakAutoHideWorkItem: DispatchWorkItem? = nil
    private let agentBridge: AgentBridge
    private let agentTracker = AgentProgressTracker()
    private var agentNotch: DynamicNotch<AgentNotchView>?

    private var isRecording = false
    private var finalTranscription: String = ""
    private var isTextInputActive = false
    private var agentActive = false
    private var agentIsTextMode = false
    private var agentExitingCleanly = false  // 主动关闭标记，避免误报退出错误
    private var autoHideWorkItem: DispatchWorkItem?
    private var reminderTimer: Timer?
    private var triggeredReminderIds = Set<String>()  // 防止重复弹窗

    override init() {
        agentBridge = AgentBridge(agentDir: Config.shared.agentDir)
        super.init()
        Self.retainedInstance = self
        speechSynthesizer.delegate = self
    }

    func applicationDidFinishLaunching(_ notification: Notification) {
        NSApp.setActivationPolicy(.accessory)
        startReminderTimer()
        requestNotificationPermission()
        beginPermissionFlow()
        print("[App] Ready")
    }

    // MARK: - Permissions

    private var permissionGuideWindow: PermissionGuideWindow?

    private func beginPermissionFlow() {
        // 辅助功能权限已就绪 → 直接启动（输入监控可选，KeyMonitor 有回退）
        if PermissionGuideWindow.checkAccessibility() {
            startKeyMonitor()
            return
        }

        // 弹出自定义权限引导窗口（内含轮询）
        let window = PermissionGuideWindow {
            print("[Permission] ✅ All permissions granted")
            self.startKeyMonitor()
        }
        permissionGuideWindow = window
        window.makeKeyAndOrderFront(nil)
    }

    private func showMicrophonePermissionAlert() {
        NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")!)
    }

    private func showSpeechPermissionAlert() {
        NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition")!)
    }

    // MARK: - Key Monitor

    private func startKeyMonitor() {
        guard keyMonitor.start() else { print("[App] Failed to start key monitor"); return }
        keyMonitor.onFnDown = { [weak self] in self?.startRecording() }
        keyMonitor.onFnUp = { [weak self] in self?.stopRecording() }
        keyMonitor.onControlDoubleClick = { [weak self] in self?.toggleModeViaDoubleClick() }
    }

    private func toggleModeViaDoubleClick() {
        let newMode: TriggerMode
        switch Config.shared.triggerMode {
        case .input:  newMode = .assist
        case .assist: newMode = .agent
        case .agent:  newMode = .input
        }
        Config.shared.triggerMode = newMode
        menuBarController.refreshModeMenu()
        print("[App] Control double-click: mode switched to \(newMode)")
    }

    // MARK: - Recording

    private func startRecording() {
        guard !isRecording else { return }

        // 正在朗读 → 停止
        if speechSynthesizer.isSpeaking {
            speechSynthesizer.stopSpeaking()
            speakingMode = nil
            speakAutoHideWorkItem?.cancel()
        }

        // 如果当前模式不是智能体但刘海弹窗还在 → 关闭
        if agentNotch != nil && Config.shared.triggerMode != .agent {
            hideAgentNotch()
        }

        // 智能体模式 → 统一显示刘海弹窗
        if Config.shared.triggerMode == .agent {
            // done 后再次按 Fn：不清空内容，只显示输入框
            if agentTracker.mode == "done" {
                agentTracker.isTextInputMode = true
                showAgentNotchInternal(isTextInput: true)
            } else {
                agentTracker.resetForInput()
                agentTracker.isTextInputMode = true
                agentTracker.toolsMode = Config.shared.agentToolsMode
                agentTracker.newConversationMode = Config.shared.agentNewConversationMode
                showAgentNotchInternal(isTextInput: true)
            }

            // 打字输入：不录音，等待用户输入
            if Config.shared.agentInputMode == "text" {
                isTextInputActive = true
                recordingPanel.updateText("Type your question...")
                recordingPanel.showAnimated()
                return
            }
            // 语音输入：继续录音流程
        }

        isRecording = true
        finalTranscription = ""

        let micStatus = AVCaptureDevice.authorizationStatus(for: .audio)
        if micStatus == .denied || micStatus == .restricted { showMicrophonePermissionAlert(); isRecording = false; return }

        let speechStatus = SFSpeechRecognizer.authorizationStatus()
        if speechStatus == .denied || speechStatus == .restricted { showSpeechPermissionAlert(); isRecording = false; return }
        if speechStatus == .notDetermined {
            SFSpeechRecognizer.requestAuthorization { [weak self] status in
                DispatchQueue.main.async {
                    self?.isRecording = false
                    guard status == .authorized else { self?.showSpeechPermissionAlert(); return }
                    self?.startRecording()
                }
            }
            return
        }

        recordingPanel.showAnimated()
        let promptText: String = {
            switch Config.shared.triggerMode {
            case .input:  return "Listening..."
            case .assist: return "Ask AI..."
            case .agent:  return "Ask Agent..."
            }
        }()
        recordingPanel.updateText(promptText)

        let language = Config.shared.language.rawValue
        Task { @MainActor in
            let audioOK = audioMonitor.start()
            if audioOK {
                audioMonitor.onLevelUpdate = { [weak self] level in
                    self?.recordingPanel.waveformView.updateLevel(level)
                }
            }
            let speechOK = await speechRecognizer.start(language: language)
            if !speechOK {
                recordingPanel.updateText("Recognition error")
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
                    self?.cancelRecording()
                }
                return
            }
            speechRecognizer.onResult = { [weak self] text in
                guard let self, isRecording else { return }
                finalTranscription = text
                recordingPanel.updateText(text)
            }
            speechRecognizer.onError = { error in
                let ns = error as NSError
                if ns.domain == "kAFAssistantErrorDomain", ns.code == 1110 { return }
                print("[App] Speech error: \(error)")
            }
        }
    }

    private func stopRecording() {
        // 打字模式：仅隐藏录音胶囊，刘海保持打开等待输入
        if isTextInputActive {
            isTextInputActive = false
            recordingPanel.hideAnimated()
            return
        }

        guard isRecording else { return }
        isRecording = false
        audioMonitor.stop()
        _ = speechRecognizer.stop()

        let text = finalTranscription.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !text.isEmpty else { recordingPanel.hideAnimated(); finalTranscription = ""; return }

        switch Config.shared.triggerMode {
        case .input:  handleInputMode(text: text, config: Config.shared.inputLLM)
        case .assist: handleAssistMode(text: text, config: Config.shared.assistLLM)
        case .agent:
            if Config.shared.agentVoiceAutoSend {
                handleTextInput(text)
            } else {
                recordingPanel.hideAnimated()
                agentTracker.voiceInputText = text
            }
        }
        finalTranscription = ""
    }

    private func cancelRecording() {
        audioMonitor.stop()
        speechRecognizer.cancel()
        recordingPanel.hideAnimated()
        isRecording = false
        finalTranscription = ""
    }

    // MARK: - Input Mode

    private func handleInputMode(text: String, config: LLMConfig) {
        guard config.enabled, config.isConfigured else {
            recordingPanel.hideAnimated { TextInjector.inject(text) }
            return
        }
        recordingPanel.showRefining()
        Task { @MainActor in
            do {
                let result = try await llmRefiner.refine(text: text, config: config)
                recordingPanel.hideAnimated { TextInjector.inject(result.text) }
            } catch {
                print("[App] LLM refine error: \(error)")
                recordingPanel.hideAnimated { TextInjector.inject(text) }
            }
        }
    }

    // MARK: - Assist Mode

    private func handleAssistMode(text: String, config: LLMConfig) {
        guard config.enabled, config.isConfigured else {
            recordingPanel.updateText("AI 助手未配置")
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in self?.recordingPanel.hideAnimated() }
            return
        }
        recordingPanel.showThinking()
        Task { @MainActor in
            do {
                let result = try await llmRefiner.chat(text: text, config: config)
                if Config.shared.assistOutputMode == .notch {
                    recordingPanel.hideAnimated()
                    if Config.shared.assistSpeakEnabled {
                        NotchDisplayManager.shared.show(text: result.text, autoHideAfter: nil)
                        speakingMode = .assist
                        speechSynthesizer.startSpeaking(result.text)
                    } else {
                        NotchDisplayManager.shared.show(text: result.text)
                    }
                } else {
                    recordingPanel.hideAnimated { TextInjector.inject(result.text) }
                }
            } catch {
                print("[App] LLM chat error: \(error)")
                recordingPanel.updateText("请求失败，请检查配置")
                DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in self?.recordingPanel.hideAnimated() }
            }
        }
    }

    // MARK: - Agent Mode

    private func handleAgentMode(text: String) {
        speakAutoHideWorkItem?.cancel()
        speechSynthesizer.stopSpeaking()
        speakingMode = nil
        let modelsFile = Config.shared.loadAgentModels()
        let validModels = modelsFile.models.filter { !$0.url.isEmpty && !$0.name.isEmpty && !$0.apikey.isEmpty && !$0.modes.isEmpty }
        let defaultModel = modelsFile.defaultModel.isEmpty ? validModels.first?.name : modelsFile.defaultModel
        guard !validModels.isEmpty, defaultModel != nil else {
            recordingPanel.updateText("智能体未配置")
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in self?.recordingPanel.hideAnimated() }
            return
        }
        recordingPanel.hideAnimated()

        // 新建对话（自动超限 或 手动点击）
        let needsNewConversation = agentTracker.autoNewTriggered || agentTracker.manualNewRequested
        if needsNewConversation {
            agentBridge.stop()
            agentActive = false
            agentTracker.autoNewTriggered = false
            agentTracker.manualNewRequested = false
        }

        // 如果对话已在进行中 → 追加消息
        if agentActive {
            autoHideWorkItem?.cancel()
            agentTracker.start()
            showAgentNotchInternal(isTextInput: agentIsTextMode)
            agentBridge.sendContinue(message: text)
            return
        }

        agentActive = true
        agentIsTextMode = Config.shared.agentInputMode == "text"
        agentTracker.start()
        showAgentNotchInternal(isTextInput: agentIsTextMode)

        agentBridge.onEvent = { [weak self] event in
            DispatchQueue.main.async { self?.handleAgentEvent(event) }
        }
        agentBridge.onExit = { [weak self] code in
            DispatchQueue.main.async {
                guard let self = self else { return }
                self.agentLog("[App] onExit code=\(code) currentMode=\(self.agentTracker.mode)")
                self.agentActive = false
                self.agentIsTextMode = false
                if !self.agentExitingCleanly && code != 0 && self.agentTracker.mode != "done" {
                    self.agentTracker.fail("进程异常退出 (code \(code))")
                }
                self.agentExitingCleanly = false
            }
        }
        guard agentBridge.start() else {
            agentActive = false
            agentTracker.fail("无法启动智能体进程")
            return
        }
        agentBridge.send(message: text, workspace: Config.shared.agentWorkspace,
                         allowMode: Config.shared.agentAllowMode,
                         modelKey: defaultModel!,
                         newConversation: needsNewConversation)
    }

    private func hideAgentNotch() {
        agentNotch?.hide(ignoreMouse: true)
        agentNotch = nil
    }

    private func handleAgentEvent(_ event: AgentEvent) {
        switch event {
        case .status: break  // no-op，内容切换由 toolPhaseActive 控制
        case .toolStart(let tool, let display):
            agentLog("[App] handleEvent toolStart tool=\(tool)")
            agentTracker.addTool(tool, display: display)
        case .toolEnd(let tool, let resultLines):
            agentLog("[App] handleEvent toolEnd tool=\(tool) lines=\(resultLines.count)")
            agentTracker.updateToolResult(tool, resultLines: resultLines)
        case .assistantChunk(let content):
            agentTracker.appendChunk(content)
        case .done(let content, _, let totalTokens):
            agentLog("[App] handleEvent done contentLen=\(content.count) totalTokens=\(totalTokens)")
            agentTracker.finish(content: content, totalTokens: totalTokens)
            let willSpeak = Config.shared.agentSpeakEnabled && !content.isEmpty
            if willSpeak {
                speakingMode = .agent
                speechSynthesizer.startSpeaking(content)
            }
            autoHideWorkItem?.cancel()
            // 打字模式下，输入栏有内容时不自动收回；朗读模式下由朗读完成回调处理
            if !agentIsTextMode && !willSpeak {
                let workItem = DispatchWorkItem { [weak self] in self?.hideAgentNotch() }
                autoHideWorkItem = workItem
                DispatchQueue.main.asyncAfter(deadline: .now() + 20, execute: workItem)
            }
        case .error(let message):
            agentLog("[App] handleEvent error message=\(message)")
            agentTracker.fail(message)
        case .reminderTriggered(let memoryId, let content):
            agentLog("[App] handleEvent reminderTriggered id=\(memoryId) content=\(content)")
            // bridge 快速通道已注入消息到对话，无需额外处理
        }
    }

    /// 日志根目录（~/Library/Logs/VoiceInput/）
    private var logDir: String {
        let lib = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
        let dir = lib.appendingPathComponent("Logs/VoiceInput").path
        if !FileManager.default.fileExists(atPath: dir) {
            try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
        }
        return dir
    }

    private func agentLog(_ msg: String) {
        let logPath = logDir + "/agent_app.log"
        let ts = ISO8601DateFormatter().string(from: Date())
        let line = "[\(ts)] \(msg)\n"
        if let handle = FileHandle(forWritingAtPath: logPath) {
            handle.seekToEndOfFile()
            handle.write(line.data(using: .utf8)!)
            handle.closeFile()
        }
    }

    private func showAgentNotch() {
        showAgentNotchInternal(isTextInput: false)
    }

    private func showTextInputNotch() {
        // 重置显示状态，但不进入 thinking（等用户发送后再开始）
        agentTracker.resetForInput()
        agentIsTextMode = true
        showAgentNotchInternal(isTextInput: true)
    }

    private func showAgentNotchInternal(isTextInput: Bool) {
        // 弹窗已存在 → 复用，只刷新显示（不覆盖已有状态）
        if agentNotch != nil {
            agentNotch?.show()
            return
        }

        agentTracker.isTextInputMode = isTextInput
        agentTracker.toolsMode = Config.shared.agentToolsMode
        agentTracker.newConversationMode = Config.shared.agentNewConversationMode

        // 创建新弹窗
        let view = AgentNotchView(
            tracker: agentTracker,
            onCopy: {
                NSPasteboard.general.clearContents()
                NSPasteboard.general.setString(self.agentTracker.finalContent, forType: .string)
            },
            onStop: { [weak self] in
                self?.speakingMode = nil
                self?.speakAutoHideWorkItem?.cancel()
                self?.speechSynthesizer.stopSpeaking()
                self?.autoHideWorkItem?.cancel()
                self?.agentExitingCleanly = true
                self?.agentBridge.stop()
                self?.hideAgentNotch()
            },
            onClose: { [weak self] in
                self?.speakingMode = nil
                self?.speakAutoHideWorkItem?.cancel()
                self?.speechSynthesizer.stopSpeaking()
                self?.autoHideWorkItem?.cancel()
                self?.hideAgentNotch()
            },
            onNewConversation: { [weak self] in
                self?.speakingMode = nil
                self?.speakAutoHideWorkItem?.cancel()
                self?.speechSynthesizer.stopSpeaking()
                self?.autoHideWorkItem?.cancel()
                self?.agentExitingCleanly = true
                self?.agentBridge.stop()
                self?.agentActive = false
                self?.agentIsTextMode = false
                self?.agentTracker.resetForInput()
                self?.agentTracker.manualNewRequested = true
            },
            onSendText: { [weak self] text in
                self?.handleTextInput(text)
            },
            onContentSizeChanged: { [weak self] in
                self?.agentNotch?.notifyContentChanged()
            }
        )
        agentNotch = DynamicNotch(style: .notch) { view }
        agentNotch?.show()
        // 兜底：强制窗口置前（accessory 应用可能需要）
        if let window = agentNotch?.windowController?.window {
            window.orderFrontRegardless()
            window.makeKey()
            print("[Notch] 强制 orderFrontRegardless makeKey=\(window.isKeyWindow)")
        }
        print("[Notch] show() 完成 window=\(agentNotch?.windowController?.window != nil ? "存在" : "nil")")
    }

    private func handleTextInput(_ text: String) {
        handleAgentMode(text: text)
    }

    // MARK: - Reminders

    /// 调试日志：写入项目根目录 agent_debug.log
    private func remLog(_ msg: String) {
        let ts = ISO8601DateFormatter().string(from: Date())
        let line = "[\(ts)] \(msg)\n"
        print(line, terminator: "")
        // 写入文件
        let logPath = (Config.shared.agentWorkspace as NSString).appendingPathComponent("agent_debug.log")
        if let data = line.data(using: .utf8) {
            if FileManager.default.fileExists(atPath: logPath) {
                if let fh = try? FileHandle(forWritingTo: URL(fileURLWithPath: logPath)) {
                    fh.seekToEndOfFile()
                    fh.write(data)
                    fh.closeFile()
                }
            } else {
                try? data.write(to: URL(fileURLWithPath: logPath), options: .atomic)
            }
        }
    }

    private func startReminderTimer() {
        remLog("⏱ startReminderTimer 启动 10s 定时器")
        reminderTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in
            self?.checkReminders()
        }
        // 启动后延迟 3 秒首次检查（等 bridge 就绪）
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
            self?.remLog("⏱ 首次 checkReminders（启动后 3s）")
            self?.checkReminders()
        }
    }

    private func loadMemories() -> [[String: Any]] {
        let path = Config.shared.memoryPath
        guard FileManager.default.fileExists(atPath: path),
              let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
              let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
            return []
        }
        return json
    }

    private func saveMemories(_ memories: [[String: Any]]) {
        let path = Config.shared.memoryPath
        let dir = (path as NSString).deletingLastPathComponent
        try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
        guard let data = try? JSONSerialization.data(withJSONObject: memories, options: .prettyPrinted) else { return }
        try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
    }

    private func markTodoCompleted(_ memoryId: String) {
        remLog("✅ markTodoCompleted id=\(memoryId)")
        var memories = loadMemories()
        guard let idx = memories.firstIndex(where: { ($0["id"] as? String) == memoryId }) else {
            remLog("⚠️ markTodoCompleted 未找到 id=\(memoryId)")
            return
        }
        let nowISO = ISO8601DateFormatter().string(from: Date())
        memories[idx]["completed"] = true
        memories[idx]["completed_at"] = nowISO
        memories[idx].removeValue(forKey: "remind_at")
        memories[idx].removeValue(forKey: "reminded")
        saveMemories(memories)
        remLog("✅ markTodoCompleted 完成 id=\(memoryId)")
    }

    private func checkReminders() {
        let memories = loadMemories()
        let now = Date()
        remLog("🔍 checkReminders 开始 memoryCount=\(memories.count) triggeredSetSize=\(triggeredReminderIds.count)")

        // 支持三种格式：带毫秒Z、带Z、不带时区（本地时间）
        let isoFormatterZ = ISO8601DateFormatter()
        isoFormatterZ.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        let isoFormatterZNoMs = ISO8601DateFormatter()
        isoFormatterZNoMs.formatOptions = [.withInternetDateTime]
        let localFormatter: DateFormatter = {
            let f = DateFormatter()
            f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
            f.timeZone = TimeZone.current
            f.locale = Locale(identifier: "en_US_POSIX")
            return f
        }()

        var foundDue = false
        for (i, mem) in memories.enumerated() {
            guard let type = mem["type"] as? String, type == "todo",
                  let remindAtStr = mem["remind_at"] as? String,
                  let content = mem["content"] as? String,
                  let memoryId = mem["id"] as? String else {
                continue
            }

            let completed = (mem["completed"] as? Bool) ?? false
            if completed {
                remLog("  skip[\(i)] completed=true id=\(memoryId)")
                continue
            }

            var remindAt = isoFormatterZ.date(from: remindAtStr)
            if remindAt == nil { remindAt = isoFormatterZNoMs.date(from: remindAtStr) }
            if remindAt == nil { remindAt = localFormatter.date(from: remindAtStr) }
            let reminded = (mem["reminded"] as? Bool) ?? false

            if remindAt == nil {
                remLog("  ⚠️ [\(i)] 无法解析 remind_at: \(remindAtStr)")
                continue
            }
            if remindAt! > now {
                remLog("  ⏳ [\(i)] 未到期 id=\(memoryId) remind=\(remindAtStr)")
                continue
            }
            if reminded {
                remLog("  🔕 [\(i)] 已提醒 id=\(memoryId)")
                continue
            }
            if triggeredReminderIds.contains(memoryId) {
                remLog("  🔁 [\(i)] 重复触发 id=\(memoryId)")
                continue
            }

            foundDue = true
            triggeredReminderIds.insert(memoryId)
            remLog("  🎯 [\(i)] 到期! id=\(memoryId) content=\(content)")

            let df = DateFormatter()
            df.locale = Locale(identifier: "zh_CN")
            df.dateFormat = "yyyy年M月d日 H:mm"
            let timeStr = df.string(from: now)

            remLog("🚀 调用 triggerReminder id=\(memoryId)")
            triggerReminder(content: content, memoryId: memoryId, timeStr: timeStr)
        }
        if !foundDue && !memories.isEmpty {
            remLog("✅ checkReminders 完成，无到期提醒")
        } else if memories.isEmpty {
            remLog("✅ checkReminders 完成，无记忆数据")
        }
    }

    private func requestNotificationPermission() {
        remLog("🔔 requestNotificationPermission 开始")
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
            if let error = error {
                self.remLog("🔔 通知权限请求失败: \(error.localizedDescription)")
            } else {
                self.remLog("🔔 通知权限 granted=\(granted)")
            }
        }
    }

    private func sendSystemNotification(content: String, memoryId: String) {
        remLog("📬 sendSystemNotification id=\(memoryId) content=\(content)")
        let notification = UNMutableNotificationContent()
        notification.title = "⏰ 待办提醒"
        notification.body = content
        notification.sound = .default

        let request = UNNotificationRequest(
            identifier: "reminder-\(memoryId)",
            content: notification,
            trigger: nil
        )
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                self.remLog("📬 系统通知发送失败: \(error.localizedDescription)")
            } else {
                self.remLog("📬 系统通知已发送 id=\(memoryId)")
            }
        }
        // 同步检查当前通知设置
        UNUserNotificationCenter.current().getNotificationSettings { settings in
            self.remLog("📬 通知设置 authorizationStatus=\(settings.authorizationStatus.rawValue) alert=\(settings.alertSetting.rawValue)")
        }
    }

    private func triggerReminder(content: String, memoryId: String, timeStr: String) {
        let mode = Config.shared.agentReminderMode
        remLog("🎬 triggerReminder id=\(memoryId) mode=\(mode.rawValue)(\(mode.displayName))")

        // 系统通知模式
        if mode == .system || mode == .both {
            remLog("  → 走系统通知分支")
            sendSystemNotification(content: content, memoryId: memoryId)
            if mode == .system {
                remLog("  → 纯系统通知模式，1s 后 markTodoCompleted 并 return")
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
                    self?.markTodoCompleted(memoryId)
                }
                return
            }
            remLog("  → both 模式，系统通知已发，继续走 Agent 分支")
        } else {
            remLog("  → 走智能体通知分支")
        }

        // 智能体通知模式
        let reminderMsg = "[系统自动发送的消息] 现在是 \(timeStr)，请提醒用户：\(content)\n（待办ID：\(memoryId)）"

        DispatchQueue.main.async { [weak self] in
            guard let self = self else { self?.remLog("❌ triggerReminder self 已释放"); return }

            let isNotchVisible = self.agentNotch?.windowController?.window?.isVisible ?? false
            self.remLog("  📍 主线程 isNotchVisible=\(isNotchVisible) agentActive=\(self.agentActive)")

            // 销毁旧弹窗（如果存在）
            if self.agentNotch != nil {
                self.remLog("  🗑 销毁旧弹窗")
                self.agentNotch?.hide()
                self.agentNotch = nil
            }

            // 完全重置 UI 状态
            self.agentTracker.resetForInput()
            self.agentTracker.start()
            self.agentTracker.isTextInputMode = false
            self.agentTracker.toolsMode = Config.shared.agentToolsMode
            self.agentTracker.newConversationMode = Config.shared.agentNewConversationMode

            if !isNotchVisible {
                self.remLog("  📤 弹窗不可见，调用 showAgentNotchInternal")
                self.showAgentNotchInternal(isTextInput: false)
                self.remLog("  📤 showAgentNotchInternal 完成")
            }

            if !self.agentActive {
                self.remLog("  🔌 bridge 未运行，启动中...")
                self.agentBridge.onEvent = { [weak self] event in
                    DispatchQueue.main.async { self?.handleAgentEvent(event) }
                }
                self.agentBridge.onExit = { [weak self] code in
                    DispatchQueue.main.async {
                        self?.agentActive = false
                        self?.agentIsTextMode = false
                    }
                }
                guard self.agentBridge.start() else {
                    self.remLog("❌ agentBridge.start 失败")
                    self.agentTracker.fail("无法启动智能体进程")
                    return
                }
                self.remLog("  🔌 bridge 已启动")
                self.agentActive = true
                self.agentBridge.send(message: reminderMsg, workspace: Config.shared.agentWorkspace, allowMode: Config.shared.agentAllowMode, modelKey: nil)
                self.remLog("  📨 bridge.send 已调用")
            } else {
                self.remLog("  🔌 bridge 已在运行，追加消息")
                self.agentBridge.sendContinue(message: reminderMsg)
                self.remLog("  📨 bridge.sendContinue 已调用")
            }

            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.markTodoCompleted(memoryId)
            }
        }
    }

    func applicationWillTerminate(_ notification: Notification) {
        speechSynthesizer.stopSpeaking()
        keyMonitor.stop()
        audioMonitor.stop()
        speechRecognizer.cancel()
        recordingPanel.close()
    }

    // MARK: - NSSpeechSynthesizerDelegate

    func speechSynthesizer(_ sender: NSSpeechSynthesizer, didFinishSpeaking finishedSpeaking: Bool) {
        guard finishedSpeaking else { return }
        DispatchQueue.main.async { [weak self] in
            guard let self, let mode = self.speakingMode else { return }
            self.speakingMode = nil

            // 朗读完成后等待 5 秒再带动画关闭弹窗
            let workItem = DispatchWorkItem { [weak self] in
                switch mode {
                case .assist:
                    NotchDisplayManager.shared.extendAutoHide(to: 0.1)
                case .agent:
                    // 打字模式下不自动关闭，用户可能还在输入框中
                    if self?.agentIsTextMode != true {
                        self?.agentNotch?.show(for: 0.1)
                    }
                default:
                    break
                }
            }
            self.speakAutoHideWorkItem = workItem
            DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: workItem)
        }
    }
}
