import CodexBarCore
import SwiftUI

extension UsageMenuCardView.Model {
    /// Resolves the displayed primary percentage/reset through the provider's binding quotas.
    /// Primary-owned detail text remains sourced from the raw primary window.
    static func bindingQuotaProjection(
        input: Input,
        primary: RateWindow,
        snapshot: UsageSnapshot) -> RateWindowBindingQuotaProjection?
    {
        let lanes = ProviderDescriptorRegistry.descriptor(for: input.provider)
            .presentation.primaryBindingQuotaLanes
        let bindingWindows = lanes.compactMap { lane -> RateWindow? in
            switch lane {
            case .primary: nil
            case .secondary: snapshot.secondary
            case .tertiary: snapshot.tertiary
            }
        }
        guard !bindingWindows.isEmpty else { return nil }
        return RateWindow.bindingQuotaProjection(
            primary: primary,
            bindingLanes: bindingWindows,
            now: input.now)
    }

    struct PaceDetail {
        let leftLabel: String
        let rightLabel: String?
        let pacePercent: Double?
        let paceOnTop: Bool
    }

    struct PrimaryMetricPresentation {
        var statusText: String?
        var resetText: String?
        var detailText: String?
        var detailLeft: String?
        var detailRight: String?
        var pacePercent: Double?
        var paceOnTop = true
    }

    static func applyPrimaryQuotaPresentation(
        _ presentation: inout PrimaryMetricPresentation,
        input: Input,
        primary: RateWindow)
    {
        let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
        guard let detail = self.trimmedResetDescription(primary) else { return }
        switch policy.primaryDescriptionPlacement {
        case .reset:
            presentation.resetText = detail
        case .detailLeft:
            presentation.detailLeft = detail
        case .detail:
            presentation.detailText = detail
        case .detailBySecondaryPresence:
            if input.snapshot?.secondary != nil {
                presentation.detailRight = detail
            } else {
                presentation.detailText = detail
            }
        case .standard:
            break
        }
    }

    static func applyPrimaryBalancePresentation(
        _ presentation: inout PrimaryMetricPresentation,
        input: Input,
        primary: RateWindow)
    {
        let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
        if policy.showsPrimaryBalanceDescription,
           let detail = nonEmptyResetDescription(primary)
        {
            presentation.detailText = detail
        }
        switch policy.primaryDetailKind {
        case .poeBalance:
            if let balance = Self.poeBalanceDetailText(input: input) {
                presentation.detailText = balance
            }
        case .kiroCredits:
            if let remaining = input.snapshot?.detailRow(label: "Credits left")?.value,
               let total = input.snapshot?.detailRow(label: "Credits total")?.value,
               total != "0"
            {
                presentation.detailLeft = String(format: L("%@ of %@ credits left"), remaining, total)
            }
        case .none, .requestQuota:
            break
        }
        if policy.clearsPrimaryReset {
            presentation.resetText = nil
        }
    }

    static func applyPrimaryResetPresentation(
        _ presentation: inout PrimaryMetricPresentation,
        input: Input,
        primary: RateWindow)
    {
        let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
        if policy.usesRawPrimaryResetDescription {
            presentation.resetText = primary.resetDescription
        }
        if policy.hidesPrimaryResetWithoutDate, primary.resetsAt == nil {
            presentation.resetText = nil
        }
        if policy.hidesPrimaryResetWithoutSecondary, input.snapshot?.secondary == nil {
            presentation.resetText = nil
        }
    }

    static func applyPrimaryPacePresentation(
        _ presentation: inout PrimaryMetricPresentation,
        input: Input,
        primary: RateWindow)
    {
        let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
        if let paceDetail = sessionPaceDetail(
            provider: input.provider,
            window: primary,
            now: input.now,
            showUsed: input.usageBarsShowUsed)
        {
            self.apply(paceDetail, to: &presentation)
        }
        if policy.usesAbacusPace {
            if let detail = Self.nonEmptyResetDescription(primary) {
                presentation.detailText = detail
            }
            if primary.resetsAt == nil {
                presentation.resetText = nil
            }
            if let pace = input.weeklyPace,
               let paceDetail = Self.weeklyPaceDetail(
                   provider: input.provider,
                   window: primary,
                   now: input.now,
                   pace: pace,
                   showUsed: input.usageBarsShowUsed)
            {
                Self.apply(paceDetail, to: &presentation)
            }
        } else if let paceDetail = Self.resetWindowPaceDetail(
            window: primary,
            input: input,
            pace: policy.resetWindowUsesWeeklyPace ? input.weeklyPace : nil)
        {
            Self.apply(paceDetail, to: &presentation)
        }
    }

    static func applyPrimaryFinalOverrides(
        _ presentation: inout PrimaryMetricPresentation,
        input: Input,
        primary: RateWindow)
    {
        let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
        // Legacy request-based Cursor plans surface the raw used/limit quota on its own line.
        if case .requestQuota = policy.primaryDetailKind,
           let quota = input.snapshot?.detailRow(label: "Request quota")?.value
        {
            presentation.detailText = "\(L("Request quota")): \(quota)"
        }
        if policy.usesSyntheticRollingRegen,
           let regen = Self.syntheticRollingRegenDetail(
               window: primary,
               now: input.now,
               showUsed: input.usageBarsShowUsed)
        {
            presentation.resetText = regen.resetText
            Self.apply(regen.pace, to: &presentation)
        }
        if policy.movesPrimaryDetailToStatus(snapshot: input.snapshot) {
            presentation.statusText = presentation.detailText
            presentation.detailText = nil
        }
    }

    private static func apply(_ paceDetail: PaceDetail, to presentation: inout PrimaryMetricPresentation) {
        presentation.detailLeft = paceDetail.leftLabel
        presentation.detailRight = paceDetail.rightLabel
        presentation.pacePercent = paceDetail.pacePercent
        presentation.paceOnTop = paceDetail.paceOnTop
    }

    private static func nonEmptyResetDescription(_ window: RateWindow) -> String? {
        guard let detail = window.resetDescription,
              !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        else { return nil }
        return detail
    }

    private static func trimmedResetDescription(_ window: RateWindow) -> String? {
        guard let detail = window.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
              !detail.isEmpty
        else { return nil }
        return detail
    }

    static func redactedMetricDetail(_ detail: String?, provider: UsageProvider, metricID: String) -> String? {
        guard let detail else { return nil }
        guard provider == .litellm,
              metricID == "secondary",
              detail.hasPrefix("Team "),
              let separator = detail.range(of: ": ", options: .backwards)
        else {
            return PersonalInfoRedactor.redactEmails(in: detail, isEnabled: true)
        }
        return PersonalInfoRedactor.redactEmails(in: "Team\(detail[separator.lowerBound...])", isEnabled: true)
    }

    static func redactedMetrics(
        _ metrics: [Metric],
        provider: UsageProvider,
        hidePersonalInfo: Bool) -> [Metric]
    {
        guard hidePersonalInfo else { return metrics }
        return metrics.map { metric in
            Metric(
                id: metric.id,
                title: PersonalInfoRedactor.redactEmails(in: metric.title, isEnabled: true) ?? metric.title,
                percent: metric.percent,
                percentStyle: metric.percentStyle,
                statusText: PersonalInfoRedactor.redactEmails(in: metric.statusText, isEnabled: true),
                resetText: PersonalInfoRedactor.redactEmails(in: metric.resetText, isEnabled: true),
                detailText: Self.redactedMetricDetail(
                    metric.detailText,
                    provider: provider,
                    metricID: metric.id),
                detailLeftText: PersonalInfoRedactor.redactEmails(in: metric.detailLeftText, isEnabled: true),
                detailRightText: PersonalInfoRedactor.redactEmails(in: metric.detailRightText, isEnabled: true),
                pacePercent: metric.pacePercent,
                paceOnTop: metric.paceOnTop,
                warningMarkerPercents: metric.warningMarkerPercents,
                workdayMarkerPercents: metric.workdayMarkerPercents,
                workdayTickAppearance: metric.workdayTickAppearance,
                cardStyle: metric.cardStyle,
                sessionEquivalentDetail: metric.sessionEquivalentDetail)
        }
    }

    static func usageNotes(input: Input) -> [String] {
        let subscriptionNotes = self.subscriptionMetadataNotes(snapshot: input.snapshot, provider: input.provider)

        if input.provider == .kiro {
            return self.kiroUsageNotes(input: input) + subscriptionNotes
        }

        if input.provider == .kilo {
            var notes = Self.kiloLoginDetails(snapshot: input.snapshot)
            let resolvedSource = input.sourceLabel?
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .lowercased()
            if input.kiloAutoMode,
               resolvedSource == "cli",
               !notes.contains(where: { $0.caseInsensitiveCompare("Using CLI fallback") == .orderedSame })
            {
                notes.append(L("Using CLI fallback"))
            }
            return notes + subscriptionNotes
        }

        if input.provider == .mimo, input.snapshot != nil {
            return Self.mimoUsageNotes(input: input, subscriptionNotes: subscriptionNotes)
        }

        if input.provider == .claude, input.snapshot?.dataConfidence == .percentOnly {
            // CLI-scraped usage carries rendered percentages only; label the reduced fidelity honestly.
            return [L("Usage via Claude CLI (limited detail)")] + subscriptionNotes
        }

        // Provider-specific by design: OpenCode Go local quota windows need an explicit authority warning.
        if input.provider == .opencodego, input.snapshot?.dataConfidence == .estimated {
            return [L("Quota estimated from local usage history")] + subscriptionNotes
        }

        if let notes = self.apiProviderUsageNotes(input: input) {
            return notes + subscriptionNotes
        }

        return subscriptionNotes
    }

    var isOverviewErrorOnly: Bool {
        self.subtitleStyle == .error &&
            self.metrics.isEmpty &&
            self.usageNotes.isEmpty &&
            self.providerDetails.isEmpty &&
            self.openAIAPIUsage == nil &&
            self.inlineUsageDashboard == nil &&
            self.creditsRemaining == nil &&
            self.providerCost == nil &&
            self.tokenUsage == nil &&
            self.placeholder == nil
    }

    var hasUsageContent: Bool {
        !self.metrics.isEmpty ||
            !self.usageNotes.isEmpty ||
            !self.providerDetails.isEmpty ||
            self.openAIAPIUsage != nil ||
            self.inlineUsageDashboard != nil ||
            self.codexResetCredits != nil ||
            self.placeholder != nil
    }

    var creditsOnlyInlineUsageDashboard: Bool {
        self.creditsText != nil &&
            self.inlineUsageDashboard != nil &&
            self.metrics.isEmpty &&
            self.usageNotes.isEmpty &&
            self.providerDetails.isEmpty &&
            self.openAIAPIUsage == nil &&
            self.codexResetCredits == nil &&
            self.placeholder == nil
    }

    var usesStackedDetailLayout: Bool {
        !self.metrics.isEmpty ||
            self.creditsText != nil ||
            self.codexResetCredits != nil ||
            self.providerCost != nil ||
            self.tokenUsage != nil
    }

    func hasCompatibleTrackedLayout(with candidate: Self) -> Bool {
        self.hasCompatibleTrackedLayout(with: candidate, includeMetrics: true)
    }

    func hasCompatibleTrackedLayoutIgnoringMetrics(with candidate: Self) -> Bool {
        self.hasCompatibleTrackedLayout(with: candidate, includeMetrics: false)
    }

    func hasCompatibleTrackedMetricSubset(of candidate: Self) -> Bool {
        guard self.metrics.count < candidate.metrics.count,
              self.hasCompatibleTrackedLayoutIgnoringMetrics(with: candidate)
        else {
            return false
        }
        return self.metrics.allSatisfy { metric in
            candidate.metrics.contains { Self.hasCompatibleMetricLayout(metric, $0) }
        }
    }

    private func hasCompatibleTrackedLayout(with candidate: Self, includeMetrics: Bool) -> Bool {
        guard self.provider == candidate.provider,
              !includeMetrics || self.metrics.count == candidate.metrics.count,
              self.usageNotes == candidate.usageNotes,
              self.providerDetails == candidate.providerDetails,
              (self.openAIAPIUsage == nil) == (candidate.openAIAPIUsage == nil),
              Self.hasCompatibleCreditsLayout(
                  currentText: self.creditsText,
                  currentRemaining: self.creditsRemaining,
                  candidateText: candidate.creditsText,
                  candidateRemaining: candidate.creditsRemaining),
              self.creditsHintText == candidate.creditsHintText,
              self.codexResetCredits == candidate.codexResetCredits,
              self.placeholder == candidate.placeholder,
              Self.hasCompatibleDashboardLayout(self.inlineUsageDashboard, candidate.inlineUsageDashboard),
              Self.hasCompatibleProviderCostLayout(self.providerCost, candidate.providerCost),
              Self.hasCompatibleTokenUsageLayout(self.tokenUsage, candidate.tokenUsage)
        else {
            return false
        }

        guard includeMetrics else { return true }
        return zip(self.metrics, candidate.metrics).allSatisfy(Self.hasCompatibleMetricLayout)
    }

    private static func hasCompatibleMetricLayout(_ current: Metric, _ candidate: Metric) -> Bool {
        current.id == candidate.id &&
            current.title == candidate.title &&
            current.percentStyle == candidate.percentStyle &&
            (current.statusText == nil) == (candidate.statusText == nil) &&
            (current.resetText == nil) == (candidate.resetText == nil) &&
            (current.detailText == nil) == (candidate.detailText == nil) &&
            (current.detailLeftText == nil) == (candidate.detailLeftText == nil) &&
            (current.detailRightText == nil) == (candidate.detailRightText == nil) &&
            current.cardStyle == candidate.cardStyle
    }

    private static func hasCompatibleCreditsLayout(
        currentText: String?,
        currentRemaining: Double?,
        candidateText: String?,
        candidateRemaining: Double?) -> Bool
    {
        switch (currentText, candidateText) {
        case (nil, nil):
            return true
        case let (currentText?, candidateText?):
            guard (currentRemaining == nil) == (candidateRemaining == nil) else { return false }
            // Numeric balances render as a fixed single line beside the full-scale label.
            // Multiline workspace balances retain their measured text until the menu reopens.
            return currentRemaining != nil || currentText == candidateText
        default:
            return false
        }
    }

    private static func hasCompatibleDashboardLayout(
        _ current: InlineUsageDashboardModel?,
        _ candidate: InlineUsageDashboardModel?) -> Bool
    {
        switch (current, candidate) {
        case (nil, nil):
            true
        case let (current?, candidate?):
            current.valueStyle == candidate.valueStyle &&
                current.kpis.count == candidate.kpis.count &&
                current.points.count == candidate.points.count &&
                current.detailLines.count == candidate.detailLines.count &&
                zip(current.kpis, candidate.kpis).allSatisfy {
                    $0.title == $1.title && $0.emphasis == $1.emphasis
                } &&
                zip(current.points, candidate.points).allSatisfy {
                    $0.id == $1.id && $0.label == $1.label
                }
        default:
            false
        }
    }

    private static func hasCompatibleProviderCostLayout(
        _ current: ProviderCostSection?,
        _ candidate: ProviderCostSection?) -> Bool
    {
        switch (current, candidate) {
        case (nil, nil):
            true
        case let (current?, candidate?):
            current.title == candidate.title &&
                (current.percentUsed == nil) == (candidate.percentUsed == nil) &&
                (current.percentLine == nil) == (candidate.percentLine == nil) &&
                (current.personalSpendLine == nil) == (candidate.personalSpendLine == nil)
        default:
            false
        }
    }

    private static func hasCompatibleTokenUsageLayout(
        _ current: TokenUsageSection?,
        _ candidate: TokenUsageSection?) -> Bool
    {
        switch (current, candidate) {
        case (nil, nil):
            true
        case let (current?, candidate?):
            current.hintLine == candidate.hintLine &&
                current.errorLine == candidate.errorLine &&
                (current.meteredLine == nil) == (candidate.meteredLine == nil) &&
                current.comparisonLines.count == candidate.comparisonLines.count
        default:
            false
        }
    }

    static func progressColor(for provider: UsageProvider) -> Color {
        let branding = ProviderDescriptorRegistry.descriptor(for: provider).branding
        if branding.progressColorStyle == .label {
            return Color(nsColor: .labelColor)
        }

        let color = ProviderAccentPalette.color(for: provider)
        return Color(red: color.red, green: color.green, blue: color.blue)
    }

    static func rateWindowLabels(
        input: Input,
        snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
    {
        if input.provider == .factory, snapshot.tertiary != nil {
            return ("5-hour", L("Weekly"), L("Monthly"), true)
        }
        // Legacy request-based Cursor plans track a request quota, not the token-based "Total" pool.
        let primaryLabel = if input.provider == .cursor, snapshot.detailRow(label: "Request quota") != nil {
            "Requests"
        } else if input.provider == .crof {
            CrofProviderDescriptor.primaryLabel(snapshot: snapshot)
        } else if input.provider == .grok {
            GrokProviderDescriptor.displayLabel(window: snapshot.primary, now: input.now) ?? input.metadata.sessionLabel
        } else if input.provider == .doubao {
            DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
        } else if input.provider == .sub2api {
            Sub2APIProviderDescriptor.primaryLabel(snapshot: snapshot) ?? input.metadata.sessionLabel
        } else if input.provider == .amp {
            AmpProviderDescriptor.primaryLabel(snapshot: snapshot) ?? input.metadata.sessionLabel
        } else if input.provider == .alibabatokenplan {
            AlibabaTokenPlanProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
        } else {
            input.metadata.sessionLabel
        }
        let secondaryLabel = if input.provider == .amp {
            AmpProviderDescriptor.secondaryLabel(snapshot: snapshot) ?? input.metadata.weeklyLabel
        } else if input.provider == .alibabatokenplan {
            AlibabaTokenPlanProviderDescriptor.secondaryLabel(window: snapshot.secondary) ?? input.metadata.weeklyLabel
        } else if input.provider == .sub2api {
            "Weekly"
        } else {
            input.metadata.weeklyLabel
        }
        let tertiaryLabel = input.provider == .sub2api
            ? L("Monthly")
            : input.metadata.opusLabel.map(L) ?? L("Sonnet")
        return (
            L(primaryLabel),
            L(secondaryLabel),
            tertiaryLabel,
            input.metadata.supportsOpus)
    }

    static func sub2APILocalizedDetails(_ details: [ProviderDetailSection]) -> [ProviderDetailSection] {
        details.map { section in
            guard section.title == "Usage summary" else { return section }

            do {
                var rows: [ProviderDetailSection.Row] = []
                var consumedLabels: Set<String> = []

                if let balance = section.rows.first(where: { $0.label == "Balance" }) {
                    try rows.append(ProviderDetailSection.Row(
                        label: L("Balance"),
                        value: balance.value,
                        secondaryValue: balance.secondaryValue))
                    consumedLabels.insert(balance.label)
                }

                for period in [
                    (label: "Today", requests: "Today requests", tokens: "Today tokens"),
                    (label: "Total", requests: "All time requests", tokens: "All time tokens"),
                ] {
                    guard let requests = section.rows.first(where: { $0.label == period.requests }),
                          let tokens = section.rows.first(where: { $0.label == period.tokens })
                    else { continue }

                    var secondaryParts = ["\(tokens.value) \(L("tokens"))"]
                    if let cost = tokens.secondaryValue {
                        secondaryParts.append("\(L("Cost")): \(cost)")
                    }
                    try rows.append(ProviderDetailSection.Row(
                        label: L(period.label),
                        value: "\(requests.value) \(L("requests"))",
                        secondaryValue: secondaryParts.joined(separator: " · ")))
                    consumedLabels.formUnion([period.requests, period.tokens])
                }

                rows.append(contentsOf: section.rows.filter { !consumedLabels.contains($0.label) })
                return try ProviderDetailSection(title: L("Usage"), rows: rows, chart: section.chart)
            } catch {
                return section
            }
        }
    }

    static func resetText(
        for window: RateWindow,
        style: ResetTimeDisplayStyle,
        now: Date) -> String?
    {
        UsageFormatter.resetLine(for: window, style: style, now: now)
    }

    static func placeholder(input: Input) -> String? {
        if self.shouldShowRateLimitsUnavailablePlaceholder(input: input) {
            return L("Limits not available")
        }

        if input.snapshot == nil, !input.isRefreshing, input.lastError == nil {
            return self.hasLocalCodexTokenUsage(input) ? nil : L("No usage yet")
        }

        return nil
    }

    static func lastError(input: Input) -> String? {
        guard let lastError = input.lastError?.trimmingCharacters(in: .whitespacesAndNewlines),
              !lastError.isEmpty
        else {
            return nil
        }
        // Local Codex session costs are independent from OAuth, CLI quota, and OpenAI web
        // dashboard access. Do not present a failed account-level quota fetch as a failure of
        // a valid local API-key ledger.
        if input.codexLocalSessionCostLedgerEnabled,
           self.hasLocalCodexTokenUsage(input),
           self.isRemoteCodexQuotaFetchError(lastError)
        {
            return nil
        }
        if self.shouldShowRateLimitsUnavailablePlaceholder(input: input, lastError: lastError) {
            return nil
        }
        return lastError
    }

    static func dashboardHint(error: String?) -> String? {
        guard let error, !error.isEmpty else { return nil }
        return error
    }

    static func mimoUsageNotes(input: Input, subscriptionNotes: [String]) -> [String] {
        let source = input.sourceLabel?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .lowercased()
        guard source != "local" else { return [] }
        return [
            L("Balance updates in near-real time (up to 5 min lag)"),
            L("Daily billing data finalizes at 07:00 UTC"),
        ] + subscriptionNotes
    }

    static func subscriptionMetadataNotes(snapshot: UsageSnapshot?, provider: UsageProvider) -> [String] {
        guard let snapshot else { return [] }
        if let renewsAt = snapshot.subscriptionRenewsAt {
            return [String(format: L("Renews: %@"), self.subscriptionDateString(renewsAt, provider: provider))]
        }
        if let expiresAt = snapshot.subscriptionExpiresAt {
            return [String(format: L("Plan expires: %@"), self.subscriptionDateString(expiresAt, provider: provider))]
        }
        return []
    }

    private static func subscriptionDateString(_ date: Date, provider: UsageProvider) -> String {
        let formatter = DateFormatter()
        formatter.locale = Locale.current
        formatter.timeZone = self.subscriptionDateTimeZone(provider: provider)
        formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy")
        return formatter.string(from: date)
    }

    private static func subscriptionDateTimeZone(provider: UsageProvider) -> TimeZone {
        switch provider {
        case .minimax:
            TimeZone(identifier: "Asia/Shanghai") ?? .current
        default:
            .current
        }
    }

    static func poeBalanceDetailText(input: Input) -> String? {
        guard input.provider == .poe else { return nil }
        return StatusItemController.poeBalanceDisplayText(snapshot: input.snapshot)
    }

    private static func hasLocalCodexTokenUsage(_ input: Input) -> Bool {
        input.provider == .codex &&
            input.tokenCostUsageEnabled &&
            self.tokenUsageSnapshot(input: input) != nil
    }

    private static func isRemoteCodexQuotaFetchError(_ error: String) -> Bool {
        error.localizedCaseInsensitiveContains("Codex usage is temporarily unavailable")
    }

    private static func shouldShowRateLimitsUnavailablePlaceholder(input: Input, lastError: String? = nil) -> Bool {
        let currentError = lastError ?? input.lastError
        if let currentError = currentError?.trimmingCharacters(in: .whitespacesAndNewlines),
           !currentError.isEmpty,
           !UsageError.isNoRateLimitsFoundDescription(currentError),
           !ClaudeStatusProbe.isSubscriptionQuotaUnavailableDescription(currentError)
        {
            return false
        }
        if input.limitsAvailability?.isUnavailable == true {
            return true
        }
        return self.rateLimitsUnavailable(input: input, lastError: currentError)
    }

    private static func rateLimitsUnavailable(input: Input, lastError: String? = nil) -> Bool {
        UsageLimitsAvailability.resolve(
            provider: input.provider,
            snapshot: input.snapshot,
            account: input.account,
            lastErrorDescription: lastError ?? input.lastError)
            .isUnavailable
    }

    static func sessionPaceDetail(
        provider: UsageProvider,
        window: RateWindow,
        now: Date,
        showUsed: Bool) -> PaceDetail?
    {
        guard let detail = UsagePaceText.sessionDetail(provider: provider, window: window, now: now) else { return nil }
        let expectedUsed = detail.expectedUsedPercent
        let actualUsed = window.usedPercent
        let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
        let actualPercent = showUsed ? actualUsed : (100 - actualUsed)
        if expectedPercent.isFinite == false || actualPercent.isFinite == false {
            return nil
        }
        let paceOnTop = actualUsed <= expectedUsed
        let pacePercent: Double? = if detail.stage == .onTrack {
            nil
        } else {
            expectedPercent
        }
        return PaceDetail(
            leftLabel: detail.leftLabel,
            rightLabel: detail.rightLabel,
            pacePercent: pacePercent,
            paceOnTop: paceOnTop)
    }

    static func weeklyPaceDetail(
        provider: UsageProvider,
        window: RateWindow,
        now: Date,
        pace: UsagePace?,
        showUsed: Bool) -> PaceDetail?
    {
        guard let pace, window.remainingPercent > 0 else { return nil }
        let detail = UsagePaceText.weeklyDetail(provider: provider, pace: pace, now: now)
        let expectedUsed = detail.expectedUsedPercent
        let actualUsed = window.usedPercent
        let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
        let actualPercent = showUsed ? actualUsed : (100 - actualUsed)
        if expectedPercent.isFinite == false || actualPercent.isFinite == false {
            return nil
        }
        let paceOnTop = actualUsed <= expectedUsed
        let pacePercent: Double? = if detail.stage == .onTrack {
            nil
        } else {
            expectedPercent
        }
        return PaceDetail(
            leftLabel: detail.leftLabel,
            rightLabel: detail.rightLabel,
            pacePercent: pacePercent,
            paceOnTop: paceOnTop)
    }

    static func standardWeeklyPace(input: Input, window: RateWindow) -> UsagePace? {
        if let weeklyPace = input.weeklyPace {
            return weeklyPace
        }
        return Self.displayableWeeklyPace(UsagePace.weekly(
            window: window,
            now: input.now,
            defaultWindowMinutes: 10080,
            workDays: input.workDaysPerWeek))
    }

    private static func displayableWeeklyPace(_ pace: UsagePace?) -> UsagePace? {
        guard let pace else { return nil }
        return pace.expectedUsedPercent >= 3 || pace.etaSeconds == 0 ? pace : nil
    }

    static func resetWindowPaceDetail(
        window: RateWindow,
        input: Input,
        pace: UsagePace? = nil) -> PaceDetail?
    {
        let capability = ProviderDescriptorRegistry.descriptor(for: input.provider).pace
        guard capability.supportsResetWindowPace(window: window, now: input.now),
              window.remainingPercent > 0
        else { return nil }
        let paceWindow = Self.resetWindowForPace(provider: input.provider, window: window)
        // A caller-supplied pace was measured against the raw window, so reuse it only when resolution
        // left the duration alone. Trusting it for a monthly sentinel would score the billing period as
        // a flat 30 days and silently undo the calendar-cycle resolution one line above.
        let reusablePace = paceWindow.windowMinutes == window.windowMinutes ? pace : nil
        let resolved = reusablePace ?? UsagePace.weekly(
            window: paceWindow,
            now: input.now,
            defaultWindowMinutes: 10080,
            workDays: input.workDaysPerWeek)
        guard let resolved = Self.displayableWeeklyPace(resolved) else { return nil }
        return Self.weeklyPaceDetail(
            provider: input.provider,
            window: paceWindow,
            now: input.now,
            pace: resolved,
            showUsed: input.usageBarsShowUsed)
    }

    private static func resetWindowForPace(provider: UsageProvider, window: RateWindow) -> RateWindow {
        // Provider snapshots use 30 days as a monthly sentinel; use the reset date for the real calendar-cycle length.
        ProviderDescriptorRegistry.descriptor(for: provider).pace.resolvedResetWindowForPace(window)
    }

    static func antigravityMetrics(input: Input, snapshot: UsageSnapshot) -> [Metric] {
        let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left
        if Self.hasAntigravityQuotaSummaryWindows(snapshot) {
            let metrics = Self.extraRateWindowMetrics(
                snapshot: snapshot,
                input: input,
                percentStyle: percentStyle)
            guard !input.showsAllUsageLanes else { return metrics }
            let idleIDs = AntigravityQuotaFamilyVisibility.idleWindowIDs(in: snapshot)
            return idleIDs.isEmpty ? metrics : metrics.filter { !idleIDs.contains($0.id) }
        }

        var metrics: [Metric] = []
        if let primary = snapshot.primary {
            metrics.append(Self.antigravityMetric(
                id: "primary",
                title: L(input.metadata.sessionLabel),
                window: primary,
                input: input,
                percentStyle: percentStyle))
        }
        if let secondary = snapshot.secondary {
            metrics.append(Self.antigravityMetric(
                id: "secondary",
                title: L(input.metadata.weeklyLabel),
                window: secondary,
                input: input,
                percentStyle: percentStyle))
        }
        if input.metadata.supportsOpus, let tertiary = snapshot.tertiary {
            metrics.append(Self.antigravityMetric(
                id: "tertiary",
                title: input.metadata.opusLabel.map(L) ?? L("Gemini Flash"),
                window: tertiary,
                input: input,
                percentStyle: percentStyle))
        }
        metrics.append(contentsOf: Self.extraRateWindowMetrics(
            snapshot: snapshot,
            input: input,
            percentStyle: percentStyle))
        return metrics
    }

    static func extraRateWindowMetrics(
        snapshot: UsageSnapshot,
        input: Input,
        percentStyle: PercentStyle) -> [Metric]
    {
        guard let extraRateWindows = snapshot.extraRateWindows else { return [] }
        // Codex additional limits (e.g. Codex Spark) are optional extra usage and follow the
        // "optional credits and extra usage" setting. Other providers' extra windows (Antigravity
        // per-model quotas, Factory core windows, etc.) are core data and must always render.
        if input.provider == .codex, !input.showOptionalCreditsAndExtraUsage {
            return []
        }
        if input.provider == .copilot, !input.copilotBudgetExtrasEnabled {
            return []
        }
        var visibleRateWindows = if input.provider == .codex, !input.codexSparkUsageVisible {
            extraRateWindows.filter { !Self.isCodexSparkRateWindow($0) }
        } else {
            extraRateWindows
        }
        if input.provider == .claude,
           !input.showOptionalCreditsAndExtraUsage || !input.claudeDailyRoutinesUsageVisible
        {
            visibleRateWindows.removeAll(where: Self.isClaudeDailyRoutinesRateWindow)
        }
        return visibleRateWindows.map { namedWindow in
            let paceDetail = Self.extraRateWindowPaceDetail(
                provider: input.provider,
                window: namedWindow.window,
                input: input)
            let usageKnown = namedWindow.usageKnown
            let resolvedResetText = Self.extraRateWindowResetText(
                namedWindow: namedWindow,
                input: input)
            let resetText = input.provider == .sub2api && namedWindow.window.resetsAt == nil
                ? nil
                : resolvedResetText
            let detailText: String? = if input.provider == .sub2api {
                namedWindow.window.resetDescription
            } else {
                nil
            }
            let statusText: String? = if usageKnown {
                nil
            } else if let resetText {
                "\(L("Unavailable")) - \(resetText)"
            } else {
                L("Unavailable")
            }
            let title = input.provider == .doubao && namedWindow.id.contains("-team-")
                ? "\(L(namedWindow.title)) (\(L("Team")))"
                : L(namedWindow.title)
            return Metric(
                id: namedWindow.id,
                title: title,
                percent: Self.clamped(
                    input.usageBarsShowUsed
                        ? namedWindow.window.usedPercent
                        : namedWindow.window.remainingPercent),
                percentStyle: percentStyle,
                statusText: statusText,
                resetText: usageKnown ? resetText : nil,
                detailText: usageKnown ? detailText : nil,
                detailLeftText: usageKnown ? paceDetail?.leftLabel : nil,
                detailRightText: usageKnown ? paceDetail?.rightLabel : nil,
                pacePercent: usageKnown ? paceDetail?.pacePercent : nil,
                paceOnTop: paceDetail?.paceOnTop ?? true,
                sessionEquivalentDetail: usageKnown
                    ? Self.sessionEquivalentDetail(
                        input: input,
                        weeklyWindow: namedWindow.window,
                        weeklyWindowID: namedWindow.id)
                    : nil)
        }
    }

    private static func isCodexSparkRateWindow(_ namedWindow: NamedRateWindow) -> Bool {
        namedWindow.id == CodexAdditionalRateLimitMapper.sparkWindowID ||
            namedWindow.id == CodexAdditionalRateLimitMapper.sparkWeeklyWindowID
    }

    private static func isClaudeDailyRoutinesRateWindow(_ namedWindow: NamedRateWindow) -> Bool {
        namedWindow.id == "claude-routines"
    }

    private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"

    private static func hasAntigravityQuotaSummaryWindows(_ snapshot: UsageSnapshot) -> Bool {
        snapshot.extraRateWindows?.contains(where: self.isAntigravityQuotaSummaryWindow) == true
    }

    private static func isAntigravityQuotaSummaryWindow(_ namedWindow: NamedRateWindow) -> Bool {
        namedWindow.id.hasPrefix(self.antigravityQuotaSummaryWindowIDPrefix)
    }

    private static func extraRateWindowResetText(
        namedWindow: NamedRateWindow,
        input: Input) -> String?
    {
        if namedWindow.window.resetsAt != nil {
            return self.resetText(
                for: namedWindow.window,
                style: input.resetTimeDisplayStyle,
                now: input.now)
        }
        if input.provider == .antigravity,
           self.isAntigravityQuotaSummaryWindow(namedWindow)
        {
            return self.antigravityQuotaSummaryResetText(namedWindow.window.resetDescription)
        }
        return self.resetText(
            for: namedWindow.window,
            style: input.resetTimeDisplayStyle,
            now: input.now)
    }

    private static func antigravityQuotaSummaryResetText(_ description: String?) -> String? {
        guard let description = description?.trimmingCharacters(in: .whitespacesAndNewlines),
              !description.isEmpty
        else { return nil }

        if let range = description.range(of: "fully refresh in ", options: .caseInsensitive) {
            var suffix = String(description[range.upperBound...])
                .trimmingCharacters(in: .whitespacesAndNewlines)
            while suffix.last == "." {
                suffix.removeLast()
            }
            guard !suffix.isEmpty else { return description }
            return String(format: L("Resets in %@"), suffix)
        }

        return description
    }

    private static func extraRateWindowPaceDetail(
        provider: UsageProvider,
        window: RateWindow,
        input: Input) -> PaceDetail?
    {
        if provider == .claude, window.windowMinutes != 10080 {
            return nil
        }
        guard provider == .codex || provider == .claude || provider == .antigravity else { return nil }
        switch window.windowMinutes {
        case 300:
            return self.sessionPaceDetail(
                provider: provider,
                window: window,
                now: input.now,
                showUsed: input.usageBarsShowUsed)
        case 10080:
            let pace = Self.displayableWeeklyPace(UsagePace.weekly(
                window: window,
                now: input.now,
                defaultWindowMinutes: 10080,
                workDays: input.workDaysPerWeek))
            return Self.weeklyPaceDetail(
                provider: provider,
                window: window,
                now: input.now,
                pace: pace,
                showUsed: input.usageBarsShowUsed)
        default:
            return nil
        }
    }

    private static func antigravityMetricPaceDetail(
        window: RateWindow,
        input: Input) -> PaceDetail?
    {
        guard input.provider == .antigravity else { return nil }
        switch window.windowMinutes {
        case nil, 300:
            return self.sessionPaceDetail(
                provider: input.provider,
                window: window,
                now: input.now,
                showUsed: input.usageBarsShowUsed)
        case 10080:
            let pace = Self.displayableWeeklyPace(UsagePace.weekly(
                window: window,
                now: input.now,
                defaultWindowMinutes: 10080,
                workDays: input.workDaysPerWeek))
            return Self.weeklyPaceDetail(
                provider: input.provider,
                window: window,
                now: input.now,
                pace: pace,
                showUsed: input.usageBarsShowUsed)
        default:
            return nil
        }
    }

    static func antigravityMetric(
        id: String,
        title: String,
        window: RateWindow?,
        input: Input,
        percentStyle: PercentStyle) -> Metric
    {
        guard let window else {
            let placeholderPercent = input.usageBarsShowUsed ? 100.0 : 0.0
            return Metric(
                id: id,
                title: title,
                percent: placeholderPercent,
                percentStyle: percentStyle,
                statusText: nil,
                resetText: nil,
                detailText: nil,
                detailLeftText: nil,
                detailRightText: nil,
                pacePercent: nil,
                paceOnTop: true)
        }
        let percent = input.usageBarsShowUsed ? window.usedPercent : window.remainingPercent
        let paceDetail = Self.antigravityMetricPaceDetail(window: window, input: input)
        return Metric(
            id: id,
            title: title,
            percent: Self.clamped(percent),
            percentStyle: percentStyle,
            resetText: Self.resetText(for: window, style: input.resetTimeDisplayStyle, now: input.now),
            detailText: nil,
            detailLeftText: paceDetail?.leftLabel,
            detailRightText: paceDetail?.rightLabel,
            pacePercent: paceDetail?.pacePercent,
            paceOnTop: paceDetail?.paceOnTop ?? true)
    }

    static func syntheticRegenDetail(
        weekly: RateWindow,
        cost: ProviderCostSnapshot?,
        now: Date,
        showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
    {
        guard let cost,
              cost.limit > 0,
              let nextRegenAmount = cost.nextRegenAmount,
              nextRegenAmount > 0,
              let resetsAt = weekly.resetsAt
        else { return nil }

        let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
        let resetText = String(format: L("Regenerates %@"), countdown)

        let nextRegenPercent = (nextRegenAmount / cost.limit) * 100
        let afterNextRegenRemaining = min(100, weekly.remainingPercent + nextRegenPercent)
        let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
        let suffix = showUsed ? L("used after next regen") : L("after next regen")
        let ticksToFull = max(0, cost.used) / nextRegenAmount
        let left = String(format: "%.0f%% %@", afterNextRegen, suffix)
        let right = if ticksToFull <= 0.1 {
            L("Near full")
        } else if ticksToFull < 1.5 {
            L("Full in ~1 regen")
        } else {
            String(format: L("Full in ~%.0f regens"), ceil(ticksToFull))
        }
        return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true))
    }

    static func syntheticRollingRegenDetail(
        window: RateWindow,
        now: Date,
        showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
    {
        guard let resetsAt = window.resetsAt,
              let nextRegenPercent = window.nextRegenPercent,
              nextRegenPercent > 0
        else { return nil }

        let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
        let resetText = String(format: L("Regenerates %@"), countdown)

        let afterNextRegenRemaining = min(100, window.remainingPercent + nextRegenPercent)
        let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
        let suffix = showUsed ? L("used after next regen") : L("after next regen")
        let left = String(format: "%.0f%% %@", afterNextRegen, suffix)

        let missingPercent = max(0, window.usedPercent)
        let ticksToFull = missingPercent / nextRegenPercent
        let right = if ticksToFull <= 0.1 {
            L("Near full")
        } else if ticksToFull < 1.5 {
            L("Full in ~1 regen")
        } else {
            String(format: L("Full in ~%.0f regens"), ceil(ticksToFull))
        }

        return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true))
    }
}
