import Foundation
import NucleicProtocol

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
/// The device half of the credential mesh (docs/COVALENCE_RUNNER.md §6, §0.2 item 5): the seam
/// a Mac (or any holding device) uses to answer a runner's `HostMsg.credentialNeeded` from the
/// credentials it already holds — the Keychain OAuth blob, the managed GitHub token — sealing
/// each directly to the runner's key so the relay and control plane only ever carry ciphertext.
///
/// Reads only; it never writes. The write direction (a runner's refreshed token flowing *back*)
/// is `credentialUpdate`, landed through `ClaudeLoginKeychain.writeBackIfNewer`, the exact
/// newest-wins reconciliation the sandbox already uses.
///
/// `plaintext(for:)` is the one platform-specific point: on macOS the sources are the login
/// Keychain (`ClaudeLoginKeychain`) and `GitHubCredentialStore`; the `#else` leg lets a Linux
/// device (a spare-Mac-less self-host acting as a holder) read the same files. A kind this
/// device doesn't hold returns nil and is simply omitted from the manifest / provision.
public struct CredentialProvider: Sendable {
    /// The device id stamped as each descriptor's provenance (this device produced the capture).
    public let deviceID: String
    /// This device's credential-sealing X25519 private key bytes — the recipient key a runner
    /// seals a rotated credential (`credentialUpdate`) back to. Persisted in the secret store
    /// (Keychain / 0600 file), minted on first use. Kept as `Data` (`Curve25519.*.PrivateKey`
    /// isn't `Sendable` on Linux — the §11.2 gotcha).
    private let sealingKeyBytes: Data
    /// Mesh-wide deletions this device knows: every offer (manifest/provision) and every
    /// landing runs through it, so a deleted kind stays dead until a strictly newer login.
    private let tombstones: CredentialTombstoneStore

    public init(deviceID: String, tombstones: CredentialTombstoneStore = CredentialTombstoneStore()) {
        self.deviceID = deviceID
        self.sealingKeyBytes = DeviceSealingKeyStore.loadOrCreate()
        self.tombstones = tombstones
    }

    /// The public half devices advertise in their manifest so a runner can seal updates back.
    public var sealingPublicKey: Data {
        guard let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: sealingKeyBytes)
        else { return Data() }
        return key.publicKey.rawRepresentation
    }

    /// The credential kinds this device can currently serve, with their freshness — the
    /// `CredentialManifest` a device gossips so a runner (and peers) know what it can provide.
    /// Carries this device's sealing key so a runner can mirror a rotated credential back.
    /// `updatedAt` is best-effort: the OAuth blob's own `expiresAt` when present (so a fresher
    /// login sorts newer across devices), else now.
    public func manifest(leases: [CredentialRefreshLease] = []) -> CredentialManifest {
        var records: [CredentialRecordDescriptor] = []
        for kind in Self.allKinds where plaintext(for: kind) != nil {
            let updatedAt = freshness(of: kind)
            // A deleted kind must not be advertised, even if a local copy lingers (this device
            // was offline when the deletion landed) — a stale holder's manifest is exactly the
            // resurrection path tombstones exist to close.
            guard !tombstones.suppresses(kind: kind, updatedAt: updatedAt) else { continue }
            records.append(CredentialRecordDescriptor(
                kind: kind, updatedAt: updatedAt, provenanceDeviceID: deviceID))
        }
        let key = sealingPublicKey
        return CredentialManifest(
            records: records, leases: leases,
            sealingPublicKey: key.isEmpty ? nil : key,
            tombstones: tombstones.all())
    }

    /// Land a credential a runner mirrored back (`HostMsg.credentialUpdate`) — the cloud
    /// analogue of `syncClaudeLoginBack`. Opens each box with this device's sealing key (AAD =
    /// the kind) and reconciles it into the local source of truth, newest-wins, never
    /// downgrading or fabricating. A box that fails to open (stale key, tampered) is skipped;
    /// returns the kinds actually written.
    @discardableResult
    public func landUpdate(_ envelope: SealedCredentialEnvelope) -> [CredentialKind] {
        guard let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: sealingKeyBytes)
        else { return [] }
        var landed: [CredentialKind] = []
        for record in envelope.records {
            guard let plaintext = try? record.box.open(
                with: key, additionalData: record.additionalData) else { continue }
            // A revision at or before a recorded deletion stays dead — only a strictly newer
            // login resurrects the kind (mesh-wide key deletion). Judged on the credential's
            // OWN clock where it has one (Claude `expiresAt`, Codex `last_refresh`) — the
            // clock tombstone bumps absorb — because a mirror-back's wire stamp is just
            // "now", which a Claude tombstone (bumped past the deleted token's future
            // expiry) would wrongly suppress for the rest of that token's lifetime.
            guard !tombstones.suppresses(
                kind: record.kind, updatedAt: Self.intrinsicStamp(of: record.kind, plaintext)
                    ?? record.updatedAt)
            else { continue }
            switch record.kind {
            case .claudeOAuth:
                // Newest-wins into the Keychain/credentials hub — same reconciliation the
                // sandbox write-back uses, so a runner's refreshed token converges every copy.
                if let json = String(data: plaintext, encoding: .utf8),
                   ClaudeLoginKeychain.shouldWriteBack(candidate: json, current: ClaudeLoginKeychain.read()) {
                    _ = ClaudeLoginKeychain.write(json)
                    landed.append(.claudeOAuth)
                }
            case .codexAuth:
                // The Codex login rotates too (last_refresh) — reconcile newest-wins into the
                // host's ~/.codex/auth.json, the same hub the sandbox write-back uses.
                let hostAuth = FileManager.default.homeDirectoryForCurrentUser
                    .appendingPathComponent(".codex/auth.json").path
                if let json = String(data: plaintext, encoding: .utf8),
                   CodexAuthFile.shouldReplace(
                       candidate: json, current: try? String(contentsOfFile: hostAuth, encoding: .utf8)) {
                    try? FileManager.default.createDirectory(
                        atPath: (hostAuth as NSString).deletingLastPathComponent,
                        withIntermediateDirectories: true)
                    if (try? json.write(toFile: hostAuth, atomically: true, encoding: .utf8)) != nil {
                        try? SecretFile.restrictToOwner(path: hostAuth, isDirectory: false)
                        landed.append(.codexAuth)
                    }
                }
            case .anthropicAPIKey:
                // An API key sealed to this device (REMOTE_AGENT_LOGIN §8 — a phone setting the
                // Console-key fallback on a Mac). Static keys have no lineage, so an explicit
                // set overwrites — exactly like typing it into Settings ▸ Agents.
                if let key = nonBlankString(plaintext), ControlAPIKeyStore.save(key) {
                    landed.append(.anthropicAPIKey)
                }
            case .openAIAPIKey:
                if let key = nonBlankString(plaintext), CodexControlAPIKeyStore.save(key) {
                    landed.append(.openAIAPIKey)
                }
            default:
                // Everything else (github-token, xai key — env-only on a Mac, grok config)
                // doesn't land here: rotating runner credentials mirror back through the two
                // OAuth cases above, and the device is the source of truth for the rest.
                break
            }
        }
        return landed
    }

    /// Land mesh-wide credential deletions (`ClientMsg.credentialRevoke` /
    /// `HostMsg.credentialRevoked` / manifest gossip). Records each stone
    /// (`deletedAt`-monotonic — a replay returns empty), clears the matching local store, and
    /// returns the stones that carried new information, each *absorbing* the local copy's
    /// freshness stamp first: Claude stamps `updatedAt` as the token's future `expiresAt`, so a
    /// deletion minted "now" wouldn't otherwise cover the very copy it deletes — the bump is
    /// what keeps every other stale holder from resurrecting it. Callers propagate the
    /// returned (possibly bumped) stones onward.
    @discardableResult
    public func applyTombstones(_ incoming: [CredentialTombstone]) -> [CredentialTombstone] {
        let fresh = tombstones.record(incoming)
        guard !fresh.isEmpty else { return [] }
        var applied: [CredentialTombstone] = []
        for stone in fresh {
            var effective = stone
            if plaintext(for: stone.kind) != nil {
                effective = stone.absorbing(freshness: freshness(of: stone.kind))
                if effective != stone { tombstones.record([effective]) }
            }
            clearLocalStore(for: stone.kind)
            applied.append(effective)
        }
        return applied
    }

    /// Every tombstone this device has recorded — pushed post-hello to connecting clients and
    /// gossiped inside the manifest so late joiners converge.
    public func recordedTombstones() -> [CredentialTombstone] {
        tombstones.all()
    }

    /// Clear this device's own copy of a deleted kind. Only the kinds with a deletable local
    /// hub; env-only kinds (xAI on a Mac) and the store-owned Git identity have no hub here.
    private func clearLocalStore(for kind: CredentialKind) {
        switch kind {
        case .claudeOAuth:
            ClaudeLoginKeychain.removeLogin()
        case .codexAuth:
            try? FileManager.default.removeItem(
                at: FileManager.default.homeDirectoryForCurrentUser
                    .appendingPathComponent(".codex/auth.json"))
        case .anthropicAPIKey:
            _ = ControlAPIKeyStore.clear()
        case .openAIAPIKey:
            _ = CodexControlAPIKeyStore.clear()
        default:
            break
        }
    }

    /// The credential's own freshness stamp parsed from `plaintext`, for the two kinds that
    /// carry one (Claude `expiresAt` ms, Codex `last_refresh` s) — the clock tombstones and
    /// descriptor merges run on. Nil for stampless kinds (API keys) and unparseable payloads.
    static func intrinsicStamp(of kind: CredentialKind, _ plaintext: Data) -> Date? {
        guard let json = String(data: plaintext, encoding: .utf8) else { return nil }
        switch kind {
        case .claudeOAuth:
            return ClaudeCredentialFormat.expiresAt(json)
                .map { Date(timeIntervalSince1970: $0 / 1000) }
        case .codexAuth:
            return CodexCredentialFormat.lastRefresh(json)
                .map(Date.init(timeIntervalSince1970:))
        default:
            return nil
        }
    }

    /// A non-blank UTF-8 string from sealed plaintext (a key is a single trimmed line), or nil.
    private func nonBlankString(_ plaintext: Data) -> String? {
        guard let s = String(data: plaintext, encoding: .utf8)?
            .trimmingCharacters(in: .whitespacesAndNewlines), !s.isEmpty else { return nil }
        return s
    }

    /// Seal every kind in `need` this device holds to the runner's sealing key. The kind string
    /// is the AAD (a box can't be replayed as a different kind — `SealedCredentialRecord`'s
    /// contract). Kinds not held are skipped; an empty result means "nothing to offer".
    public func sealedEnvelope(for need: WireCredentialNeed) -> SealedCredentialEnvelope? {
        var records: [SealedCredentialRecord] = []
        for kind in need.kinds {
            guard let plaintext = plaintext(for: kind) else { continue }
            let updatedAt = freshness(of: kind)
            // Never provision a deleted kind from a lingering local copy (see `manifest`).
            guard !tombstones.suppresses(kind: kind, updatedAt: updatedAt) else { continue }
            let stub = SealedCredentialRecord(
                kind: kind, updatedAt: updatedAt,
                box: SealedCredentialBox(ephemeralPublicKey: Data(), ciphertext: Data()))
            guard let box = try? SealedCredentialBox.seal(
                plaintext, to: need.sealingPublicKey, additionalData: stub.additionalData)
            else { continue }
            records.append(SealedCredentialRecord(kind: kind, updatedAt: updatedAt, box: box))
        }
        return records.isEmpty ? nil : SealedCredentialEnvelope(records: records)
    }

    // MARK: - Local sources

    /// Every kind this provider knows how to read — the full set the runner can land
    /// (`RunnerCredentialVault.landings`); kept in lockstep with that table.
    public static let allKinds: [CredentialKind] = [
        .claudeOAuth, .codexAuth, .grokConfig,
        .githubToken, .anthropicAPIKey, .openAIAPIKey, .xaiAPIKey,
        .gitCredentialBundle,
    ]

    /// The plaintext bytes for a kind, or nil when this device doesn't hold it.
    public func plaintext(for kind: CredentialKind) -> Data? {
        switch kind {
        case .claudeOAuth:
            // The login blob in the exact `.credentials.json` shape the CLI reads — Keychain
            // stores it as the bare OAuth object, so wrap it under `claudeAiOauth` if needed.
            guard let raw = ClaudeLoginKeychain.read() else { return nil }
            return Self.claudeCredentialsFileBytes(fromKeychainBlob: raw)
        case .codexAuth:
            // The Codex ChatGPT login is a plain file the host shares with its containers.
            return fileBytes(atHomePath: ".codex/auth.json")
        case .grokConfig:
            return fileBytes(atHomePath: ".grok/config.toml")
        case .githubToken:
            return nonBlank(GitHubCredentialStore.loadToken())
        case .anthropicAPIKey:
            // The user-supplied Control API key (Keychain), else the process env.
            return nonBlank(ControlAPIKeyStore.load()) ?? envBytes("ANTHROPIC_API_KEY")
        case .openAIAPIKey:
            return nonBlank(CodexControlAPIKeyStore.load()) ?? envBytes("OPENAI_API_KEY")
        case .xaiAPIKey:
            // No dedicated store — xAI is env-only on the Mac today.
            return envBytes("XAI_API_KEY")
        case .gitCredentialBundle:
            // The whole Managed Git identity (mode + secret + flags), JSON-encoded. Nil when the
            // device has no usable git credential to forward (mode `.none` / empty slot).
            return try? GitCredentialBundle.capture()?.encoded()
        default:
            return nil
        }
    }

    /// Bytes of a file under the user's home, or nil if absent.
    private func fileBytes(atHomePath path: String) -> Data? {
        let url = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(path)
        return try? Data(contentsOf: url)
    }

    /// A non-blank string as UTF-8 bytes, or nil.
    private func nonBlank(_ value: String?) -> Data? {
        guard let value, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        else { return nil }
        return Data(value.utf8)
    }

    /// A non-blank env var as bytes, or nil.
    private func envBytes(_ name: String) -> Data? {
        nonBlank(ProcessInfo.processInfo.environment[name])
    }

    /// Wrap the raw Keychain OAuth value into the `{ "claudeAiOauth": { … } }` file shape the
    /// `claude` CLI reads. The Keychain item is already that object (or already wrapped, on some
    /// versions) — detect and normalize so the landed file is always CLI-valid.
    static func claudeCredentialsFileBytes(fromKeychainBlob raw: String) -> Data? {
        guard let data = raw.data(using: .utf8),
              let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        else { return nil }
        if obj["claudeAiOauth"] != nil {
            return data  // already file-shaped
        }
        let wrapped = ["claudeAiOauth": obj]
        return try? JSONSerialization.data(withJSONObject: wrapped)
    }

    /// Best-effort freshness for a kind: the rotating logins' own timestamps (Claude `expiresAt`
    /// ms, Codex `last_refresh` s) when readable, so cross-device merge picks the newest; else
    /// now (a static key has no lineage — the newest capture wins trivially).
    private func freshness(of kind: CredentialKind) -> Date {
        switch kind {
        case .claudeOAuth:
            if let raw = ClaudeLoginKeychain.read(), let expiresMs = ClaudeLoginKeychain.expiresAt(raw) {
                return Date(timeIntervalSince1970: expiresMs / 1000)
            }
        case .codexAuth:
            if let bytes = plaintext(for: .codexAuth), let json = String(data: bytes, encoding: .utf8),
               let last = CodexAuthFile.lastRefresh(json) {
                return Date(timeIntervalSince1970: last)
            }
        default:
            break
        }
        return Date()
    }
}

/// The device's credential-sealing keypair persistence — the recipient key a runner seals a
/// mirrored-back credential to (`credentialUpdate`). Keychain on macOS, the 0600-file store on
/// Linux. Distinct account from the runner's own sealing key (a device is never a runner).
private enum DeviceSealingKeyStore {
    private static let account = "com.nucleic.device.credential-sealing"

    static func loadOrCreate() -> Data {
        if let data = load(), data.count == 32 { return data }
        let fresh = Curve25519.KeyAgreement.PrivateKey().rawRepresentation
        save(fresh)
        return fresh
    }

    private static func load() -> Data? { SecretStores.default.get(service: nil, account: account) }
    private static func save(_ data: Data) {
        SecretStores.default.set(service: nil, account: account, data: data)
    }
}
