import AppKit
import Foundation

struct StatusItemVisibilitySnapshot: Equatable {
    let isVisible: Bool
    let hasButton: Bool
    let hasWindow: Bool
    let hasScreen: Bool
    let isOnCurrentScreen: Bool
    let buttonWidth: CGFloat

    init(
        isVisible: Bool,
        hasButton: Bool,
        hasWindow: Bool,
        hasScreen: Bool,
        isOnCurrentScreen: Bool = true,
        buttonWidth: CGFloat)
    {
        self.isVisible = isVisible
        self.hasButton = hasButton
        self.hasWindow = hasWindow
        self.hasScreen = hasScreen
        self.isOnCurrentScreen = isOnCurrentScreen
        self.buttonWidth = buttonWidth
    }
}

extension StatusItemVisibilitySnapshot: CustomStringConvertible {
    var description: String {
        "visible=\(self.isVisible),button=\(self.hasButton),window=\(self.hasWindow),"
            + "screen=\(self.hasScreen),currentScreen=\(self.isOnCurrentScreen),"
            + "width=\(String(format: "%.1f", Double(self.buttonWidth)))"
    }
}

struct StatusItemStartupVisibilityEvidence: Equatable, CustomStringConvertible {
    let autosaveName: String
    let expectsVisibility: Bool
    let visibilityDefault: Bool?
    let snapshot: StatusItemVisibilitySnapshot

    var description: String {
        "name=\(self.autosaveName),expected=\(self.expectsVisibility),"
            + "default=\(self.visibilityDefault.map(String.init) ?? "unset"),\(self.snapshot)"
    }
}

@MainActor
func isStatusItemBlocked(_ item: NSStatusItem) -> Bool {
    MenuBarVisibilityWatcher.isBlockedSnapshot(snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
}

enum MenuBarVisibilityWatcher {
    static let guidanceShownKey = "hasShownTahoeAllowListGuidance"
    static let guidanceLastShownAtKey = "tahoeAllowListGuidanceLastShownAt"
    static let guidanceRepeatInterval: TimeInterval = 24 * 60 * 60
    static let startupFreshnessInterval: TimeInterval = 10
    static let startupCheckDelay: TimeInterval = 2
    static let screenChangeCheckDelay: Duration = .milliseconds(750)
    static let screenChangeFollowUpDelay: Duration = .seconds(2)
    static let settingsURL = URL(string: "x-apple.systempreferences:com.apple.MenuBarSettings")!

    @MainActor
    static func visibilitySnapshot(_ item: NSStatusItem) -> StatusItemVisibilitySnapshot {
        let screen = item.button?.window?.screen
        return StatusItemVisibilitySnapshot(
            isVisible: item.isVisible,
            hasButton: item.button != nil,
            hasWindow: item.button?.window != nil,
            hasScreen: screen != nil,
            isOnCurrentScreen: screen.map(self.isCurrentScreen) ?? false,
            buttonWidth: item.button?.frame.size.width ?? 0)
    }

    @MainActor
    private static func isCurrentScreen(_ screen: NSScreen) -> Bool {
        let screenNumber = self.screenNumber(screen)
        return NSScreen.screens.contains { candidate in
            if let screenNumber, let candidateNumber = self.screenNumber(candidate) {
                return candidateNumber == screenNumber
            }
            return candidate === screen
        }
    }

    private static func screenNumber(_ screen: NSScreen) -> NSNumber? {
        screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber
    }

    static func isBlockedSnapshot(snapshot: StatusItemVisibilitySnapshot) -> Bool {
        guard snapshot.isVisible else { return false }
        guard snapshot.hasButton else { return true }
        // Menu bar managers can park status-item windows off the current screen while preserving the
        // underlying NSStatusItem. Recreating in that state makes those managers see a new item.
        return !snapshot.hasWindow || snapshot.buttonWidth <= 0
    }

    static func isDisplacedSnapshot(snapshot: StatusItemVisibilitySnapshot) -> Bool {
        guard snapshot.isVisible, snapshot.hasButton, snapshot.hasWindow, snapshot.buttonWidth > 0 else {
            return false
        }
        return !snapshot.hasScreen || !snapshot.isOnCurrentScreen
    }

    static func hasBlockedVisibleSnapshots(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
        let visibleItems = snapshots.filter(\.isVisible)
        guard !visibleItems.isEmpty else { return false }
        return visibleItems.allSatisfy { snapshot in
            self.isBlockedSnapshot(snapshot: snapshot)
        }
    }

    static func hasAnyBlockedVisibleSnapshot(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
        snapshots.contains { snapshot in
            snapshot.isVisible && self.isBlockedSnapshot(snapshot: snapshot)
        }
    }

    static func hasAnyDisplacedVisibleSnapshot(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
        snapshots.contains { snapshot in
            self.isDisplacedSnapshot(snapshot: snapshot)
        }
    }

    static func hasAnyStartupRecoveryCandidate(
        snapshots: [StatusItemVisibilitySnapshot],
        evidence: [StatusItemStartupVisibilityEvidence] = [],
        windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
        detectTahoeBlockedStatusItem: Bool = false)
        -> Bool
    {
        if self.hasAnyBlockedVisibleSnapshot(snapshots) {
            return true
        }
        if detectTahoeBlockedStatusItem,
           self.hasAnyTahoeHiddenNoProxyCandidate(evidence: evidence, windowSnapshots: windowSnapshots)
        {
            return true
        }
        guard detectTahoeBlockedStatusItem,
              self.hasAnyDisplacedVisibleSnapshot(snapshots),
              windowSnapshots.contains(where: \.isTahoeBlockedProxy)
        else {
            return false
        }
        return true
    }

    static func hasAnyTahoeHiddenNoProxyCandidate(
        evidence: [StatusItemStartupVisibilityEvidence],
        windowSnapshots: [MenuBarStatusItemWindowSnapshot])
        -> Bool
    {
        evidence.contains { item in
            // Tahoe can destroy the Control Center scene while leaving its enabled default behind.
            // Requiring both app intent and that default avoids treating ordinary hidden items as blocked.
            item.expectsVisibility
                && item.visibilityDefault == true
                && !item.snapshot.isVisible
                && !item.snapshot.hasWindow
                && !windowSnapshots.contains {
                    $0.name == item.autosaveName && $0.isOnscreen && $0.isWithinDisplayBounds
                }
        }
    }

    @MainActor
    static func visibilitySnapshots(_ items: [NSStatusItem]) -> [StatusItemVisibilitySnapshot] {
        items.map { item in
            self.visibilitySnapshot(item)
        }
    }

    @MainActor
    static func hasBlockedVisibleStatusItems(_ items: [NSStatusItem]) -> Bool {
        self.hasBlockedVisibleSnapshots(self.visibilitySnapshots(items))
    }

    static func shouldAttemptStartupRecovery(
        appLaunchedAt: Date,
        now: Date = Date(),
        snapshots: [StatusItemVisibilitySnapshot],
        evidence: [StatusItemStartupVisibilityEvidence] = [],
        windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
        detectTahoeBlockedStatusItem: Bool = false)
        -> Bool
    {
        guard now.timeIntervalSince(appLaunchedAt) <= self.startupFreshnessInterval else { return false }
        return self.hasAnyStartupRecoveryCandidate(
            snapshots: snapshots,
            evidence: evidence,
            windowSnapshots: windowSnapshots,
            detectTahoeBlockedStatusItem: detectTahoeBlockedStatusItem)
    }

    static func shouldRefreshScreenChangePlacement(
        previousScreenCount _: Int,
        currentScreenCount _: Int,
        snapshots: [StatusItemVisibilitySnapshot])
        -> Bool
    {
        self.hasAnyDisplacedVisibleSnapshot(snapshots)
    }

    static func shouldAttemptScreenChangeRecovery(snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
        self.hasAnyBlockedVisibleSnapshot(snapshots)
    }

    static func shouldShowGuidance(defaults: UserDefaults, now: Date = Date()) -> Bool {
        guard defaults.bool(forKey: self.guidanceShownKey) else { return true }
        let lastShownAt = defaults.double(forKey: self.guidanceLastShownAtKey)
        guard lastShownAt > 0 else { return false }
        return now.timeIntervalSince1970 - lastShownAt >= self.guidanceRepeatInterval
    }

    static func markGuidanceShown(defaults: UserDefaults, now: Date = Date()) {
        defaults.set(true, forKey: self.guidanceShownKey)
        defaults.set(now.timeIntervalSince1970, forKey: self.guidanceLastShownAtKey)
    }

    @MainActor
    static func presentGuidance(
        defaults: UserDefaults,
        now: Date = Date(),
        openURL: (URL) -> Void = { NSWorkspace.shared.open($0) })
    {
        self.markGuidanceShown(defaults: defaults, now: now)

        let alert = NSAlert()
        alert.messageText = L("CodexBar can't show its menu bar icon")
        alert.informativeText = L(
            "macOS Tahoe can block menu bar apps in System Settings → Menu Bar → Allow in the Menu Bar. "
                + "CodexBar is running, but macOS may be hiding its icon. Open Menu Bar settings and turn CodexBar on.")
        alert.alertStyle = .warning
        alert.addButton(withTitle: L("Open Menu Bar Settings"))
        alert.addButton(withTitle: L("Dismiss"))

        if alert.runModal() == .alertFirstButtonReturn {
            openURL(self.settingsURL)
        }
    }
}

extension StatusItemController {
    func scheduleStartupStatusItemVisibilityCheck(appLaunchedAt: Date = Date()) {
        guard !SettingsStore.isRunningTests else { return }
        DispatchQueue.main.asyncAfter(deadline: .now() + MenuBarVisibilityWatcher.startupCheckDelay) { [weak self] in
            Task { @MainActor [weak self] in
                self?.checkStartupStatusItemVisibility(appLaunchedAt: appLaunchedAt)
            }
        }
    }

    private func checkStartupStatusItemVisibility(appLaunchedAt: Date, now: Date = Date()) {
        let evidence = self.startupStatusItemVisibilityEvidence()
        let snapshots = evidence.map(\.snapshot)
        let windowSnapshots = self.statusItemWindowSnapshots()
        guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
            appLaunchedAt: appLaunchedAt,
            now: now,
            snapshots: snapshots,
            evidence: evidence,
            windowSnapshots: windowSnapshots,
            detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
        else {
            return
        }

        self.menuLogger.error(
            "Status item failed to materialize or remained detached; recreating status items",
            metadata: [
                "snapshots": snapshots.map(\.description).joined(separator: " | "),
                "evidence": evidence.map(\.description).joined(separator: " | "),
                "windows": self.statusItemWindowDiagnosticsDescription(windowSnapshots),
            ])
        self.recreateStatusItemsForVisibilityRecovery()

        let recoveredEvidence = self.startupStatusItemVisibilityEvidence()
        let recoveredSnapshots = recoveredEvidence.map(\.snapshot)
        let recoveredWindowSnapshots = self.statusItemWindowSnapshots()
        guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
            appLaunchedAt: appLaunchedAt,
            now: now,
            snapshots: recoveredSnapshots,
            evidence: recoveredEvidence,
            windowSnapshots: recoveredWindowSnapshots,
            detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
        else {
            self.menuLogger.info(
                "Status item materialized after recreation",
                metadata: ["snapshots": recoveredSnapshots.map(\.description).joined(separator: " | ")])
            return
        }

        self.menuLogger.error(
            "Status item still unavailable after recreation",
            metadata: [
                "snapshots": recoveredSnapshots.map(\.description).joined(separator: " | "),
                "evidence": recoveredEvidence.map(\.description).joined(separator: " | "),
                "windows": self.statusItemWindowDiagnosticsDescription(recoveredWindowSnapshots),
            ])
        guard #available(macOS 26.0, *),
              MenuBarVisibilityWatcher.shouldShowGuidance(defaults: self.settings.userDefaults, now: now)
        else {
            return
        }
        MenuBarVisibilityWatcher.presentGuidance(defaults: self.settings.userDefaults, now: now)
    }

    @objc func handleScreenParametersDidChange(_: Notification) {
        let previousScreenCount = max(
            self.pendingScreenChangePreviousCount ?? self.lastKnownScreenCount,
            self.lastKnownScreenCount)
        let currentScreenCount = NSScreen.screens.count
        self.pendingScreenChangePreviousCount = previousScreenCount
        self.lastKnownScreenCount = currentScreenCount
        self.scheduleScreenChangeStatusItemVisibilityCheck(
            previousScreenCount: previousScreenCount,
            currentScreenCount: currentScreenCount)
    }

    private func scheduleScreenChangeStatusItemVisibilityCheck(
        previousScreenCount: Int,
        currentScreenCount: Int)
    {
        guard !SettingsStore.isRunningTests else { return }
        self.screenChangeVisibilityTask?.cancel()
        self.screenChangeVisibilityTask = Task { @MainActor [weak self] in
            do {
                try await Task.sleep(for: MenuBarVisibilityWatcher.screenChangeCheckDelay)
            } catch {
                return
            }
            self?.checkScreenChangeStatusItemVisibility(
                previousScreenCount: previousScreenCount,
                currentScreenCount: currentScreenCount)
        }
    }

    private func checkScreenChangeStatusItemVisibility(previousScreenCount: Int, currentScreenCount: Int) {
        self.pendingScreenChangePreviousCount = nil
        let settledCurrentScreenCount = NSScreen.screens.count
        self.lastKnownScreenCount = settledCurrentScreenCount
        let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
        if MenuBarVisibilityWatcher.shouldAttemptScreenChangeRecovery(snapshots: snapshots) {
            self.menuLogger.error(
                "Display configuration changed; recreating status items",
                metadata: [
                    "previousScreenCount": "\(previousScreenCount)",
                    "currentScreenCount": "\(settledCurrentScreenCount)",
                    "capturedScreenCount": "\(currentScreenCount)",
                    "snapshots": snapshots.map(\.description).joined(separator: " | "),
                    "windows": self.statusItemWindowDiagnosticsDescription(),
                ])
            self.recreateStatusItemsForVisibilityRecovery()
            self.schedulePostScreenChangeRecoveryVerification(attempt: 1)
            return
        }

        guard MenuBarVisibilityWatcher.shouldRefreshScreenChangePlacement(
            previousScreenCount: previousScreenCount,
            currentScreenCount: settledCurrentScreenCount,
            snapshots: snapshots)
        else {
            return
        }

        self.menuLogger.info(
            "Display configuration changed; refreshing existing status items",
            metadata: [
                "previousScreenCount": "\(previousScreenCount)",
                "currentScreenCount": "\(settledCurrentScreenCount)",
                "capturedScreenCount": "\(currentScreenCount)",
                "snapshots": snapshots.map(\.description).joined(separator: " | "),
            ])
        self.refreshExistingStatusItemsForVisibilityRecovery()
    }

    private func schedulePostScreenChangeRecoveryVerification(attempt: Int) {
        self.screenChangeVisibilityTask = Task { @MainActor [weak self] in
            do {
                try await Task.sleep(for: MenuBarVisibilityWatcher.screenChangeFollowUpDelay)
            } catch {
                return
            }
            self?.verifyScreenChangeRecoveryIfNeeded(attempt: attempt)
        }
    }

    private func verifyScreenChangeRecoveryIfNeeded(attempt: Int) {
        let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
        guard MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(snapshots) else {
            self.menuLogger.info(
                "Status item recovered after display-change recovery",
                metadata: ["attempt": "\(attempt)", "snapshots": snapshots.map(\.description).joined(separator: " | ")])
            return
        }

        self.menuLogger.error(
            "Status item still blocked after display-change recovery; recreating status items again",
            metadata: [
                "attempt": "\(attempt)",
                "snapshots": snapshots.map(\.description).joined(separator: " | "),
                "windows": self.statusItemWindowDiagnosticsDescription(),
            ])
        self.recreateStatusItemsForVisibilityRecovery()
        // No further async retries: a menu bar manager may park the newly recreated item in a state
        // that still looks blocked, causing repeated NSStatusItem destruction that corrupts Control Center.
        // Instead, do one synchronous re-check to surface guidance if macOS itself is blocking the item.
        let finalSnapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
        guard MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(finalSnapshots) else { return }
        self.menuLogger.error(
            "Status item still blocked after display-change recovery recreation",
            metadata: [
                "snapshots": finalSnapshots.map(\.description).joined(separator: " | "),
                "windows": self.statusItemWindowDiagnosticsDescription(),
            ])
        guard #available(macOS 26.0, *),
              MenuBarVisibilityWatcher.shouldShowGuidance(defaults: self.settings.userDefaults)
        else { return }
        MenuBarVisibilityWatcher.presentGuidance(defaults: self.settings.userDefaults)
    }

    private var startupVisibilityStatusItems: [NSStatusItem] {
        [self.statusItem] + Array(self.statusItems.values)
    }

    private func startupStatusItemVisibilityEvidence() -> [StatusItemStartupVisibilityEvidence] {
        self.startupVisibilityStatusItems.map { item in
            let autosaveName = item.autosaveName ?? ""
            return StatusItemStartupVisibilityEvidence(
                autosaveName: autosaveName,
                expectsVisibility: self.expectedVisibleStatusItemAutosaveNames.contains(autosaveName),
                visibilityDefault: MenuBarStatusItemDefaultsRepair.visibilityDefault(
                    defaults: self.settings.userDefaults,
                    autosaveName: autosaveName),
                snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
        }
    }

    private var canDetectTahoeBlockedStatusItem: Bool {
        if #available(macOS 26.0, *) {
            return true
        }
        return false
    }

    private func statusItemWindowSnapshots() -> [MenuBarStatusItemWindowSnapshot] {
        let names = Set(self.startupVisibilityStatusItems.compactMap { item in
            item.autosaveName.isEmpty ? nil : item.autosaveName
        })
        return MenuBarStatusItemWindowProbe.snapshots(matching: names)
    }

    private func statusItemWindowDiagnosticsDescription(
        _ snapshots: [MenuBarStatusItemWindowSnapshot]? = nil)
        -> String
    {
        let snapshots = snapshots ?? self.statusItemWindowSnapshots()
        guard !snapshots.isEmpty else { return "none" }
        return snapshots.map(\.description).joined(separator: " | ")
    }
}
