import AppKit
import NucleicCore

/// Bounces the app's Dock icon to call the user back to a chat that has needed them for a
/// while. `AppStore` tracks *which* chats are blocked-and-unviewed and calls `setPending(_:)`
/// as that set flips between empty and non-empty (`AppStore.chatDockBounceHook`, installed in
/// `NucleicApp.init`) — the very signal that also drives `ChatAlarm`. This type adds the "for
/// more than a minute" gate: a `true` arms a 60-second countdown, and only if a chat is *still*
/// blocked-and-unviewed when it elapses does the Dock icon start bouncing. macOS bounces a
/// critical attention request until the app is foregrounded, then stops it on its own; we also
/// cancel it early if the chat resolves (or the user opens it) before they return. A singleton
/// so the one pending request is shared app-wide. Honors the on-by-default Settings switch.
@MainActor
final class DockBounce {
    static let shared = DockBounce()
    private init() {}

    /// `@AppStorage` key for the Settings switch. On by default — a background Dock bounce is
    /// the standard, unobtrusive macOS "come back, something needs you" cue, so it's opt-*out*
    /// (unlike the noisier, opt-in escalating alarm). Read with the `object(forKey:) as? Bool
    /// ?? true` dance so it stays on until the Settings view first writes a value: a plain
    /// `bool(forKey:)` would read the unset key as `false` and silently disable the feature.
    static let enabledKey = "nucleic.chat.dockBounce"
    static var isEnabled: Bool {
        UserDefaults.standard.object(forKey: enabledKey) as? Bool ?? true
    }

    /// How long a chat must sit blocked-and-unviewed before the Dock starts bouncing.
    private let delay = Duration.seconds(60)

    /// The armed countdown before bouncing, or nil when disarmed. Cancelling it stops a bounce
    /// that hasn't begun yet.
    private var timer: Task<Void, Never>?
    /// The in-flight AppKit attention request, held so we can cancel the bounce if the chat
    /// resolves before the user foregrounds the app. macOS clears it on its own once the app
    /// is activated, but the stored id is harmless to cancel again.
    private var request: Int?

    /// Arm or disarm the Dock bounce. `pending == true` starts the one-minute countdown (a no-op
    /// while the switch is off, so a blocked chat simply never bounces); `pending == false` cancels
    /// a countdown that hasn't fired and stops a bounce already in progress. Re-arming while a
    /// countdown or bounce is live is a no-op, so the minute is never restarted underneath itself.
    func setPending(_ pending: Bool) {
        if pending {
            guard Self.isEnabled else { return }
            arm()
        } else {
            disarm()
        }
    }

    private func arm() {
        guard timer == nil, request == nil else { return }  // already counting down or bouncing
        let delay = self.delay
        timer = Task { @MainActor [weak self] in
            try? await Task.sleep(for: delay)
            guard let self, !Task.isCancelled else { return }
            self.timer = nil
            self.bounce()
        }
    }

    /// Start the Dock bounce. A critical request bounces the icon until the app is foregrounded
    /// (or we cancel it); if the app is already frontmost this is a harmless no-op.
    private func bounce() {
        guard request == nil else { return }
        request = NSApp.requestUserAttention(.criticalRequest)
    }

    private func disarm() {
        timer?.cancel()
        timer = nil
        if let request {
            NSApp.cancelUserAttentionRequest(request)
            self.request = nil
        }
    }
}
