import Foundation
import CryptoKit

/// X3DH key agreement protocol (Signal Protocol)
enum X3DH {

    // MARK: - Pre-Key Generation

    /// Generate a signed pre-key (SPK).
    /// Returns (private, public, signature, id).
    /// Matches Python: generate_signed_prekey(identity_private)
    static func generateSignedPrekey(
        identityPrivate: Curve25519.Signing.PrivateKey
    ) throws -> (privateKey: Curve25519.KeyAgreement.PrivateKey,
                  publicKey: Curve25519.KeyAgreement.PublicKey,
                  signature: Data,
                  id: String) {
        let (spkPriv, spkPub) = X25519Crypto.generateKeypair()
        let spkPubBytes = X25519Crypto.serializePublic(spkPub)
        let signature = try Ed25519Crypto.sign(identityPrivate, data: spkPubBytes)
        return (spkPriv, spkPub, signature, UUID().uuidString)
    }

    /// Generate a batch of one-time pre-keys.
    /// Matches Python: generate_one_time_prekeys(count=50)
    static func generateOneTimePrekeys(count: Int = 50) -> [(privateKey: Curve25519.KeyAgreement.PrivateKey,
                                                               publicKey: Curve25519.KeyAgreement.PublicKey,
                                                               id: String)] {
        (0..<count).map { _ in
            let (priv, pub) = X25519Crypto.generateKeypair()
            return (priv, pub, UUID().uuidString)
        }
    }

    // MARK: - X3DH Initiate (Alice)

    /// Initiator side of X3DH.
    /// Returns (sharedSecret, ephemeralPrivate, ephemeralPublic).
    /// Matches Python: x3dh_initiate(ik_private_ed, ik_public_remote_ed, spk_remote, spk_signature, opk_remote?)
    static func initiate(
        ikPrivateEd: Curve25519.Signing.PrivateKey,
        ikPublicRemoteEd: Curve25519.Signing.PublicKey,
        spkRemote: Curve25519.KeyAgreement.PublicKey,
        spkSignature: Data,
        opkRemote: Curve25519.KeyAgreement.PublicKey? = nil
    ) throws -> (sharedSecret: Data,
                  ephemeralPrivate: Curve25519.KeyAgreement.PrivateKey,
                  ephemeralPublic: Curve25519.KeyAgreement.PublicKey) {
        // Verify SPK signature
        let spkRemoteBytes = X25519Crypto.serializePublic(spkRemote)
        guard Ed25519Crypto.verify(ikPublicRemoteEd, signature: spkSignature, data: spkRemoteBytes) else {
            throw CryptoError.x3dhFailed("Invalid SPK signature")
        }

        // Convert identity keys to X25519
        let ikX25519Private = try X25519Crypto.fromEd25519Private(ikPrivateEd)
        let ikX25519Remote = try X25519Crypto.fromEd25519Public(ikPublicRemoteEd)

        // Generate ephemeral keypair
        let (ekPriv, ekPub) = X25519Crypto.generateKeypair()

        // DH computations
        let dh1 = try X25519Crypto.dh(ikX25519Private, spkRemote)   // IK_A, SPK_B
        let dh2 = try X25519Crypto.dh(ekPriv, ikX25519Remote)        // EK_A, IK_B
        let dh3 = try X25519Crypto.dh(ekPriv, spkRemote)             // EK_A, SPK_B

        var dhConcat = dh1 + dh2 + dh3
        if let opk = opkRemote {
            let dh4 = try X25519Crypto.dh(ekPriv, opk)               // EK_A, OPK_B
            dhConcat += dh4
        }

        // Derive shared secret
        let sharedSecret = CryptoUtils.hkdfDerive(
            inputKey: dhConcat,
            salt: Data(repeating: 0x00, count: 32),
            info: Data(Constants.x3dhInfo.utf8),
            length: 32
        )

        return (sharedSecret, ekPriv, ekPub)
    }

    // MARK: - X3DH Respond (Bob)

    /// Responder side of X3DH.
    /// Returns sharedSecret.
    /// Matches Python: x3dh_respond(ik_private_ed, spk_private, ik_remote_ed, ek_remote, opk_private?)
    static func respond(
        ikPrivateEd: Curve25519.Signing.PrivateKey,
        spkPrivate: Curve25519.KeyAgreement.PrivateKey,
        ikRemoteEd: Curve25519.Signing.PublicKey,
        ekRemote: Curve25519.KeyAgreement.PublicKey,
        opkPrivate: Curve25519.KeyAgreement.PrivateKey? = nil
    ) throws -> Data {
        let ikX25519Private = try X25519Crypto.fromEd25519Private(ikPrivateEd)
        let ikX25519Remote = try X25519Crypto.fromEd25519Public(ikRemoteEd)

        let dh1 = try X25519Crypto.dh(spkPrivate, ikX25519Remote)   // SPK_B, IK_A
        let dh2 = try X25519Crypto.dh(ikX25519Private, ekRemote)     // IK_B, EK_A
        let dh3 = try X25519Crypto.dh(spkPrivate, ekRemote)          // SPK_B, EK_A

        var dhConcat = dh1 + dh2 + dh3
        if let opk = opkPrivate {
            let dh4 = try X25519Crypto.dh(opk, ekRemote)             // OPK_B, EK_A
            dhConcat += dh4
        }

        let sharedSecret = CryptoUtils.hkdfDerive(
            inputKey: dhConcat,
            salt: Data(repeating: 0x00, count: 32),
            info: Data(Constants.x3dhInfo.utf8),
            length: 32
        )

        return sharedSecret
    }
}
