import Foundation
#if canImport(CryptoKit)
import CryptoKit
#else
// @preconcurrency: swift-crypto's SymmetricKey (unlike CryptoKit's) isn't marked Sendable;
// the stored subkeys are immutable value types, so the warning-not-error treatment is sound.
@preconcurrency import Crypto
#endif

/// Carbon's at-rest cryptography (docs/CARBON_SHARDING.md §4): a per-project master key,
/// HKDF-derived subkeys, and a **deterministic** (SIV-equivalent) AEAD, so that
///
/// - identical plaintext shards dedup to one stored blob and one wire transfer,
/// - a shard/manifest ID is derivable by re-encryption (no plaintext→ID map anywhere), and
/// - any holder — keyed or not — verifies a blob by re-hashing it (`blobID`), never by
///   decrypting.
///
/// swift-crypto has no AES-SIV, so this is the standard derived-nonce construction, which
/// gives the same guarantee (same plaintext ⇒ same ciphertext; nonce reuse impossible across
/// distinct plaintexts because the nonce is a PRF of the plaintext):
///
///     nonce   = HMAC-SHA256(K_siv, plaintext)[0..<12]
///     sealed  = AES-256-GCM.seal(plaintext, key: K_enc, nonce: nonce)
///     blob    = nonce ‖ ciphertext ‖ tag
///     blobID  = SHA256(blob).hex          // lowercase, 64 chars
///
/// Accepted leak (spec §4.2/§13): equality of identical plaintext within one project is
/// visible to non-key-holders. Replica members hold the key anyway.
public struct CarbonCrypto: Sendable {
    /// A project's 32-byte master key `K_p` (spec §4.1). Only subkeys derived from it are used
    /// by the store; the master key itself travels only key-custody paths (credential mesh /
    /// iCloud Keychain escrow).
    public struct ProjectKey: Sendable, Equatable {
        public let bytes: Data

        public init(bytes: Data) throws {
            guard bytes.count == 32 else { throw CarbonCryptoError.invalidKeyLength(bytes.count) }
            self.bytes = bytes
        }

        public static func random() -> ProjectKey {
            let raw = SymmetricKey(size: .bits256).withUnsafeBytes { Data($0) }
            // Force-try is safe: the count is 32 by construction.
            return try! ProjectKey(bytes: raw)
        }
    }

    private let encKey: SymmetricKey
    private let sivKey: SymmetricKey

    public init(projectKey: ProjectKey) {
        let master = SymmetricKey(data: projectKey.bytes)
        encKey = HKDF<SHA256>.deriveKey(
            inputKeyMaterial: master, info: Data("nucleic.carbon.enc.v1".utf8), outputByteCount: 32)
        sivKey = HKDF<SHA256>.deriveKey(
            inputKeyMaterial: master, info: Data("nucleic.carbon.siv.v1".utf8), outputByteCount: 32)
    }

    /// A sealed blob and its content address. `id == blobID(ciphertext)` always.
    public struct Sealed: Sendable, Equatable {
        public let id: String
        public let ciphertext: Data
    }

    /// Deterministically encrypt `plaintext` (spec §4.2). Same plaintext ⇒ byte-identical
    /// result on every platform and every call.
    public func seal(_ plaintext: Data) throws -> Sealed {
        let mac = HMAC<SHA256>.authenticationCode(for: plaintext, using: sivKey)
        let nonceBytes = Data(mac).prefix(12)
        let nonce = try AES.GCM.Nonce(data: nonceBytes)
        let box = try AES.GCM.seal(plaintext, using: encKey, nonce: nonce)
        var blob = Data(capacity: 12 + box.ciphertext.count + box.tag.count)
        blob.append(nonceBytes)
        blob.append(box.ciphertext)
        blob.append(box.tag)
        return Sealed(id: Self.blobID(blob), ciphertext: blob)
    }

    /// Decrypt and authenticate a sealed blob. Any bit flip anywhere in the blob fails the
    /// GCM tag (or, for a nonce flip, decrypts to garbage that fails the tag) — there is no
    /// partial trust path.
    public func open(_ blob: Data) throws -> Data {
        guard blob.count >= 12 + 16 else { throw CarbonCryptoError.malformedBlob }
        // Data slices retain the parent's indices — re-wrap so ranges are zero-based.
        let bytes = Data(blob)
        let nonce = try AES.GCM.Nonce(data: bytes.prefix(12))
        let tag = bytes.suffix(16)
        let ciphertext = bytes.dropFirst(12).dropLast(16)
        let box = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
        do {
            return try AES.GCM.open(box, using: encKey)
        } catch {
            throw CarbonCryptoError.integrityFailure
        }
    }

    /// The content address of a sealed blob: lowercase SHA-256 hex of its ciphertext bytes.
    /// Computable — and *verified on every write and read* (spec §7.2) — without the key.
    public static func blobID(_ blob: Data) -> String {
        SHA256.hash(data: blob).map { String(format: "%02x", $0) }.joined()
    }
}

public enum CarbonCryptoError: Error, Equatable {
    case invalidKeyLength(Int)
    case malformedBlob
    case integrityFailure
}

#if canImport(Security)
import Security

/// iCloud Keychain escrow of project master keys (spec §4.4, D7): a synchronizable generic-
/// password item per project, so a replacement Mac that signs into iCloud and pairs can
/// recover every project key and decrypt back-filled shards. Linux runners never touch this —
/// they receive keys over the credential mesh (§4.3).
///
/// Failure here is non-fatal by design: enrollment proceeds and the durability chip shows
/// "key not escrowed" (§13). Never called from tests (a headless Keychain read can hang on an
/// invisible ACL prompt — see MESH_TRANSFER's test note).
public enum CarbonKeyEscrow {
    public static let service = "nucleic.carbon.projectKey"

    public static func escrow(_ key: CarbonCrypto.ProjectKey, projectUUID: UUID) throws {
        let account = projectUUID.uuidString
        var query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
            kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
        ]
        SecItemDelete(query as CFDictionary)
        query[kSecAttrSynchronizable as String] = true
        query[kSecValueData as String] = key.bytes
        query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else { throw CarbonEscrowError.keychain(status) }
    }

    public static func recover(projectUUID: UUID) throws -> CarbonCrypto.ProjectKey? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: projectUUID.uuidString,
            kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
            kSecReturnData as String: true,
        ]
        var out: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &out)
        if status == errSecItemNotFound { return nil }
        guard status == errSecSuccess, let data = out as? Data else {
            throw CarbonEscrowError.keychain(status)
        }
        return try CarbonCrypto.ProjectKey(bytes: data)
    }
}

public enum CarbonEscrowError: Error, Equatable {
    case keychain(OSStatus)
}
#endif
