import Foundation

/// One Codex (ChatGPT-subscription) rate-limit window — the shape the `codex` app-server
/// pushes in its `account/rateLimits/updated` notification: a percentage consumed plus the
/// window's length and when it refreshes. Codex reports two windows — a short rolling one
/// (`primary`, typically ~5 hours) and a long one (`secondary`, typically weekly) — so this
/// is the Codex counterpart to Claude's `UsageWindow`.
public struct CodexUsageWindow: Sendable, Codable, Equatable {
    /// Percent of the window consumed, 0–100.
    public let utilization: Double
    /// The window's length in minutes, when reported. Used only to label the row ("5-hour"
    /// vs "Weekly"); the percentage and reset are what actually render.
    public let windowMinutes: Int?
    /// When this window rolls over and frees capacity (nil if not reported). Computed from
    /// codex's *relative* `resetsInSeconds` at the moment the snapshot was decoded, so it
    /// survives the wire and the periodic re-render as an absolute instant.
    public let resetsAt: Date?

    public init(utilization: Double, windowMinutes: Int?, resetsAt: Date?) {
        self.utilization = utilization
        self.windowMinutes = windowMinutes
        self.resetsAt = resetsAt
    }

    /// The utilization to show at `now`, correcting for a stale snapshot across a reset — the
    /// same coarse-poll correction `UsageWindow` makes. Codex only pushes a fresh snapshot
    /// during a turn, so between turns a lapsed window would otherwise report its stale
    /// pre-reset percentage; once `resetsAt` has passed it reads as empty (0%) instead.
    public func utilization(at now: Date) -> Double {
        if let resetsAt, now >= resetsAt { return 0 }
        return utilization
    }

    /// A short human label derived from the window's length: sub-day windows read as "N-hour",
    /// a ~week reads "Weekly", other multiples of a day read "N-day". `nil` when codex omits the
    /// length, so the caller can fall back to a positional default ("5-hour" / "Weekly").
    public var label: String? {
        guard let windowMinutes, windowMinutes > 0 else { return nil }
        if windowMinutes % (60 * 24) == 0 {
            let days = windowMinutes / (60 * 24)
            return days == 7 ? "Weekly" : "\(days)-day"
        }
        let hours = windowMinutes / 60
        return hours >= 1 ? "\(hours)-hour" : "\(windowMinutes)-min"
    }
}

/// Codex subscription usage — the two rolling rate-limit windows the `codex` app-server
/// reports for a ChatGPT-authenticated account, the Codex counterpart to Claude's
/// `SubscriptionUsage`. Unlike Claude's (polled from an OAuth usage endpoint), these arrive
/// as a push notification (`account/rateLimits/updated`) during a turn, so the newest one
/// seen is the freshest available (`AppStore.codexUsage`). Codable so it rides both the
/// normalized `AgentEvent` stream and `DashboardSnapshot` to paired phones unchanged.
public struct CodexUsage: Sendable, Codable, Equatable {
    /// The short rolling window (typically ~5 hours).
    public let primary: CodexUsageWindow?
    /// The long rolling window (typically weekly).
    public let secondary: CodexUsageWindow?

    public init(primary: CodexUsageWindow?, secondary: CodexUsageWindow?) {
        self.primary = primary
        self.secondary = secondary
    }

    /// The most-constrained window's utilization — the single headline number, since hitting
    /// *either* window throttles you.
    public var peakUtilization: Double? {
        [primary, secondary].compactMap { $0?.utilization }.max()
    }

    /// True when at least one window carries a real reading — lets the UI choose between the
    /// live card and the "no data yet" placeholder.
    public var hasData: Bool { primary != nil || secondary != nil }
}

extension CodexUsage {
    /// Parse the `params` of a codex `account/rateLimits/updated` notification into a snapshot.
    ///
    /// Deliberately lenient about field naming (camelCase from the app-server, snake_case from
    /// the exec/core wire) and missing fields, since the shape is codex-internal and unversioned:
    /// returns `nil` only when *no* window can be read at all, so a malformed or empty snapshot
    /// never displaces a good one already on screen. `now` anchors each window's relative
    /// `resetsInSeconds` to an absolute `resetsAt` (injectable for tests).
    public static func fromRateLimitsNotification(_ params: JSONValue, now: Date) -> CodexUsage? {
        // The two windows sit under `rateLimits` (app-server) / `rate_limits` (core), or,
        // tolerantly, at the params root.
        let root = params["rateLimits"] ?? params["rate_limits"] ?? params
        let primary = window(root["primary"], now: now)
        let secondary = window(root["secondary"], now: now)
        guard primary != nil || secondary != nil else { return nil }
        return CodexUsage(primary: primary, secondary: secondary)
    }

    /// One window object → `CodexUsageWindow`; `nil` unless it carries a usable percentage.
    private static func window(_ value: JSONValue?, now: Date) -> CodexUsageWindow? {
        guard let value, value.objectValue != nil,
              let used = (value["usedPercent"] ?? value["used_percent"])?.numberValue
        else { return nil }
        let minutes = (value["windowMinutes"] ?? value["window_minutes"])?.intValue
        let resetsAt = (value["resetsInSeconds"] ?? value["resets_in_seconds"])?.numberValue
            .map { now.addingTimeInterval($0) }
        return CodexUsageWindow(
            utilization: min(100, max(0, used)), windowMinutes: minutes, resetsAt: resetsAt)
    }
}
