import Foundation

// The pure newest-wins comparators for the two rotating agent logins, shared by every mesh
// member that reconciles them: the Mac's Keychain hub (`ClaudeLoginKeychain`), the host-file
// hub (`CodexAuthFile`), and the phone vault (iOS links only NucleicProtocol, so the logic
// must live here — the phone lands `credentialUpdate`s with the SAME rules, never a parallel
// reimplementation). I/O-free on purpose: callers own where the JSON comes from.

/// The Claude subscription login (`.credentials.json` / the Keychain OAuth blob). Freshness is
/// `claudeAiOauth.expiresAt` — a fresher login/refresh always carries a later expiry.
public enum ClaudeCredentialFormat {
    /// The `claudeAiOauth.expiresAt` (milliseconds since epoch) from a credentials JSON string,
    /// or nil if absent/unparseable. Tolerates a JSON number or a numeric string.
    public static func expiresAt(_ json: String) -> Double? {
        guard let oauth = oauthObject(json) else { return nil }
        if let n = oauth["expiresAt"] as? Double { return n }
        if let n = oauth["expiresAt"] as? Int { return Double(n) }
        if let s = oauth["expiresAt"] as? String, let n = Double(s) { return n }
        return nil
    }

    /// Whether `json` is a usable OAuth login: a `claudeAiOauth` object carrying a non-empty
    /// `accessToken` and `refreshToken`. Guards reconciliation from ever adopting a truncated
    /// or half-written credential.
    public static func isValidLogin(_ json: String) -> Bool {
        guard let oauth = oauthObject(json),
            let access = oauth["accessToken"] as? String, !access.isEmpty,
            let refresh = oauth["refreshToken"] as? String, !refresh.isEmpty
        else { return false }
        return true
    }

    /// Whether `candidate` should replace `current`: a valid candidate is adopted when there is
    /// no current value; otherwise it must be strictly newer by `expiresAt`, and an unparseable
    /// existing value is never overwritten. Identical bytes are a no-op.
    public static func shouldReplace(candidate: String, current: String?) -> Bool {
        guard isValidLogin(candidate), let candExp = expiresAt(candidate) else { return false }
        guard let current else { return true }
        guard current != candidate, let curExp = expiresAt(current) else { return false }
        return candExp > curExp
    }

    static func oauthObject(_ json: String) -> [String: Any]? {
        guard let data = json.data(using: .utf8),
            let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
            let oauth = obj["claudeAiOauth"] as? [String: Any]
        else { return nil }
        return oauth
    }
}

/// The Codex ChatGPT login (`~/.codex/auth.json`). Freshness is `last_refresh` — the client
/// rotates the refresh token and stamps the rotation time, so strictly-later wins.
public enum CodexCredentialFormat {
    /// The `last_refresh` timestamp (seconds since epoch) from an `auth.json` string, or nil if
    /// absent/unparseable. Codex writes it as an ISO-8601 UTC string, with or without
    /// fractional seconds; both forms parse.
    public static func lastRefresh(_ json: String) -> Double? {
        guard let obj = object(json), let stamp = obj["last_refresh"] as? String else { return nil }
        return date(from: stamp)?.timeIntervalSince1970
    }

    /// Whether `json` is a usable Codex ChatGPT login: a `tokens` object carrying a non-empty
    /// `refresh_token`. Guards the sync from propagating a truncated file or an API-key-only
    /// `auth.json` (no refresh lineage to reconcile).
    public static func isValidLogin(_ json: String) -> Bool {
        guard let obj = object(json),
            let tokens = obj["tokens"] as? [String: Any],
            let refresh = tokens["refresh_token"] as? String, !refresh.isEmpty
        else { return false }
        return true
    }

    /// Whether `candidate` should replace `current`: a valid, timestamped candidate seeds a
    /// missing destination; an unparseable `current` is treated as stale (a valid candidate
    /// wins); otherwise the strictly-later `last_refresh` wins. Identical bytes are a no-op.
    public static func shouldReplace(candidate: String, current: String?) -> Bool {
        guard isValidLogin(candidate), let cand = lastRefresh(candidate) else { return false }
        guard let current else { return true }
        guard current != candidate else { return false }
        guard let cur = lastRefresh(current) else { return true }
        return cand > cur
    }

    private static func object(_ json: String) -> [String: Any]? {
        guard let data = json.data(using: .utf8),
            let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        else { return nil }
        return obj
    }

    /// Parse an ISO-8601 UTC instant, tolerating the optional fractional seconds Codex emits.
    private static func date(from stamp: String) -> Date? {
        let withFraction = ISO8601DateFormatter()
        withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        if let d = withFraction.date(from: stamp) { return d }
        let plain = ISO8601DateFormatter()
        plain.formatOptions = [.withInternetDateTime]
        return plain.date(from: stamp)
    }
}
