#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
import Foundation

/// Sealed-credential crypto (docs/ANTIMATTER_RUNNER.md §6): encrypt a credential directly *to*
/// a recipient's X25519 public key so no intermediary — relay, Worker, control plane — ever
/// holds plaintext. HPKE-shaped with the primitives this package already ships (X25519 +
/// HKDF-SHA256 + ChaChaPoly), matching the Noise stack's cipher family so the eventual Linux
/// port swaps one crypto backend, not two.
///
///   seal:  eph ← X25519.generate()
///          shared = X25519(eph.priv, recipientPub)
///          key    = HKDF-SHA256(ikm: shared, salt: "nucleic-sealed-credential-v1",
///                               info: ephPub ‖ recipientPub, 32 bytes)
///          box    = ChaChaPoly.seal(plaintext, key, aad)
///
/// Binding the *ephemeral and recipient* keys into the KDF info means a box decrypts only with
/// the exact recipient key it was sealed to; the caller-supplied AAD (the credential kind)
/// additionally pins what the box claims to be.
public struct SealedCredentialBox: Sendable, Codable, Equatable {
    /// The sender's one-shot X25519 public key (32 bytes).
    public let ephemeralPublicKey: Data
    /// ChaChaPoly combined form: nonce ‖ ciphertext ‖ tag.
    public let ciphertext: Data

    public init(ephemeralPublicKey: Data, ciphertext: Data) {
        self.ephemeralPublicKey = ephemeralPublicKey
        self.ciphertext = ciphertext
    }

    /// Domain-separation salt. Versioned: a future v2 (new KDF layout, new suite) changes the
    /// string, so v1 boxes can never silently open under v2 rules.
    public static let context = "nucleic-sealed-credential-v1"

    public enum SealedCredentialError: Error, Equatable {
        case malformedKey
        case decryptFailed
    }

    private static func deriveKey(
        shared: SharedSecret, ephemeralPublicKey: Data, recipientPublicKey: Data
    ) -> SymmetricKey {
        shared.hkdfDerivedSymmetricKey(
            using: SHA256.self,
            salt: Data(Self.context.utf8),
            sharedInfo: ephemeralPublicKey + recipientPublicKey,
            outputByteCount: 32)
    }

    /// Seal `plaintext` to `recipientPublicKey` (raw 32-byte X25519), binding `additionalData`.
    public static func seal(
        _ plaintext: Data, to recipientPublicKey: Data, additionalData: Data = Data()
    ) throws -> SealedCredentialBox {
        let recipient: Curve25519.KeyAgreement.PublicKey
        do {
            recipient = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: recipientPublicKey)
        } catch {
            throw SealedCredentialError.malformedKey
        }
        let ephemeral = Curve25519.KeyAgreement.PrivateKey()
        let ephemeralPublic = ephemeral.publicKey.rawRepresentation
        let shared = try ephemeral.sharedSecretFromKeyAgreement(with: recipient)
        let key = deriveKey(
            shared: shared, ephemeralPublicKey: ephemeralPublic, recipientPublicKey: recipientPublicKey)
        let sealed = try ChaChaPoly.seal(plaintext, using: key, authenticating: additionalData)
        return SealedCredentialBox(ephemeralPublicKey: ephemeralPublic, ciphertext: sealed.combined)
    }

    /// Open with the recipient's private key. Throws `.decryptFailed` on any mismatch — wrong
    /// key, tampered ciphertext, or AAD that doesn't match what was sealed.
    public func open(
        with recipientKey: Curve25519.KeyAgreement.PrivateKey, additionalData: Data = Data()
    ) throws -> Data {
        let ephemeral: Curve25519.KeyAgreement.PublicKey
        do {
            ephemeral = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: ephemeralPublicKey)
        } catch {
            throw SealedCredentialError.malformedKey
        }
        let recipientPublic = recipientKey.publicKey.rawRepresentation
        do {
            let shared = try recipientKey.sharedSecretFromKeyAgreement(with: ephemeral)
            let key = Self.deriveKey(
                shared: shared, ephemeralPublicKey: ephemeralPublicKey,
                recipientPublicKey: recipientPublic)
            let box = try ChaChaPoly.SealedBox(combined: ciphertext)
            return try ChaChaPoly.open(box, using: key, authenticating: additionalData)
        } catch {
            throw SealedCredentialError.decryptFailed
        }
    }
}
