import CodexBarCore
import Foundation

extension StatusItemController {
    private nonisolated static let menuBarCountdownRefreshEpsilon: TimeInterval = 0.05

    func scheduleMenuBarCountdownRefreshIfNeeded(now: Date = .init()) {
        self.menuBarCountdownRefreshTask?.cancel()
        self.menuBarCountdownRefreshTask = nil

        var delays: [TimeInterval] = []
        let providers = self.menuBarRefreshProviders()
        let displayMode = self.settings.menuBarDisplayMode
        let smartExhaustedActive = self.settings.menuBarShowsBrandIconWithPercent
            && self.settings.menuBarShowsResetTimeWhenExhausted
            && displayMode != .resetTime

        var countdownResetDates: [Date] = []
        var absoluteResetDates: [Date] = []
        for provider in providers {
            let resetDates = self.menuBarDisplayedResetDates(for: provider, now: now)
            let resolution = self.settings.menuBarLayoutResolution(for: provider)
            if !resolution.usesLegacyRendering,
               self.settings.menuBarIconStyle == .iconAndPercent
            {
                let tokens = resolution.layout.lines.joined()
                if tokens.contains(.resetCountdown) {
                    countdownResetDates.append(contentsOf: resetDates)
                }
                if tokens.contains(.resetAbsolute) {
                    absoluteResetDates.append(contentsOf: resetDates)
                }
                continue
            }

            guard self.settings.menuBarShowsBrandIconWithPercent,
                  displayMode == .resetTime || smartExhaustedActive
            else { continue }
            switch self.settings.resetTimeDisplayStyle {
            case .countdown:
                countdownResetDates.append(contentsOf: resetDates)
            case .absolute:
                absoluteResetDates.append(contentsOf: resetDates)
            }
        }

        if let delay = Self.menuBarCountdownRefreshDelay(resetDates: countdownResetDates, now: now) {
            // Countdown text ticks every minute; refresh on each displayed-minute boundary (the last of
            // which lands at the reset, flipping a smart-exhausted lane back to the percentage).
            delays.append(delay)
        }
        if let delay = Self.menuBarAbsoluteRefreshDelay(resetDates: absoluteResetDates, now: now) {
            // Absolute clocks don't tick each minute, but their human-friendly date label can change at
            // local midnight (for example, "tomorrow" becomes a same-day time). Wake at that boundary or
            // the reset itself, whichever comes first; the next icon update schedules any later boundary.
            delays.append(delay)
        }
        delays += self.menuBarWeeklyPaceRefreshDelays(providers: providers, now: now)

        if self.menuBarObservesCodexReset(providers: providers) {
            let projection = self.store.codexConsumerProjection(surface: .menuBar, now: now)
            if let resetAt = projection.nextMenuBarStateChangeAt {
                delays.append(max(
                    Self.menuBarCountdownRefreshEpsilon,
                    resetAt.timeIntervalSince(now) + Self.menuBarCountdownRefreshEpsilon))
            }
        }
        guard let delay = delays.min() else { return }

        self.menuBarCountdownRefreshTask = Task { @MainActor [weak self] in
            do {
                try await Task.sleep(for: .seconds(delay))
            } catch {
                return
            }
            guard let self, !Task.isCancelled else { return }
            self.menuBarCountdownRefreshTask = nil
            self.updateIcons()
        }
    }

    nonisolated static func menuBarCountdownRefreshDelay(
        resetDates: [Date],
        now: Date)
        -> TimeInterval?
    {
        resetDates.compactMap { resetDate -> TimeInterval? in
            let remaining = resetDate.timeIntervalSince(now)
            guard remaining > 0 else { return nil }
            let displayedMinutes = ceil(remaining / 60)
            let nextBoundaryRemaining = max(0, displayedMinutes - 1) * 60
            return max(
                self.menuBarCountdownRefreshEpsilon,
                remaining - nextBoundaryRemaining + self.menuBarCountdownRefreshEpsilon)
        }.min()
    }

    nonisolated static func menuBarAbsoluteRefreshDelay(
        resetDates: [Date],
        now: Date,
        calendar: Calendar = .current)
        -> TimeInterval?
    {
        guard let nextDayStart = calendar.dateInterval(of: .day, for: now)?.end else { return nil }

        return resetDates.compactMap { resetDate -> TimeInterval? in
            guard resetDate > now else { return nil }
            let nextTextChange = min(resetDate, nextDayStart)
            return max(
                self.menuBarCountdownRefreshEpsilon,
                nextTextChange.timeIntervalSince(now) + self.menuBarCountdownRefreshEpsilon)
        }.min()
    }

    private func menuBarRefreshProviders() -> [UsageProvider] {
        if self.shouldMergeIcons {
            return [self.primaryProviderForUnifiedIcon()]
        }
        return UsageProvider.allCases.filter(self.isVisible)
    }

    private func menuBarObservesCodexReset(providers: [UsageProvider]) -> Bool {
        if providers.contains(.codex) {
            return true
        }
        guard self.shouldMergeIcons, self.settings.menuBarShowsHighestUsage else {
            return false
        }
        let activeProviders = self.store.enabledFirstPartyProvidersForDisplay()
        return self.settings.resolvedMergedOverviewProviders(
            activeProviders: activeProviders,
            maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit).contains(.codex)
    }

    nonisolated static func menuBarPaceRefreshDelay(window: RateWindow, now: Date) -> TimeInterval? {
        guard let boundary = UsageStore.paceElapsedBoundary(
            window: window,
            minimumElapsedPercent: 1),
            boundary > now
        else { return nil }
        return max(
            self.menuBarCountdownRefreshEpsilon,
            boundary.timeIntervalSince(now) + self.menuBarCountdownRefreshEpsilon)
    }

    private func menuBarWeeklyPaceRefreshDelays(
        providers: [UsageProvider],
        now: Date)
        -> [TimeInterval]
    {
        providers.compactMap { provider in
            let resolution = self.settings.menuBarLayoutResolution(for: provider)
            guard !resolution.usesLegacyRendering,
                  self.settings.menuBarIconStyle == .iconAndPercent,
                  resolution.layout.lines.joined().contains(where: {
                      if case .pace(window: .weekly) = $0 { return true }
                      return false
                  })
            else { return nil }
            let snapshot = self.store.menuBarSnapshot(for: provider.instanceID)
            guard let window = self.menuBarLayoutWindows(
                provider: provider,
                snapshot: snapshot,
                now: now).weekly
            else { return nil }
            let elapsedWindow = self.store.paceWindowForElapsedEligibility(provider: provider, window: window)
            return Self.menuBarPaceRefreshDelay(window: elapsedWindow, now: now)
        }
    }

    func observeMenuBarTimeEnvironmentChanges() {
        for name in [
            Notification.Name.NSSystemClockDidChange,
            .NSSystemTimeZoneDidChange,
            .NSCalendarDayChanged,
        ] {
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(self.handleMenuBarTimeEnvironmentDidChange),
                name: name,
                object: nil)
        }
    }

    @objc nonisolated func handleMenuBarTimeEnvironmentDidChange() {
        Task { @MainActor [weak self] in
            guard let self, !self.hasPreparedForAppShutdown else { return }
            self.handleMenuBarTimeEnvironmentChange()
        }
    }

    func handleMenuBarTimeEnvironmentChange() {
        self.updateIcons()
    }

    #if DEBUG
    func _test_isMenuBarCountdownRefreshScheduled() -> Bool {
        self.menuBarCountdownRefreshTask != nil
    }
    #endif
}
