import Foundation

public struct RateWindow: Codable, Equatable, Sendable {
    /// Provider usage value, intentionally not normalized globally. Pace and provider-specific diagnostics may
    /// preserve raw over-quota values; display-only projections should use `UsagePercent.displayClamped`.
    public let usedPercent: Double
    public let windowMinutes: Int?
    public let resetsAt: Date?
    /// Optional textual reset description (used by Claude CLI UI scrape).
    public let resetDescription: String?
    /// Optional percent restored on the next regeneration tick for providers with rolling recovery.
    public let nextRegenPercent: Double?
    /// Whether this window was synthesized to stand in for a quota lane the provider did not actually
    /// report, rather than being a real zero-usage window.
    ///
    /// Claude web returns a `0%` five-hour window when `five_hour` is `null` (an account with no live
    /// session but a real weekly lane). Lane classifiers — e.g. the combined "Session + Weekly" menu-bar
    /// metric — must treat such a window as "no session lane present" instead of surfacing a phantom
    /// `5h 0%`/`5h 100%` session. A genuine session, even one freshly reset to 0%, is NOT a placeholder.
    /// Missing values decode as `false` for older cached payloads.
    public let isSyntheticPlaceholder: Bool

    public init(
        usedPercent: Double,
        windowMinutes: Int?,
        resetsAt: Date?,
        resetDescription: String?,
        nextRegenPercent: Double? = nil,
        isSyntheticPlaceholder: Bool = false)
    {
        self.usedPercent = usedPercent
        self.windowMinutes = windowMinutes
        self.resetsAt = resetsAt
        self.resetDescription = resetDescription
        self.nextRegenPercent = nextRegenPercent
        self.isSyntheticPlaceholder = isSyntheticPlaceholder
    }

    private enum CodingKeys: String, CodingKey {
        case usedPercent
        case windowMinutes
        case resetsAt
        case resetDescription
        case nextRegenPercent
        case isSyntheticPlaceholder
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.usedPercent = try container.decode(Double.self, forKey: .usedPercent)
        self.windowMinutes = try container.decodeIfPresent(Int.self, forKey: .windowMinutes)
        self.resetsAt = try container.decodeIfPresent(Date.self, forKey: .resetsAt)
        self.resetDescription = try container.decodeIfPresent(String.self, forKey: .resetDescription)
        self.nextRegenPercent = try container.decodeIfPresent(Double.self, forKey: .nextRegenPercent)
        self.isSyntheticPlaceholder =
            try container.decodeIfPresent(Bool.self, forKey: .isSyntheticPlaceholder) ?? false
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.usedPercent, forKey: .usedPercent)
        try container.encodeIfPresent(self.windowMinutes, forKey: .windowMinutes)
        try container.encodeIfPresent(self.resetsAt, forKey: .resetsAt)
        try container.encodeIfPresent(self.resetDescription, forKey: .resetDescription)
        try container.encodeIfPresent(self.nextRegenPercent, forKey: .nextRegenPercent)
        // Only persist the flag when set, keeping payloads identical for the common (real-window) case.
        if self.isSyntheticPlaceholder {
            try container.encode(true, forKey: .isSyntheticPlaceholder)
        }
    }

    public var remainingPercent: Double {
        max(0, 100 - self.usedPercent)
    }

    public func backfillingResetTime(from cached: RateWindow?, now: Date = .init()) -> RateWindow {
        if self.resetsAt != nil {
            return self
        }
        guard let cachedReset = cached?.resetsAt, cachedReset > now else { return self }
        let windowMinutes = if let windowMinutes = self.windowMinutes, windowMinutes > 0 {
            windowMinutes
        } else {
            cached?.windowMinutes
        }
        return RateWindow(
            usedPercent: self.usedPercent,
            windowMinutes: windowMinutes,
            resetsAt: cachedReset,
            resetDescription: self.resetDescription ?? cached?.resetDescription,
            nextRegenPercent: self.nextRegenPercent,
            // Preserve the placeholder marker: backfilling a stale reset onto Claude web's null-session
            // placeholder must not let it masquerade as a real session lane.
            isSyntheticPlaceholder: self.isSyntheticPlaceholder)
    }
}

public struct NamedRateWindow: Codable, Equatable, Sendable {
    public let id: String
    public let title: String
    public let window: RateWindow
    /// Whether `window.usedPercent` reflects known quota usage.
    ///
    /// Some providers expose reset metadata for a named quota window before
    /// they expose remaining usage. Keep those windows visible for reset/debug
    /// context, but mark them so clients do not render `usedPercent` as a real
    /// exhausted quota. Missing values decode as `true` for older cached payloads.
    public let usageKnown: Bool

    public init(id: String, title: String, window: RateWindow, usageKnown: Bool = true) {
        self.id = id
        self.title = title
        self.window = window
        self.usageKnown = usageKnown
    }

    private enum CodingKeys: String, CodingKey {
        case id
        case title
        case window
        case usageKnown
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.title = try container.decode(String.self, forKey: .title)
        self.window = try container.decode(RateWindow.self, forKey: .window)
        self.usageKnown = try container.decodeIfPresent(Bool.self, forKey: .usageKnown) ?? true
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.id, forKey: .id)
        try container.encode(self.title, forKey: .title)
        try container.encode(self.window, forKey: .window)
        if !self.usageKnown {
            try container.encode(false, forKey: .usageKnown)
        }
    }
}

public struct UsageSnapshot: Codable, Sendable {
    public let primary: RateWindow?
    public let secondary: RateWindow?
    public let tertiary: RateWindow?
    public let extraRateWindows: [NamedRateWindow]?
    public let providerCost: ProviderCostSnapshot?
    public let details: [ProviderDetailSection]
    public let deepseekDetailedUsageState: DeepSeekDetailedUsageState
    public let deepseekPlatformProfiles: [DeepSeekPlatformProfile]
    public let opencodegoUsage: OpenCodeGoUsageSnapshot?
    public let openAIAPIUsage: OpenAIAPIUsageSnapshot?
    public let codexResetCredits: CodexRateLimitResetCreditsSnapshot?
    public let mistralUsage: MistralUsageSnapshot?
    /// Live-only marker for optional Command Code subscription lookup failure.
    public let commandCodeSubscriptionEnrichmentUnavailable: Bool
    /// Live-only marker that Command Code returned a recognized subscription plan.
    public let commandCodeHasSubscriptionPlan: Bool
    /// Live-only marker that Command Code's monthly grant has no remaining credits.
    public let commandCodeMonthlyGrantDepleted: Bool
    public let subscriptionExpiresAt: Date?
    public let subscriptionRenewsAt: Date?
    public let updatedAt: Date
    public let identity: ProviderIdentitySnapshot?
    public let dataConfidence: UsageDataConfidence

    private enum CodingKeys: String, CodingKey {
        case primary
        case secondary
        case tertiary
        case extraRateWindows
        case providerCost
        case details
        case openAIAPIUsage
        case codexResetCredits
        case mistralUsage
        case subscriptionExpiresAt
        case subscriptionRenewsAt
        case updatedAt
        case identity
        case dataConfidence
        case accountEmail
        case accountOrganization
        case loginMethod
    }

    public init(
        primary: RateWindow?,
        secondary: RateWindow?,
        tertiary: RateWindow? = nil,
        extraRateWindows: [NamedRateWindow]? = nil,
        providerCost: ProviderCostSnapshot? = nil,
        details: [ProviderDetailSection] = [],
        deepseekDetailedUsageState: DeepSeekDetailedUsageState = .notRequested,
        deepseekPlatformProfiles: [DeepSeekPlatformProfile] = [],
        opencodegoUsage: OpenCodeGoUsageSnapshot? = nil,
        openAIAPIUsage: OpenAIAPIUsageSnapshot? = nil,
        codexResetCredits: CodexRateLimitResetCreditsSnapshot? = nil,
        mistralUsage: MistralUsageSnapshot? = nil,
        commandCodeSubscriptionEnrichmentUnavailable: Bool = false,
        commandCodeHasSubscriptionPlan: Bool = false,
        commandCodeMonthlyGrantDepleted: Bool = false,
        subscriptionExpiresAt: Date? = nil,
        subscriptionRenewsAt: Date? = nil,
        updatedAt: Date,
        identity: ProviderIdentitySnapshot? = nil,
        dataConfidence: UsageDataConfidence = .unknown)
    {
        precondition(
            details.count <= ProviderDetailSection.maximumSectionsPerSnapshot,
            "UsageSnapshot details exceeds \(ProviderDetailSection.maximumSectionsPerSnapshot) sections")
        self.primary = primary
        self.secondary = secondary
        self.tertiary = tertiary
        self.extraRateWindows = extraRateWindows
        self.providerCost = providerCost
        self.details = details
        self.deepseekDetailedUsageState = deepseekDetailedUsageState
        self.deepseekPlatformProfiles = deepseekPlatformProfiles
        self.opencodegoUsage = opencodegoUsage
        self.openAIAPIUsage = openAIAPIUsage
        self.codexResetCredits = codexResetCredits
        self.mistralUsage = mistralUsage
        self.commandCodeSubscriptionEnrichmentUnavailable = commandCodeSubscriptionEnrichmentUnavailable
        self.commandCodeHasSubscriptionPlan = commandCodeHasSubscriptionPlan
        self.commandCodeMonthlyGrantDepleted = commandCodeMonthlyGrantDepleted
        self.subscriptionExpiresAt = subscriptionExpiresAt
        self.subscriptionRenewsAt = subscriptionRenewsAt
        self.updatedAt = updatedAt
        self.identity = identity
        self.dataConfidence = dataConfidence
    }

    public func with(extraRateWindows: [NamedRateWindow]?) -> UsageSnapshot {
        self.replacing(extraRateWindows: .value(extraRateWindows))
    }

    public func withCodexResetCredits(_ resetCredits: CodexRateLimitResetCreditsSnapshot?) -> UsageSnapshot {
        self.replacing(codexResetCredits: .value(resetCredits))
    }

    public func withSubscriptionMetadata(expiresAt: Date?, renewsAt: Date?) -> UsageSnapshot {
        self.replacing(
            subscriptionExpiresAt: .value(expiresAt),
            subscriptionRenewsAt: .value(renewsAt))
    }

    public func with(primary: RateWindow?, secondary: RateWindow?) -> UsageSnapshot {
        self.replacing(
            primary: .value(primary),
            secondary: .value(secondary))
    }

    public func with(tertiary: RateWindow?) -> UsageSnapshot {
        self.replacing(tertiary: .value(tertiary))
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.primary = try container.decodeIfPresent(RateWindow.self, forKey: .primary)
        self.secondary = try container.decodeIfPresent(RateWindow.self, forKey: .secondary)
        self.tertiary = try container.decodeIfPresent(RateWindow.self, forKey: .tertiary)
        self.extraRateWindows = try container.decodeIfPresent([NamedRateWindow].self, forKey: .extraRateWindows)
        self.providerCost = try container.decodeIfPresent(ProviderCostSnapshot.self, forKey: .providerCost)
        self.details = try container.decodeIfPresent([ProviderDetailSection].self, forKey: .details) ?? []
        try ProviderDetailSection.validateSections(self.details)
        self.deepseekDetailedUsageState = .notRequested // Live-only fetch state
        self.deepseekPlatformProfiles = [] // Live-only browser profile catalog
        self.opencodegoUsage = nil // Not persisted, fetched fresh each time
        self.openAIAPIUsage = try container.decodeIfPresent(OpenAIAPIUsageSnapshot.self, forKey: .openAIAPIUsage)
        self.codexResetCredits = try container.decodeIfPresent(
            CodexRateLimitResetCreditsSnapshot.self,
            forKey: .codexResetCredits)
        self.mistralUsage = try container.decodeIfPresent(MistralUsageSnapshot.self, forKey: .mistralUsage)
        self.commandCodeSubscriptionEnrichmentUnavailable = false // Live-only fetch state
        self.commandCodeHasSubscriptionPlan = false // Live-only fetch state
        self.commandCodeMonthlyGrantDepleted = false // Live-only fetch state
        self.subscriptionExpiresAt = try container.decodeIfPresent(Date.self, forKey: .subscriptionExpiresAt)
        self.subscriptionRenewsAt = try container.decodeIfPresent(Date.self, forKey: .subscriptionRenewsAt)
        self.updatedAt = try container.decode(Date.self, forKey: .updatedAt)
        if let dataConfidence = try container.decodeIfPresent(String.self, forKey: .dataConfidence) {
            self.dataConfidence = UsageDataConfidence(rawValue: dataConfidence) ?? .unknown
        } else {
            self.dataConfidence = .unknown
        }
        if let identity = try container.decodeIfPresent(ProviderIdentitySnapshot.self, forKey: .identity) {
            self.identity = identity
        } else {
            let email = try container.decodeIfPresent(String.self, forKey: .accountEmail)
            let organization = try container.decodeIfPresent(String.self, forKey: .accountOrganization)
            let loginMethod = try container.decodeIfPresent(String.self, forKey: .loginMethod)
            if email != nil || organization != nil || loginMethod != nil {
                self.identity = ProviderIdentitySnapshot(
                    providerID: nil,
                    accountEmail: email,
                    accountOrganization: organization,
                    loginMethod: loginMethod)
            } else {
                self.identity = nil
            }
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // Stable JSON schema: keep window keys present (encode `nil` as `null`).
        try container.encode(self.primary, forKey: .primary)
        try container.encode(self.secondary, forKey: .secondary)
        try container.encode(self.tertiary, forKey: .tertiary)
        try container.encodeIfPresent(self.extraRateWindows, forKey: .extraRateWindows)
        try container.encodeIfPresent(self.providerCost, forKey: .providerCost)
        if !self.details.isEmpty {
            try container.encode(self.details, forKey: .details)
        }
        try container.encodeIfPresent(self.openAIAPIUsage, forKey: .openAIAPIUsage)
        try container.encodeIfPresent(self.codexResetCredits, forKey: .codexResetCredits)
        try container.encodeIfPresent(self.mistralUsage, forKey: .mistralUsage)
        try container.encodeIfPresent(self.subscriptionExpiresAt, forKey: .subscriptionExpiresAt)
        try container.encodeIfPresent(self.subscriptionRenewsAt, forKey: .subscriptionRenewsAt)
        try container.encode(self.updatedAt, forKey: .updatedAt)
        try container.encodeIfPresent(self.identity, forKey: .identity)
        if self.dataConfidence != .unknown {
            try container.encode(self.dataConfidence, forKey: .dataConfidence)
        }
        try container.encodeIfPresent(self.identity?.accountEmail, forKey: .accountEmail)
        try container.encodeIfPresent(self.identity?.accountOrganization, forKey: .accountOrganization)
        try container.encodeIfPresent(self.identity?.loginMethod, forKey: .loginMethod)
    }

    public func identity(for instanceID: ProviderInstanceID) -> ProviderIdentitySnapshot? {
        guard let identity, identity.providerID == instanceID else { return nil }
        return identity
    }

    public func automaticPerplexityWindow() -> RateWindow? {
        let fallbackWindows = self.orderedPerplexityFallbackWindows()
        guard let primary = self.primary else {
            return fallbackWindows.first
        }
        if primary.remainingPercent > 0 || fallbackWindows.isEmpty {
            return primary
        }
        return fallbackWindows.first
    }

    public func orderedPerplexityDisplayWindows() -> [RateWindow] {
        let fallbackWindows = self.orderedPerplexityFallbackWindows()
        guard let primary = self.primary else {
            return fallbackWindows
        }
        if primary.remainingPercent > 0 || fallbackWindows.isEmpty {
            return [primary] + fallbackWindows
        }
        return fallbackWindows + [primary]
    }

    public func accountEmail(for provider: UsageProvider) -> String? {
        self.identity(for: provider.instanceID)?.accountEmail
    }

    public func accountOrganization(for provider: UsageProvider) -> String? {
        self.identity(for: provider.instanceID)?.accountOrganization
    }

    public func loginMethod(for provider: UsageProvider) -> String? {
        self.identity(for: provider.instanceID)?.loginMethod
    }

    public var hasRateLimitWindows: Bool {
        self.primary != nil || self.secondary != nil || self.tertiary != nil ||
            !(self.extraRateWindows?.isEmpty ?? true)
    }

    public func detailRow(label: String) -> ProviderDetailSection.Row? {
        self.details.lazy.flatMap(\.rows).first { $0.label == label }
    }

    public func rateLimitsUnavailable(for provider: UsageProvider) -> Bool {
        UsageLimitsAvailability.resolve(provider: provider, snapshot: self).isUnavailable
    }

    public func withIdentity(_ identity: ProviderIdentitySnapshot?) -> UsageSnapshot {
        self.replacing(identity: .value(identity))
    }

    public func withDataConfidence(_ dataConfidence: UsageDataConfidence) -> UsageSnapshot {
        self.replacing(dataConfidence: .value(dataConfidence))
    }

    public func scoped(to provider: UsageProvider) -> UsageSnapshot {
        guard let identity else { return self }
        let scopedIdentity = identity.scoped(to: provider)
        if scopedIdentity.providerID == identity.providerID {
            return self
        }
        return self.withIdentity(scopedIdentity)
    }

    public func backfillingResetTimes(from cached: UsageSnapshot?, now: Date = .init()) -> UsageSnapshot {
        guard let cached else { return self }
        guard Self.identitiesMatch(self.identity, cached.identity) else { return self }
        // Amp's percentage-based daily quota supersedes the legacy rolling-replenishment cadence. Do not attach
        // that older exact reset to the new daily window; other providers retain the shared backfill behavior.
        // Provider-specific by design: Amp daily quotas must not inherit its obsolete rolling-reset cadence.
        let cachedPrimary: RateWindow? = if self.identity?.providerID == .amp,
                                            self.primary?.resetDescription == "resets daily"
        {
            nil
        } else {
            cached.primary
        }
        let primary = self.primary?.backfillingResetTime(from: cachedPrimary, now: now)
        let secondary = self.secondary?.backfillingResetTime(from: cached.secondary, now: now)
        let tertiary = self.tertiary?.backfillingResetTime(from: cached.tertiary, now: now)
        if primary == self.primary, secondary == self.secondary, tertiary == self.tertiary {
            return self
        }
        return self.replacing(
            primary: .value(primary),
            secondary: .value(secondary),
            tertiary: .value(tertiary))
    }

    private func orderedPerplexityFallbackWindows() -> [RateWindow] {
        let fallbackWindows = [self.tertiary, self.secondary].compactMap(\.self)
        let usableFallback = fallbackWindows.filter { $0.remainingPercent > 0 }
        let exhaustedFallback = fallbackWindows.filter { $0.remainingPercent <= 0 }
        return usableFallback + exhaustedFallback
    }

    private static func identitiesMatch(_ lhs: ProviderIdentitySnapshot?, _ rhs: ProviderIdentitySnapshot?) -> Bool {
        if lhs == nil, rhs == nil {
            return true
        }
        guard let lhs, let rhs else { return false }
        let lhsAccountID = lhs.accountID?.trimmingCharacters(in: .whitespacesAndNewlines)
        let rhsAccountID = rhs.accountID?.trimmingCharacters(in: .whitespacesAndNewlines)
        if let lhsAccountID, let rhsAccountID, !lhsAccountID.isEmpty, !rhsAccountID.isEmpty {
            return lhsAccountID == rhsAccountID
        }
        let lhsEmail = lhs.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
        let rhsEmail = rhs.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
        if let lhsEmail, let rhsEmail, !lhsEmail.isEmpty, !rhsEmail.isEmpty {
            return lhsEmail == rhsEmail
        }
        return true
    }

    enum Replacement<Value> {
        case unchanged
        case value(Value)

        func resolving(_ current: Value) -> Value {
            switch self {
            case .unchanged: current
            case let .value(value): value
            }
        }
    }

    func replacing(
        primary: Replacement<RateWindow?> = .unchanged,
        secondary: Replacement<RateWindow?> = .unchanged,
        tertiary: Replacement<RateWindow?> = .unchanged,
        extraRateWindows: Replacement<[NamedRateWindow]?> = .unchanged,
        details: Replacement<[ProviderDetailSection]> = .unchanged,
        deepseekDetailedUsageState: Replacement<DeepSeekDetailedUsageState> = .unchanged,
        deepseekPlatformProfiles: Replacement<[DeepSeekPlatformProfile]> = .unchanged,
        codexResetCredits: Replacement<CodexRateLimitResetCreditsSnapshot?> = .unchanged,
        subscriptionExpiresAt: Replacement<Date?> = .unchanged,
        subscriptionRenewsAt: Replacement<Date?> = .unchanged,
        identity: Replacement<ProviderIdentitySnapshot?> = .unchanged,
        dataConfidence: Replacement<UsageDataConfidence> = .unchanged) -> UsageSnapshot
    {
        UsageSnapshot(
            primary: primary.resolving(self.primary),
            secondary: secondary.resolving(self.secondary),
            tertiary: tertiary.resolving(self.tertiary),
            extraRateWindows: extraRateWindows.resolving(self.extraRateWindows),
            providerCost: self.providerCost,
            details: details.resolving(self.details),
            deepseekDetailedUsageState: deepseekDetailedUsageState.resolving(self.deepseekDetailedUsageState),
            deepseekPlatformProfiles: deepseekPlatformProfiles.resolving(self.deepseekPlatformProfiles),
            opencodegoUsage: self.opencodegoUsage,
            openAIAPIUsage: self.openAIAPIUsage,
            codexResetCredits: codexResetCredits.resolving(self.codexResetCredits),
            mistralUsage: self.mistralUsage,
            commandCodeSubscriptionEnrichmentUnavailable: self.commandCodeSubscriptionEnrichmentUnavailable,
            commandCodeHasSubscriptionPlan: self.commandCodeHasSubscriptionPlan,
            commandCodeMonthlyGrantDepleted: self.commandCodeMonthlyGrantDepleted,
            subscriptionExpiresAt: subscriptionExpiresAt.resolving(self.subscriptionExpiresAt),
            subscriptionRenewsAt: subscriptionRenewsAt.resolving(self.subscriptionRenewsAt),
            updatedAt: self.updatedAt,
            identity: identity.resolving(self.identity),
            dataConfidence: dataConfidence.resolving(self.dataConfidence))
    }
}

public struct AccountInfo: Equatable, Sendable {
    public let email: String?
    public let plan: String?

    public var hasIdentity: Bool {
        self.email?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ||
            self.plan?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
    }

    public init(email: String?, plan: String?) {
        self.email = email
        self.plan = plan
    }
}

public struct CodexCLIAccountSnapshot: Sendable {
    public let usage: UsageSnapshot?
    public let credits: CreditsSnapshot?
    public let identity: ProviderIdentitySnapshot?

    public init(
        usage: UsageSnapshot?,
        credits: CreditsSnapshot?,
        identity: ProviderIdentitySnapshot? = nil)
    {
        self.usage = usage
        self.credits = credits
        self.identity = identity
    }
}

public enum UsageError: LocalizedError, Sendable {
    case noSessions
    case noRateLimitsFound
    case decodeFailed

    public var errorDescription: String? {
        switch self {
        case .noSessions:
            "No Codex sessions found yet. Run at least one Codex prompt first."
        case .noRateLimitsFound:
            "Found sessions, but no rate limit events yet."
        case .decodeFailed:
            "Could not parse Codex session log."
        }
    }

    public static func isNoRateLimitsFoundDescription(_ text: String?) -> Bool {
        text?.trimmingCharacters(in: .whitespacesAndNewlines) == UsageError.noRateLimitsFound.errorDescription
    }
}

public enum UsageLimitsAvailability: Equatable, Sendable {
    case available
    case unavailable

    public var isUnavailable: Bool {
        self == .unavailable
    }

    public static func resolve(
        provider: UsageProvider,
        snapshot: UsageSnapshot?,
        account: AccountInfo? = nil,
        lastErrorDescription: String? = nil) -> Self
    {
        // Provider-specific by design: Claude error text, Codex identity, and Doubao/Antigravity identities signal
        // whether a successful payload actually contains subscription limits.
        if provider == .claude {
            guard snapshot == nil else { return .available }
            return ClaudeStatusProbe.isSubscriptionQuotaUnavailableDescription(lastErrorDescription)
                ? .unavailable
                : .available
        }

        if provider == .doubao || provider == .antigravity {
            guard let snapshot,
                  snapshot.identity(for: provider.instanceID) != nil
            else {
                return .available
            }
            return snapshot.hasRateLimitWindows ? .available : .unavailable
        }

        guard provider == .codex else { return .available }

        if let snapshot {
            guard snapshot.identity(for: provider.instanceID) != nil else { return .available }
            return snapshot.hasRateLimitWindows ? .available : .unavailable
        }

        guard UsageError.isNoRateLimitsFoundDescription(lastErrorDescription),
              account?.hasIdentity == true
        else {
            return .available
        }
        return .unavailable
    }
}

// MARK: - Codex RPC client (local process)

private struct RPCAccountResponse: Decodable {
    let account: RPCAccountDetails?
    let requiresOpenaiAuth: Bool?
}

private enum RPCAccountDetails: Decodable {
    case apiKey
    case chatgpt(email: String, planType: String)

    enum CodingKeys: String, CodingKey {
        case type
        case email
        case planType
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)
        switch type.lowercased() {
        case "apikey":
            self = .apiKey
        case "chatgpt":
            let email = try container.decodeIfPresent(String.self, forKey: .email) ?? "unknown"
            let plan = try container.decodeIfPresent(String.self, forKey: .planType) ?? "unknown"
            self = .chatgpt(email: email, planType: plan)
        default:
            throw DecodingError.dataCorruptedError(
                forKey: .type,
                in: container,
                debugDescription: "Unknown account type \(type)")
        }
    }
}

private struct RPCRateLimitsResponse: Decodable, Encodable {
    let rateLimits: RPCRateLimitSnapshot
    let rateLimitsByLimitId: [String: RPCRateLimitSnapshot]?

    enum CodingKeys: String, CodingKey {
        case rateLimits
        case rateLimitsByLimitId
        case rateLimitsByLimitIdSnake = "rate_limits_by_limit_id"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.rateLimits = try container.decode(RPCRateLimitSnapshot.self, forKey: .rateLimits)
        self.rateLimitsByLimitId = (try? container.decodeIfPresent(
            [String: RPCRateLimitSnapshot].self,
            forKey: .rateLimitsByLimitId))
            ?? (try? container.decodeIfPresent(
                [String: RPCRateLimitSnapshot].self,
                forKey: .rateLimitsByLimitIdSnake))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.rateLimits, forKey: .rateLimits)
        try container.encodeIfPresent(self.rateLimitsByLimitId, forKey: .rateLimitsByLimitId)
    }
}

private struct RPCRateLimitSnapshot: Decodable, Encodable {
    let limitId: String?
    let limitName: String?
    let primary: RPCRateLimitWindow?
    let secondary: RPCRateLimitWindow?
    let credits: RPCCreditsSnapshot?
    let individualLimit: RPCSpendControlLimitSnapshot?
    let planType: String?
    let rateLimitReachedType: String?

    enum CodingKeys: String, CodingKey {
        case limitId
        case limitIdSnake = "limit_id"
        case limitName
        case limitNameSnake = "limit_name"
        case primary
        case secondary
        case credits
        case individualLimit
        case individualLimitSnake = "individual_limit"
        case planType
        case planTypeSnake = "plan_type"
        case rateLimitReachedType
        case rateLimitReachedTypeSnake = "rate_limit_reached_type"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.limitId = (try? container.decodeIfPresent(String.self, forKey: .limitId))
            ?? (try? container.decodeIfPresent(String.self, forKey: .limitIdSnake))
        self.limitName = (try? container.decodeIfPresent(String.self, forKey: .limitName))
            ?? (try? container.decodeIfPresent(String.self, forKey: .limitNameSnake))
        self.primary = try? container.decodeIfPresent(RPCRateLimitWindow.self, forKey: .primary)
        self.secondary = try? container.decodeIfPresent(RPCRateLimitWindow.self, forKey: .secondary)
        self.credits = try? container.decodeIfPresent(RPCCreditsSnapshot.self, forKey: .credits)
        self.individualLimit = (try? container.decodeIfPresent(
            RPCSpendControlLimitSnapshot.self,
            forKey: .individualLimit))
            ?? (try? container.decodeIfPresent(RPCSpendControlLimitSnapshot.self, forKey: .individualLimitSnake))
        self.planType = (try? container.decodeIfPresent(String.self, forKey: .planType))
            ?? (try? container.decodeIfPresent(String.self, forKey: .planTypeSnake))
        self.rateLimitReachedType = (try? container.decodeIfPresent(String.self, forKey: .rateLimitReachedType))
            ?? (try? container.decodeIfPresent(String.self, forKey: .rateLimitReachedTypeSnake))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(self.limitId, forKey: .limitId)
        try container.encodeIfPresent(self.limitName, forKey: .limitName)
        try container.encodeIfPresent(self.primary, forKey: .primary)
        try container.encodeIfPresent(self.secondary, forKey: .secondary)
        try container.encodeIfPresent(self.credits, forKey: .credits)
        try container.encodeIfPresent(self.individualLimit, forKey: .individualLimit)
        try container.encodeIfPresent(self.planType, forKey: .planType)
        try container.encodeIfPresent(self.rateLimitReachedType, forKey: .rateLimitReachedType)
    }
}

private struct RPCRateLimitWindow: Decodable, Encodable {
    let usedPercent: Double
    let windowDurationMins: Int?
    let resetsAt: Int?
}

private struct RPCCreditsSnapshot: Decodable, Encodable {
    let hasCredits: Bool
    let unlimited: Bool
    let balance: String?
}

private struct RPCSpendControlLimitSnapshot: Decodable, Encodable {
    let limit: Double?
    let used: Double?
    let remainingPercent: Double?
    let resetsAt: Int?

    enum CodingKeys: String, CodingKey {
        case limit
        case used
        case remainingPercent
        case remainingPercentSnake = "remaining_percent"
        case resetsAt
        case resetsAtSnake = "resets_at"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.limit = Self.decodeFlexibleDouble(container, forKey: .limit)
        self.used = Self.decodeFlexibleDouble(container, forKey: .used)
        self.remainingPercent = Self.decodeFlexibleDouble(container, forKey: .remainingPercent)
            ?? Self.decodeFlexibleDouble(container, forKey: .remainingPercentSnake)
        self.resetsAt = Self.decodeFlexibleInt(container, forKey: .resetsAt)
            ?? Self.decodeFlexibleInt(container, forKey: .resetsAtSnake)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(self.limit, forKey: .limit)
        try container.encodeIfPresent(self.used, forKey: .used)
        try container.encodeIfPresent(self.remainingPercent, forKey: .remainingPercent)
        try container.encodeIfPresent(self.resetsAt, forKey: .resetsAt)
    }

    private static func decodeFlexibleDouble(
        _ container: KeyedDecodingContainer<CodingKeys>,
        forKey key: CodingKeys) -> Double?
    {
        if let value = try? container.decodeIfPresent(Double.self, forKey: key) {
            return value
        }
        if let value = try? container.decodeIfPresent(Int.self, forKey: key) {
            return Double(value)
        }
        if let value = try? container.decodeIfPresent(String.self, forKey: key) {
            return Double(value.trimmingCharacters(in: .whitespacesAndNewlines))
        }
        return nil
    }

    private static func decodeFlexibleInt(
        _ container: KeyedDecodingContainer<CodingKeys>,
        forKey key: CodingKeys) -> Int?
    {
        if let value = try? container.decodeIfPresent(Int.self, forKey: key) {
            return value
        }
        if let value = try? container.decodeIfPresent(Double.self, forKey: key) {
            return Int(value)
        }
        if let value = try? container.decodeIfPresent(String.self, forKey: key) {
            return Int(value.trimmingCharacters(in: .whitespacesAndNewlines))
        }
        return nil
    }
}

private struct RPCRateLimitsErrorBody: Decodable {
    let email: String?
    let planType: String?
    let rateLimit: CodexUsageResponse.RateLimitDetails?
    let credits: CodexUsageResponse.CreditDetails?

    enum CodingKeys: String, CodingKey {
        case email
        case planType = "plan_type"
        case rateLimit = "rate_limit"
        case credits
    }
}

enum RPCWireError: Error, LocalizedError {
    case startFailed(String)
    case requestFailed(String)
    case malformed(String)
    case timeout(method: String)

    var errorDescription: String? {
        switch self {
        case let .startFailed(message):
            "Codex not running. Try running a Codex command first. (\(message))"
        case let .requestFailed(message):
            "Codex connection failed: \(message)"
        case let .malformed(message):
            "Codex returned invalid data: \(message)"
        case let .timeout(method):
            "Codex RPC timed out waiting for `\(method)` reply."
        }
    }
}

/// RPC helper used on background tasks; safe because we confine it to the owning task.
private final class CodexRPCClient: @unchecked Sendable {
    // Provider-specific by design: Codex RPC owns its dedicated subprocess log category.
    private static let log = CodexBarLog.logger(LogCategories.provider(.codex, scope: "rpc"))
    private let process = Process()
    private let stdinPipe = Pipe()
    private let stdoutPipe = Pipe()
    private let stderrPipe = Pipe()
    private let stdoutLineStream: AsyncStream<Data>
    private let stdoutLineContinuation: AsyncStream<Data>.Continuation
    private var nextID = 1
    private let initializeTimeoutSeconds: TimeInterval
    private let requestTimeoutSeconds: TimeInterval

    private static func debugWriteStderr(_ message: String) {
        #if !os(Linux)
        fputs(message, stderr)
        #endif
    }

    init(
        executable: String = "codex", // Provider-specific by design: this RPC client launches Codex app-server.
        arguments: [String] = ["-s", "read-only", "-a", "untrusted", "app-server"],
        environment: [String: String] = ProcessInfo.processInfo.environment,
        initializeTimeoutSeconds: TimeInterval = 8.0,
        requestTimeoutSeconds: TimeInterval = 3.0,
        resolveExecutable: CodexExecutableResolver = defaultCodexExecutableResolver) throws
    {
        self.initializeTimeoutSeconds = initializeTimeoutSeconds
        self.requestTimeoutSeconds = requestTimeoutSeconds
        var stdoutContinuation: AsyncStream<Data>.Continuation!
        self.stdoutLineStream = AsyncStream<Data> { continuation in
            stdoutContinuation = continuation
        }
        self.stdoutLineContinuation = stdoutContinuation

        let resolution = resolveExecutable(environment, executable)

        guard let resolution else {
            Self.log.warning("Codex RPC binary not found", metadata: ["binary": executable])
            throw CodexStatusProbeError.codexNotInstalled
        }
        let resolvedExec = resolution.executable
        var env = environment
        let loginPATH = resolution.loginPATH ?? LoginShellPathCache.shared.current
        env["PATH"] = PathBuilder.effectivePATH(
            purposes: [.rpc, .nodeTooling],
            env: env,
            loginPATH: loginPATH)

        self.process.environment = env
        self.process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
        self.process.arguments = [resolvedExec] + arguments
        self.process.standardInput = self.stdinPipe
        self.process.standardOutput = self.stdoutPipe
        self.process.standardError = self.stderrPipe

        if let message = CodexCLILaunchGate.shared.backgroundSkipMessage(binary: resolvedExec) {
            Self.log.warning("Codex RPC launch skipped after recent launch failure", metadata: ["binary": resolvedExec])
            throw RPCWireError.startFailed(message)
        }

        do {
            try self.process.run()
            Self.log.debug("Codex RPC started", metadata: ["binary": resolvedExec])
        } catch {
            let message = error.localizedDescription
            let throttled = CodexCLILaunchGate.shared.recordLaunchFailure(binary: resolvedExec, message: message)
            Self.log.warning("Codex RPC failed to start", metadata: ["error": message])
            throw RPCWireError.startFailed(throttled ?? message)
        }

        let stdoutHandle = self.stdoutPipe.fileHandleForReading
        let stdoutLineContinuation = self.stdoutLineContinuation
        let stdoutBuffer = BoundedLineBuffer()
        let process = self.process
        stdoutHandle.readabilityHandler = { handle in
            let data = handle.availableData
            if data.isEmpty {
                handle.readabilityHandler = nil
                stdoutLineContinuation.finish()
                return
            }

            let result = stdoutBuffer.appendAndDrainLines(data)
            if result.didExceedLimit {
                Self.log.warning("Codex RPC line exceeded memory limit; terminating process")
                handle.readabilityHandler = nil
                process.terminate()
                stdoutLineContinuation.finish()
                return
            }

            for lineData in result.lines {
                stdoutLineContinuation.yield(lineData)
            }
        }

        let stderrHandle = self.stderrPipe.fileHandleForReading
        stderrHandle.readabilityHandler = { handle in
            let data = handle.availableData
            // When the child closes stderr, availableData returns empty and will keep re-firing; clear the handler
            // to avoid a busy read loop on the file-descriptor monitoring queue.
            if data.isEmpty {
                handle.readabilityHandler = nil
                return
            }
            guard let text = String(data: data, encoding: .utf8), !text.isEmpty else { return }
            for line in text.split(whereSeparator: \.isNewline) {
                Self.debugWriteStderr("[codex stderr] \(line)\n")
            }
        }
    }

    func initialize(clientName: String, clientVersion: String) async throws {
        _ = try await self.request(
            method: "initialize",
            params: ["clientInfo": ["name": clientName, "version": clientVersion]],
            timeout: self.initializeTimeoutSeconds)
        try self.sendNotification(method: "initialized")
    }

    func fetchAccount() async throws -> RPCAccountResponse {
        let message = try await self.request(method: "account/read")
        return try self.decodeResult(from: message)
    }

    func fetchRateLimits() async throws -> RPCRateLimitsResponse {
        let message = try await self.request(method: "account/rateLimits/read")
        return try self.decodeResult(from: message)
    }

    func shutdown() {
        if self.process.isRunning {
            Self.log.debug("Codex RPC stopping")
            self.process.terminate()
        }
    }

    // MARK: - JSON-RPC helpers

    private struct SendableJSONMessage: @unchecked Sendable {
        let value: [String: Any]
    }

    private func request(
        method: String,
        params: [String: Any]? = nil,
        timeout: TimeInterval? = nil) async throws -> [String: Any]
    {
        let id = self.nextID
        self.nextID += 1
        try self.sendRequest(id: id, method: method, params: params)

        let resolvedTimeout = timeout ?? self.requestTimeoutSeconds
        let wrapped = try await self.withTimeout(seconds: resolvedTimeout, method: method) {
            while true {
                let message = try await self.readNextMessage()

                if message["id"] == nil, let methodName = message["method"] as? String {
                    Self.debugWriteStderr("[codex notify] \(methodName)\n")
                    continue
                }

                guard let messageID = self.jsonID(message["id"]), messageID == id else { continue }

                if let error = message["error"] as? [String: Any], let messageText = error["message"] as? String {
                    throw RPCWireError.requestFailed(messageText)
                }

                return SendableJSONMessage(value: message)
            }
        }
        return wrapped.value
    }

    private func withTimeout<T: Sendable>(
        seconds: TimeInterval,
        method: String,
        body: @escaping @Sendable () async throws -> T) async throws -> T
    {
        try await withThrowingTaskGroup(of: T.self) { group in
            group.addTask {
                try await body()
            }
            group.addTask { [weak self] in
                try await Task.sleep(for: .seconds(seconds))
                self?.terminateProcessForTimeout(method: method)
                throw RPCWireError.timeout(method: method)
            }
            do {
                guard let result = try await group.next() else {
                    throw RPCWireError.timeout(method: method)
                }
                group.cancelAll()
                return result
            } catch {
                group.cancelAll()
                throw error
            }
        }
    }

    private func terminateProcessForTimeout(method: String) {
        if self.process.isRunning {
            Self.log.warning("Codex RPC timed out on `\(method)`; terminating process")
            self.process.terminate()
        }
    }

    private func sendNotification(method: String, params: [String: Any]? = nil) throws {
        let paramsValue: Any = params ?? [:]
        try self.sendPayload(["method": method, "params": paramsValue])
    }

    private func sendRequest(id: Int, method: String, params: [String: Any]?) throws {
        let paramsValue: Any = params ?? [:]
        let payload: [String: Any] = ["id": id, "method": method, "params": paramsValue]
        try self.sendPayload(payload)
    }

    private func sendPayload(_ payload: [String: Any]) throws {
        let data = try JSONSerialization.data(withJSONObject: payload)
        self.stdinPipe.fileHandleForWriting.write(data)
        self.stdinPipe.fileHandleForWriting.write(Data([0x0A]))
    }

    private func readNextMessage() async throws -> [String: Any] {
        for await lineData in self.stdoutLineStream {
            if lineData.isEmpty {
                continue
            }
            if let json = try? JSONSerialization.jsonObject(with: lineData) as? [String: Any] {
                return json
            }
        }
        throw RPCWireError.malformed("codex app-server closed stdout")
    }

    private func decodeResult<T: Decodable>(from message: [String: Any]) throws -> T {
        guard let result = message["result"] else {
            throw RPCWireError.malformed("missing result field")
        }
        let data = try JSONSerialization.data(withJSONObject: result)
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    }

    private func jsonID(_ value: Any?) -> Int? {
        switch value {
        case let int as Int:
            int
        case let number as NSNumber:
            number.intValue
        default:
            nil
        }
    }
}

// MARK: - Public fetcher used by the app

public struct UsageFetcher: Sendable {
    private let environment: [String: String]
    private let initializeTimeoutSeconds: TimeInterval
    private let requestTimeoutSeconds: TimeInterval
    private let codexExecutableResolver: CodexExecutableResolver
    private let codexArguments: [String]

    public init(environment: [String: String] = ProcessInfo.processInfo.environment) {
        self.environment = environment
        self.initializeTimeoutSeconds = 8.0
        self.requestTimeoutSeconds = 3.0
        self.codexExecutableResolver = defaultCodexExecutableResolver
        self.codexArguments = ["-s", "read-only", "-a", "untrusted", "app-server"]
    }

    init(
        environment: [String: String],
        initializeTimeoutSeconds: TimeInterval,
        requestTimeoutSeconds: TimeInterval,
        codexArguments: [String] = ["-s", "read-only", "-a", "untrusted", "app-server"],
        codexExecutableResolver: @escaping CodexExecutableResolver = defaultCodexExecutableResolver)
    {
        self.environment = environment
        self.initializeTimeoutSeconds = initializeTimeoutSeconds
        self.requestTimeoutSeconds = requestTimeoutSeconds
        self.codexExecutableResolver = codexExecutableResolver
        self.codexArguments = codexArguments
    }

    public func loadLatestUsage(keepCLISessionsAlive: Bool = false) async throws -> UsageSnapshot {
        _ = keepCLISessionsAlive
        guard let usage = try await self.loadLatestCLIAccountSnapshot().usage else {
            throw UsageError.noRateLimitsFound
        }
        return usage
    }

    public func loadLatestCLIAccountSnapshot() async throws -> CodexCLIAccountSnapshot {
        let rpc = try CodexRPCClient(
            arguments: self.codexArguments,
            environment: self.environment,
            initializeTimeoutSeconds: self.initializeTimeoutSeconds,
            requestTimeoutSeconds: self.requestTimeoutSeconds,
            resolveExecutable: self.codexExecutableResolver)
        defer { rpc.shutdown() }
        do {
            try await rpc.initialize(clientName: "codexbar", clientVersion: "0.5.4")
            // The app-server answers on a single stdout stream, so keep requests
            // serialized to avoid starving one reader when multiple awaiters race
            // for the same pipe.
            let limitsResponse = try await rpc.fetchRateLimits()
            let limits = limitsResponse.rateLimits
            let account = try? await rpc.fetchAccount()
            let rateLimitsPlan = Self.normalizedCodexAccountField(limits.planType)
            // Provider-specific by design: Codex app-server responses construct Codex reconciled identity.
            let identity = ProviderIdentitySnapshot(
                providerID: .codex,
                accountEmail: account?.account.flatMap { details in
                    if case let .chatgpt(email, _) = details {
                        email
                    } else {
                        nil
                    }
                },
                accountOrganization: nil,
                loginMethod: account?.account.flatMap { details in
                    if case let .chatgpt(_, plan) = details {
                        plan
                    } else {
                        nil
                    }
                } ?? rateLimitsPlan)
            let credits = Self.makeCredits(from: limits, rateLimitsByLimitId: limitsResponse.rateLimitsByLimitId)
            let shouldReturnUnavailableUsage = credits == nil || rateLimitsPlan != nil
            let usage = CodexReconciledState.fromCLI(
                primary: Self.makeWindow(from: limits.primary),
                secondary: Self.makeWindow(from: limits.secondary),
                identity: identity)?
                .toUsageSnapshot()
                ?? (shouldReturnUnavailableUsage ? Self.emptyCodexUsageSnapshotIfIdentified(identity: identity) : nil)
            guard usage != nil || credits != nil else {
                throw UsageError.noRateLimitsFound
            }
            return CodexCLIAccountSnapshot(
                usage: usage,
                credits: credits,
                identity: identity)
        } catch {
            let usage = Self.recoverUsageFromRPCError(error)
            let credits = Self.recoverCreditsFromRPCError(error)
            if usage != nil || credits != nil {
                return CodexCLIAccountSnapshot(
                    usage: usage,
                    credits: credits,
                    identity: usage?.identity)
            }
            throw error
        }
    }

    public func loadLatestCredits(keepCLISessionsAlive: Bool = false) async throws -> CreditsSnapshot {
        _ = keepCLISessionsAlive
        guard let credits = try await self.loadLatestCLIAccountSnapshot().credits else {
            throw UsageError.noRateLimitsFound
        }
        return credits
    }

    public func debugRawRateLimits() async -> String {
        do {
            let rpc = try CodexRPCClient(
                arguments: self.codexArguments,
                environment: self.environment,
                initializeTimeoutSeconds: self.initializeTimeoutSeconds,
                requestTimeoutSeconds: self.requestTimeoutSeconds,
                resolveExecutable: self.codexExecutableResolver)
            defer { rpc.shutdown() }
            try await rpc.initialize(clientName: "codexbar", clientVersion: "0.5.4")
            let limits = try await rpc.fetchRateLimits()
            let data = try JSONEncoder().encode(limits)
            return String(data: data, encoding: .utf8) ?? "<unprintable>"
        } catch {
            return "Codex RPC probe failed: \(error)"
        }
    }

    public func loadAccountInfo() -> AccountInfo {
        let account = self.loadAuthBackedCodexAccount()
        return AccountInfo(email: account.email, plan: account.plan)
    }

    public func loadAuthBackedCodexAccount() -> CodexAuthBackedAccount {
        guard let credentials = try? CodexOAuthCredentialsStore.load(env: self.environment) else {
            return CodexAuthBackedAccount(identity: .unresolved, email: nil, plan: nil)
        }

        let payload = credentials.idToken.flatMap(Self.parseJWT)
        let authDict = payload?["https://api.openai.com/auth"] as? [String: Any]
        let profileDict = payload?["https://api.openai.com/profile"] as? [String: Any]

        let email = Self.normalizedCodexAccountField(
            (payload?["email"] as? String) ?? (profileDict?["email"] as? String))
        let plan = Self.normalizedCodexAccountField(
            (authDict?["chatgpt_plan_type"] as? String) ?? (payload?["chatgpt_plan_type"] as? String))
        let accountId = Self.normalizedCodexAccountField(
            credentials.accountId
                ?? (authDict?["chatgpt_account_id"] as? String)
                ?? (payload?["chatgpt_account_id"] as? String))
        let identity = CodexIdentityResolver.resolve(accountId: accountId, email: email)

        return CodexAuthBackedAccount(identity: identity, email: email, plan: plan)
    }

    // MARK: - Helpers

    private static func makeWindow(from rpc: RPCRateLimitWindow?) -> RateWindow? {
        guard let rpc else { return nil }
        let resetsAtDate = rpc.resetsAt.map { Date(timeIntervalSince1970: TimeInterval($0)) }
        let resetDescription = resetsAtDate.map { UsageFormatter.resetDescription(from: $0) }
        return RateWindow(
            usedPercent: rpc.usedPercent,
            windowMinutes: rpc.windowDurationMins,
            resetsAt: resetsAtDate,
            resetDescription: resetDescription)
    }

    private static func makeWindow(from response: CodexUsageResponse.WindowSnapshot?) -> RateWindow? {
        guard let response else { return nil }
        let resetsAtDate = Date(timeIntervalSince1970: TimeInterval(response.resetAt))
        return RateWindow(
            usedPercent: Double(response.usedPercent),
            windowMinutes: response.limitWindowSeconds / 60,
            resetsAt: resetsAtDate,
            resetDescription: UsageFormatter.resetDescription(from: resetsAtDate))
    }

    private static func makeTTYWindow(
        percentLeft: Int?,
        windowMinutes: Int,
        resetsAt: Date?,
        resetDescription: String?) -> RateWindow?
    {
        guard let percentLeft else { return nil }
        return RateWindow(
            usedPercent: max(0, 100 - Double(percentLeft)),
            windowMinutes: windowMinutes,
            resetsAt: resetsAt,
            resetDescription: resetDescription)
    }

    private static func parseCredits(_ balance: String?) -> Double {
        guard let balance, let val = Double(balance) else { return 0 }
        return val
    }

    private static func makeCredits(
        from limits: RPCRateLimitSnapshot,
        rateLimitsByLimitId: [String: RPCRateLimitSnapshot]? = nil) -> CreditsSnapshot?
    {
        let updatedAt = Date()
        let balance = limits.credits.map { self.parseCredits($0.balance) }
        let creditLimit = self.codexCreditLimit(
            from: limits,
            rateLimitsByLimitId: rateLimitsByLimitId,
            updatedAt: updatedAt)
        guard balance != nil || creditLimit != nil else { return nil }
        return CreditsSnapshot(
            remaining: balance ?? 0,
            events: [],
            updatedAt: updatedAt,
            codexCreditLimit: creditLimit)
    }

    private static func codexCreditLimit(
        from limits: RPCRateLimitSnapshot,
        rateLimitsByLimitId: [String: RPCRateLimitSnapshot]?,
        updatedAt: Date) -> CodexCreditLimitSnapshot?
    {
        let candidates = [limits] + (rateLimitsByLimitId?.values.sorted {
            ($0.limitName ?? $0.limitId ?? "") < ($1.limitName ?? $1.limitId ?? "")
        } ?? [])
        for candidate in candidates {
            if let limit = self.codexCreditLimit(from: candidate, updatedAt: updatedAt) {
                return limit
            }
        }
        return nil
    }

    private static func codexCreditLimit(
        from snapshot: RPCRateLimitSnapshot,
        updatedAt: Date) -> CodexCreditLimitSnapshot?
    {
        guard let individualLimit = snapshot.individualLimit else { return nil }
        guard let limit = individualLimit.limit, limit > 0 else { return nil }
        let used: Double = if let used = individualLimit.used {
            used
        } else if let remainingPercent = individualLimit.remainingPercent {
            limit * max(0, min(100, 100 - remainingPercent)) / 100
        } else {
            0
        }
        let remainingPercent = individualLimit.remainingPercent ?? max(0, min(100, 100 - (used / limit * 100)))
        let resetsAt = individualLimit.resetsAt.flatMap { value -> Date? in
            guard value > 0 else { return nil }
            return Date(timeIntervalSince1970: TimeInterval(value))
        }
        return CodexCreditLimitSnapshot(
            title: self.codexCreditLimitTitle(from: snapshot.limitName),
            used: used,
            limit: limit,
            remainingPercent: remainingPercent,
            resetsAt: resetsAt,
            updatedAt: updatedAt)
    }

    private static func codexCreditLimitTitle(from limitName: String?) -> String {
        let trimmed = limitName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        if trimmed.isEmpty {
            return "Monthly credit limit"
        }
        return trimmed
    }

    private static func emptyCodexUsageSnapshotIfIdentified(identity: ProviderIdentitySnapshot) -> UsageSnapshot? {
        guard identity.accountEmail != nil || identity.loginMethod != nil else { return nil }
        return UsageSnapshot(
            primary: nil,
            secondary: nil,
            tertiary: nil,
            updatedAt: Date(),
            identity: identity)
    }

    private static func recoverUsageFromRPCError(_ error: Error) -> UsageSnapshot? {
        // Provider-specific by design: Codex RPC error bodies can still carry authoritative rate-limit payloads.
        guard let body = self.decodeRateLimitsErrorBody(from: error) else { return nil }
        let identity = ProviderIdentitySnapshot(
            providerID: .codex,
            accountEmail: self.normalizedCodexAccountField(body.email),
            accountOrganization: nil,
            loginMethod: self.normalizedCodexAccountField(body.planType))
        guard let state = CodexReconciledState.fromCLI(
            primary: self.makeWindow(from: body.rateLimit?.primaryWindow),
            secondary: self.makeWindow(from: body.rateLimit?.secondaryWindow),
            identity: identity)
        else {
            return nil
        }
        if body.rateLimit?.hasWindowDecodeFailure == true,
           state.session == nil
        {
            return nil
        }
        return state.toUsageSnapshot()
    }

    private static func recoverCreditsFromRPCError(_ error: Error) -> CreditsSnapshot? {
        guard let credits = self.decodeRateLimitsErrorBody(from: error)?.credits else { return nil }
        guard let remaining = credits.balance else { return nil }
        return CreditsSnapshot(remaining: remaining, events: [], updatedAt: Date())
    }

    private static func decodeRateLimitsErrorBody(from error: Error) -> RPCRateLimitsErrorBody? {
        guard case let RPCWireError.requestFailed(message) = error else { return nil }
        guard let json = self.extractJSONObject(after: "body=", in: message) else { return nil }
        guard let data = json.data(using: .utf8) else { return nil }
        return try? JSONDecoder().decode(RPCRateLimitsErrorBody.self, from: data)
    }

    private static func extractJSONObject(after marker: String, in text: String) -> String? {
        guard let markerRange = text.range(of: marker) else { return nil }
        let suffix = text[markerRange.upperBound...]
        guard let start = suffix.firstIndex(of: "{") else { return nil }

        var depth = 0
        var inString = false
        var isEscaped = false

        for index in suffix[start...].indices {
            let character = suffix[index]

            if inString {
                if isEscaped {
                    isEscaped = false
                } else if character == "\\" {
                    isEscaped = true
                } else if character == "\"" {
                    inString = false
                }
                continue
            }

            switch character {
            case "\"":
                inString = true
            case "{":
                depth += 1
            case "}":
                depth -= 1
                if depth == 0 {
                    return String(suffix[start...index])
                }
            default:
                break
            }
        }

        return nil
    }

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

    public static func parseJWT(_ token: String) -> [String: Any]? {
        let parts = token.split(separator: ".")
        guard parts.count >= 2 else { return nil }
        let payloadPart = parts[1]

        var padded = String(payloadPart)
            .replacingOccurrences(of: "-", with: "+")
            .replacingOccurrences(of: "_", with: "/")
        while padded.count % 4 != 0 {
            padded.append("=")
        }
        guard let data = Data(base64Encoded: padded) else { return nil }
        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }
        return json
    }
}

#if DEBUG
extension UsageFetcher {
    static func _mapCodexRPCLimitsForTesting(
        primary: (usedPercent: Double, windowMinutes: Int, resetsAt: Int?)?,
        secondary: (usedPercent: Double, windowMinutes: Int, resetsAt: Int?)?,
        planType: String? = nil) throws -> UsageSnapshot
    {
        let identity = ProviderIdentitySnapshot(
            providerID: .codex,
            accountEmail: nil,
            accountOrganization: nil,
            loginMethod: self.normalizedCodexAccountField(planType))
        guard let state = CodexReconciledState.fromCLI(
            primary: primary.map(self.makeTestingWindow),
            secondary: secondary.map(self.makeTestingWindow),
            identity: identity)
        else {
            if let usage = self.emptyCodexUsageSnapshotIfIdentified(identity: identity) {
                return usage
            }
            throw UsageError.noRateLimitsFound
        }
        return state.toUsageSnapshot()
    }

    static func _mapCodexStatusForTesting(_ status: CodexStatusSnapshot) throws -> UsageSnapshot {
        guard let state = CodexReconciledState.fromCLI(
            primary: self.makeTTYWindow(
                percentLeft: status.fiveHourPercentLeft,
                windowMinutes: 300,
                resetsAt: status.fiveHourResetsAt,
                resetDescription: status.fiveHourResetDescription),
            secondary: self.makeTTYWindow(
                percentLeft: status.weeklyPercentLeft,
                windowMinutes: 10080,
                resetsAt: status.weeklyResetsAt,
                resetDescription: status.weeklyResetDescription),
            identity: nil)
        else {
            throw UsageError.noRateLimitsFound
        }
        return state.toUsageSnapshot()
    }

    public static func _recoverCodexRPCUsageFromErrorForTesting(_ message: String) -> UsageSnapshot? {
        self.recoverUsageFromRPCError(RPCWireError.requestFailed(message))
    }

    public static func _recoverCodexRPCCreditsFromErrorForTesting(_ message: String) -> CreditsSnapshot? {
        self.recoverCreditsFromRPCError(RPCWireError.requestFailed(message))
    }

    private static func makeTestingWindow(
        _ value: (usedPercent: Double, windowMinutes: Int, resetsAt: Int?))
        -> RateWindow
    {
        let resetsAt = value.resetsAt.map { Date(timeIntervalSince1970: TimeInterval($0)) }
        return RateWindow(
            usedPercent: value.usedPercent,
            windowMinutes: value.windowMinutes,
            resetsAt: resetsAt,
            resetDescription: resetsAt.map { UsageFormatter.resetDescription(from: $0) })
    }
}
#endif
