import Foundation
import NucleicProtocol

/// Nucleic's own Claude subscription credential store.
///
/// Nucleic mediates the OAuth flow itself (``ClaudeOAuth`` via ``ClaudeCredentialBroker``): sign-in
/// and refresh both write the rotated result into *Nucleic's own* Keychain item with newest-wins
/// reconciliation, and the broker briefly exports a 0600 `.credentials.json` into a session home
/// only to seed the container, then removes it.
///
/// Nucleic reads and writes ONLY its own item (`xyz.blakeslee.nucleic.credentials`). It never reads
/// Claude Code's `Claude Code-credentials` — or any other application's — Keychain item: that would
/// raise a cross-application authorization prompt and would mean depending on a credential Nucleic
/// didn't mint. The login it needs is always the one it minted, so no external adoption is required.
public enum ClaudeLoginKeychain {
    #if canImport(Security)
    private static let ownedService = "xyz.blakeslee.nucleic.credentials"
    private static let ownedAccount = "claude-oauth"
    #endif

    /// Serializes the read-compare-write across sessions: every `SessionController` lives in one
    /// process, so an in-process lock is enough to keep two turns from racing the same update.
    private static let lock = NSLock()

    /// Kill switch for credential persistence. Tests set this before exercising session code so
    /// they never inspect or mutate the developer's real login; CI may also set
    /// `NUCLEIC_DISABLE_LOGIN_SYNC=1`.
    /// `nonisolated(unsafe)`: written once, before any session runs, then only read.
    public nonisolated(unsafe) static var disabled: Bool =
        ProcessInfo.processInfo.environment["NUCLEIC_DISABLE_LOGIN_SYNC"] == "1"

    // MARK: - Reconciliation (write-back)

    /// Adopt a valid container login when Nucleic has none, or write it back when it is strictly
    /// newer. This captures first-time sandbox login as well as access/refresh-token rotation.
    public static func writeBackIfNewer(fromContainerFile path: String) {
        guard !disabled else { return }
        guard let fileJSON = try? String(contentsOfFile: path, encoding: .utf8) else { return }
        lock.lock()
        defer { lock.unlock() }
        let current = read()
        guard shouldWriteBack(candidate: fileJSON, current: current) else { return }
        write(fileJSON)
    }

    /// Whether `candidate` should replace Nucleic's current credential. A valid container login is
    /// adopted when Nucleic has none (for example, after the user logs in inside a fresh sandbox).
    /// Otherwise the candidate must be strictly newer by `expiresAt`; an unparseable existing value
    /// is never overwritten. The comparator itself lives in NucleicProtocol
    /// (`ClaudeCredentialFormat`) so the phone vault reconciles with the exact same rule.
    public static func shouldWriteBack(candidate: String, current: String?) -> Bool {
        ClaudeCredentialFormat.shouldReplace(candidate: candidate, current: current)
    }

    // MARK: - Reconciliation (export)

    /// Whether Nucleic's login should overwrite an existing sandbox `.credentials.json`. A sandbox
    /// can hold the newest rotated refresh-token lineage after a crash, so a strictly newer valid
    /// file is preserved; missing or invalid files are repaired from the owned value.
    public static func shouldExport(keychain: String, toExistingFile existing: String?) -> Bool {
        guard let existing, isValidLogin(existing), let fileExp = expiresAt(existing) else { return true }
        guard let keyExp = expiresAt(keychain) else { return true }
        return keyExp >= fileExp
    }

    // MARK: - Pure parsing (shared with the phone via NucleicProtocol's ClaudeCredentialFormat)

    /// The `claudeAiOauth.expiresAt` (milliseconds since epoch) from a credentials JSON string, or
    /// nil if absent/unparseable. Tolerates the value arriving as a JSON number or a numeric string.
    public static func expiresAt(_ json: String) -> Double? {
        ClaudeCredentialFormat.expiresAt(json)
    }

    /// The current OAuth access token, used to keep refresh-token ownership inside the broker.
    static func accessToken(_ json: String) -> String? {
        guard let data = json.data(using: .utf8),
            let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
            let oauth = obj["claudeAiOauth"] as? [String: Any],
            let token = oauth["accessToken"] as? String, !token.isEmpty
        else { return nil }
        return token
    }

    /// Whether `json` is a usable OAuth login: a `claudeAiOauth` object carrying a non-empty
    /// `accessToken` and `refreshToken`. Guards write-back from ever pushing a truncated or
    /// half-written credential file back into Nucleic's store.
    public static func isValidLogin(_ json: String) -> Bool {
        ClaudeCredentialFormat.isValidLogin(json)
    }

    // MARK: - Session-home export

    /// Reconcile a session's file with the canonical credential, then export the freshest login.
    /// This is shared by ordinary turn setup and the CLI broker so a refresh performed for one
    /// session is propagated before any concurrently-starting session can use an older token.
    static func reconcileAndExport(into claudeHome: String) {
        let path = (claudeHome as NSString).appendingPathComponent(".credentials.json")
        writeBackIfNewer(fromContainerFile: path)
        guard let json = read(), !json.isEmpty else { return }
        let existing = try? String(contentsOfFile: path, encoding: .utf8)
        guard shouldExport(keychain: json, toExistingFile: existing) else { return }
        guard (try? json.write(toFile: path, atomically: true, encoding: .utf8)) != nil else { return }
        try? SecretFile.restrictToOwner(path: path, isDirectory: false)
    }

    /// Remove the full OAuth credential after a broker operation. Session processes authenticate
    /// with `CLAUDE_CODE_OAUTH_TOKEN` and never receive the rotating refresh-token lineage.
    static func removeExportedCredential(from claudeHome: String) {
        let path = (claudeHome as NSString).appendingPathComponent(".credentials.json")
        try? FileManager.default.removeItem(atPath: path)
    }

    // MARK: - Keychain I/O

    #if canImport(Security)
    /// Nucleic's own OAuth credential — the one its mediated login (`ClaudeCredentialBroker`) writes.
    ///
    /// Nucleic reads ONLY this item. It never reaches into Claude Code's `Claude Code-credentials`
    /// (or any other application's) Keychain item: doing so raises a cross-application authorization
    /// prompt and, more fundamentally, would mean depending on a credential Nucleic didn't mint.
    /// Since Nucleic now mediates the OAuth flow end to end, the login it needs is always in its own
    /// store — signing in through Nucleic is the supported path, not adopting an external CLI login.
    static func read() -> String? {
        guard !disabled else { return nil }
        return SecretStores.default.get(service: ownedService, account: ownedAccount)
            .flatMap { String(data: $0, encoding: .utf8) }
    }

    /// Writes only Nucleic's item. Claude Code's external Keychain item is never modified.
    @discardableResult
    static func write(_ json: String) -> Bool {
        guard !disabled, isValidLogin(json), let data = json.data(using: .utf8) else { return false }
        return SecretStores.default.set(service: ownedService, account: ownedAccount, data: data)
    }

    /// Delete Nucleic's own login item (mesh-wide credential deletion — a tombstone landed).
    /// Claude Code's external Keychain item is never touched.
    static func removeLogin() {
        guard !disabled else { return }
        SecretStores.default.remove(service: ownedService, account: ownedAccount)
    }
    #else
    // Linux (runner): the shared source of truth is Claude's OWN credentials file —
    // `~/.claude/.credentials.json`, exactly where the Linux `claude` CLI keeps its login. The
    // reconciliation semantics above (newest-wins by `expiresAt`, never fabricate, never
    // downgrade) apply unchanged; only the hub moves from the Keychain to that file. Credentials
    // arrive here via the sealed credential mesh (COVALENCE_RUNNER §6).

    /// Claude's credentials file on Linux (`$CLAUDE_CONFIG_DIR` honored like the CLI does).
    static var linuxCredentialsFile: URL {
        let env = ProcessInfo.processInfo.environment
        let configDir = env["CLAUDE_CONFIG_DIR"].flatMap { $0.isEmpty ? nil : $0 }
            .map { URL(fileURLWithPath: $0, isDirectory: true) }
            ?? FileManager.default.homeDirectoryForCurrentUser
                .appendingPathComponent(".claude", isDirectory: true)
        return configDir.appendingPathComponent(".credentials.json")
    }

    static func read() -> String? {
        guard !disabled else { return nil }
        return try? String(contentsOf: linuxCredentialsFile, encoding: .utf8)
    }

    /// Update the credentials file in place. Like the Keychain path, this only ever *updates* an
    /// existing login — a missing file means logged out, and we never fabricate one.
    @discardableResult
    static func write(_ json: String) -> Bool {
        guard !disabled, FileManager.default.fileExists(atPath: linuxCredentialsFile.path) else {
            return false
        }
        do {
            try Data(json.utf8).write(to: linuxCredentialsFile, options: .atomic)
            try? SecretFile.restrictToOwner(path: linuxCredentialsFile.path, isDirectory: false)
            return true
        } catch {
            return false
        }
    }

    /// Delete the credentials file (mesh-wide credential deletion — a tombstone landed).
    static func removeLogin() {
        guard !disabled else { return }
        try? FileManager.default.removeItem(at: linuxCredentialsFile)
    }
    #endif

}
