import Foundation
import Security

/// One subscription rate-limit window from the usage endpoint.
public struct UsageWindow: Sendable, Equatable {
    /// Percent of the window consumed, 0–100.
    public let utilization: Double
    /// When this window rolls over and frees capacity (nil if the server omits it).
    public let resetsAt: Date?

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

/// Quantitative Claude **subscription** usage (Pro/Max) — the same numbers the
/// `claude` CLI's `/usage` screen shows: how much of your rolling 5-hour and weekly
/// windows you've consumed. This is plan usage, *not* API billing.
///
/// Sourced from the undocumented `GET /api/oauth/usage` endpoint, authenticated with
/// the OAuth token Claude Code stores in the login keychain. Both the endpoint and the
/// keychain layout are internal to Claude Code and may change without notice, so every
/// caller must tolerate `SubscriptionUsageFetcher.fetch()` throwing and fall back to
/// the coarser `rate_limit_event` status (see `AppStore.latestRateLimit`).
public struct SubscriptionUsage: Sendable, Equatable {
    /// The rolling 5-hour ("session") limit.
    public let fiveHour: UsageWindow?
    /// The rolling 7-day ("weekly") limit, across all models.
    public let sevenDay: UsageWindow?
    /// The weekly Opus sub-limit (nil when the plan has no separate Opus cap).
    public let sevenDayOpus: UsageWindow?
    /// The weekly Sonnet sub-limit.
    public let sevenDaySonnet: UsageWindow?

    public init(
        fiveHour: UsageWindow?, sevenDay: UsageWindow?,
        sevenDayOpus: UsageWindow?, sevenDaySonnet: UsageWindow?
    ) {
        self.fiveHour = fiveHour
        self.sevenDay = sevenDay
        self.sevenDayOpus = sevenDayOpus
        self.sevenDaySonnet = sevenDaySonnet
    }

    /// The most-constrained window's utilization — what a single headline indicator
    /// should track, since hitting *any* window throttles you.
    public var peakUtilization: Double? {
        [fiveHour, sevenDay, sevenDayOpus, sevenDaySonnet]
            .compactMap { $0?.utilization }.max()
    }
}

public enum SubscriptionUsageError: Error, Sendable {
    /// No Claude Code OAuth token in the keychain (logged out, or using an API key).
    case noToken
    case badResponse
    case http(Int)
}

/// Fetches `SubscriptionUsage` from Anthropic's OAuth usage endpoint. `nonisolated`
/// and stateless: safe to call from the main actor — the network await runs off-main.
public enum SubscriptionUsageFetcher {
    /// The keychain generic-password item Claude Code writes its OAuth credentials to.
    static let keychainService = "Claude Code-credentials"
    private static let endpoint = URL(string: "https://api.anthropic.com/api/oauth/usage")!
    /// The OAuth beta the CLI sends on this endpoint (mirrors claude 2.1.x).
    private static let oauthBeta = "oauth-2025-04-20"

    public static func fetch() async throws -> SubscriptionUsage {
        guard let token = oauthToken() else { throw SubscriptionUsageError.noToken }
        var request = URLRequest(url: endpoint)
        request.timeoutInterval = 10
        request.httpMethod = "GET"
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(oauthBeta, forHTTPHeaderField: "anthropic-beta")
        request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")

        let (data, response) = try await URLSession.shared.data(for: request)
        guard let http = response as? HTTPURLResponse else { throw SubscriptionUsageError.badResponse }
        guard http.statusCode == 200 else { throw SubscriptionUsageError.http(http.statusCode) }
        return try parse(data)
    }

    /// Reads `claudeAiOauth.accessToken` from Claude Code's keychain item. Reading an
    /// item another app created may prompt the user for keychain access the first time.
    static func oauthToken() -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: keychainService,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne,
        ]
        var item: CFTypeRef?
        guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
              let data = item as? Data,
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let oauth = json["claudeAiOauth"] as? [String: Any],
              let token = oauth["accessToken"] as? String
        else { return nil }
        return token
    }

    static func parse(_ data: Data) throws -> SubscriptionUsage {
        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw SubscriptionUsageError.badResponse
        }
        func window(_ key: String) -> UsageWindow? {
            guard let obj = json[key] as? [String: Any],
                  let util = obj["utilization"] as? Double else { return nil }
            let resets = (obj["resets_at"] as? String).flatMap(parseDate)
            return UsageWindow(utilization: util, resetsAt: resets)
        }
        return SubscriptionUsage(
            fiveHour: window("five_hour"),
            sevenDay: window("seven_day"),
            sevenDayOpus: window("seven_day_opus"),
            sevenDaySonnet: window("seven_day_sonnet"))
    }

    /// The endpoint stamps microsecond fractional seconds (`…851919+00:00`); try the
    /// fractional-seconds parser first, then fall back to whole seconds.
    private static func parseDate(_ string: String) -> Date? {
        let withFraction = ISO8601DateFormatter()
        withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        if let date = withFraction.date(from: string) { return date }
        let plain = ISO8601DateFormatter()
        plain.formatOptions = [.withInternetDateTime]
        return plain.date(from: string)
    }
}
