//! RFC-0830 Section 2.3: PQXDH Protocol
//!
//! Post-Quantum Extended Diffie-Hellman Key Agreement
//!
//! This module implements hybrid key agreement combining:
//! - 4× X25519 elliptic curve handshakes (classical)
//! - 1× ML-KEM-768 post-quantum key encapsulation
//! - HKDF-SHA256 to combine 5 shared secrets into root key
//!
//! Security: Attacker must break BOTH X25519 AND ML-KEM-768 to compromise
//! This provides defense against "harvest now, decrypt later" attacks.

const std = @import("std");
const crypto = std.crypto;

// ============================================================================
// C FFI: liboqs (ML-KEM-768)
// ============================================================================
// Link against liboqs (C library, compiled in build.zig)
// Source: https://github.com/open-quantum-safe/liboqs
// FIPS 203: ML-KEM-768 (post-standardization naming for Kyber-768)

/// ML-KEM-768 key generation
extern "c" fn OQS_KEM_ml_kem_768_keypair(
    public_key: ?*u8,
    secret_key: ?*u8,
) c_int;

/// ML-KEM-768 encapsulation (creates shared secret + ciphertext)
extern "c" fn OQS_KEM_ml_kem_768_encaps(
    ciphertext: ?*u8,
    shared_secret: ?*u8,
    public_key: ?*const u8,
) c_int;

/// ML-KEM-768 decapsulation (recovers shared secret from ciphertext)
extern "c" fn OQS_KEM_ml_kem_768_decaps(
    shared_secret: ?*u8,
    ciphertext: ?*const u8,
    secret_key: ?*const u8,
) c_int;

/// Switch liboqs RNG algorithm (e.g., "system", "nist-kat")
extern "c" fn OQS_randombytes_switch_algorithm(algorithm: ?[*:0]const u8) c_int;

/// Set custom RNG callback
extern "c" fn OQS_randombytes_custom_algorithm(algorithm_ptr: *const fn ([*]u8, usize) callconv(.c) void) void;

/// Global mutex to protect RNG state during deterministic generation
var rng_mutex = std.Thread.Mutex{};

/// Global SHAKE256 state for deterministic RNG
var deterministic_rng: std.crypto.hash.sha3.Shake256 = undefined;

/// Custom RNG callback for liboqs -> uses global SHAKE256 state
fn custom_rng_callback(dest: [*]u8, len: usize) callconv(.c) void {
    deterministic_rng.squeeze(dest[0..len]);
}

pub const KeyPair = struct {
    public_key: [ML_KEM_768.PUBLIC_KEY_SIZE]u8,
    secret_key: [ML_KEM_768.SECRET_KEY_SIZE]u8,
};

/// Generate ML-KEM-768 keypair deterministically from a 32-byte seed
/// Thread-safe via global mutex (liboqs RNG is global state)
pub fn generateKeypairFromSeed(seed: [32]u8) !KeyPair {
    rng_mutex.lock();
    defer rng_mutex.unlock();

    // 1. Initialize deterministic RNG with seed
    deterministic_rng = std.crypto.hash.sha3.Shake256.init(.{});
    // Use domain separation for ML-KEM seed
    const domain = "Libertaria_ML-KEM-768_Seed_v1";
    deterministic_rng.update(domain);
    deterministic_rng.update(&seed);

    // 2. Switch liboqs to use our custom callback
    OQS_randombytes_custom_algorithm(custom_rng_callback);

    // 3. Generate keypair
    var kp: KeyPair = undefined;

    // Call liboqs key generation
    // Note: liboqs keygen consumes randomness from the RNG we set
    if (OQS_KEM_ml_kem_768_keypair(&kp.public_key[0], &kp.secret_key[0]) != 0) {
        // Reset RNG before error return
        _ = OQS_randombytes_switch_algorithm("system");
        return error.KeyGenerationFailed;
    }

    // 4. Restore system RNG (important!)
    _ = OQS_randombytes_switch_algorithm("system");

    return kp;
}

// ============================================================================
// ML-KEM-768 Parameters (NIST FIPS 203)
// ============================================================================

pub const ML_KEM_768 = struct {
    pub const PUBLIC_KEY_SIZE = 1184;
    pub const SECRET_KEY_SIZE = 2400;
    pub const CIPHERTEXT_SIZE = 1088;
    pub const SHARED_SECRET_SIZE = 32;
    pub const SECURITY_LEVEL = 3; // NIST Level 3 (≈AES-192)
};

// ============================================================================
// X25519 Parameters (Classical)
// ============================================================================

pub const X25519 = struct {
    pub const PUBLIC_KEY_SIZE = 32;
    pub const PRIVATE_KEY_SIZE = 32;
    pub const SHARED_SECRET_SIZE = 32;
};

// ============================================================================
// PQXDH Prekey Bundle
// ============================================================================
// Sent by Bob to Alice (or published to prekey server)
// Contains all keys needed to initiate a hybrid key agreement

pub const PrekeyBundle = struct {
    /// Long-term identity key (Ed25519 public key)
    /// Used to verify all signatures in bundle
    identity_key: [32]u8,

    /// Medium-term signed prekey (X25519 public key)
    /// Rotated every 30 days
    signed_prekey_x25519: [X25519.PUBLIC_KEY_SIZE]u8,

    /// Signature of signed_prekey_x25519 by identity_key (Ed25519)
    /// Proves Bob authorized this prekey
    signed_prekey_signature: [64]u8,

    /// Post-quantum signed prekey (ML-KEM-768 public key)
    /// Rotated every 30 days, paired with X25519 signed prekey
    signed_prekey_mlkem: [ML_KEM_768.PUBLIC_KEY_SIZE]u8,

    /// One-time ephemeral prekey (X25519 public key)
    /// Consumed on first use, provides forward secrecy
    one_time_prekey_x25519: [X25519.PUBLIC_KEY_SIZE]u8,

    /// One-time ephemeral prekey (ML-KEM-768 public key)
    /// Consumed on first use, provides PQ forward secrecy
    one_time_prekey_mlkem: [ML_KEM_768.PUBLIC_KEY_SIZE]u8,

    /// Serialize bundle to bytes for transmission
    /// Total size: 32 + 32 + 64 + 1184 + 32 + 1184 = 2528 bytes
    pub fn toBytes(self: *const PrekeyBundle, allocator: std.mem.Allocator) ![]u8 {
        const total_size = 32 + 32 + 64 + ML_KEM_768.PUBLIC_KEY_SIZE + 32 + ML_KEM_768.PUBLIC_KEY_SIZE;
        var buffer = try allocator.alloc(u8, total_size);
        var offset: usize = 0;

        @memcpy(buffer[offset .. offset + 32], &self.identity_key);
        offset += 32;

        @memcpy(buffer[offset .. offset + 32], &self.signed_prekey_x25519);
        offset += 32;

        @memcpy(buffer[offset .. offset + 64], &self.signed_prekey_signature);
        offset += 64;

        @memcpy(buffer[offset .. offset + ML_KEM_768.PUBLIC_KEY_SIZE], &self.signed_prekey_mlkem);
        offset += ML_KEM_768.PUBLIC_KEY_SIZE;

        @memcpy(buffer[offset .. offset + 32], &self.one_time_prekey_x25519);
        offset += 32;

        @memcpy(buffer[offset .. offset + ML_KEM_768.PUBLIC_KEY_SIZE], &self.one_time_prekey_mlkem);

        return buffer;
    }

    /// Deserialize bundle from bytes
    pub fn fromBytes(_: std.mem.Allocator, data: []const u8) !PrekeyBundle {
        const expected_size = 32 + 32 + 64 + ML_KEM_768.PUBLIC_KEY_SIZE + 32 + ML_KEM_768.PUBLIC_KEY_SIZE;
        if (data.len != expected_size) {
            return error.InvalidBundleSize;
        }

        var bundle: PrekeyBundle = undefined;
        var offset: usize = 0;

        @memcpy(&bundle.identity_key, data[offset .. offset + 32]);
        offset += 32;

        @memcpy(&bundle.signed_prekey_x25519, data[offset .. offset + 32]);
        offset += 32;

        @memcpy(&bundle.signed_prekey_signature, data[offset .. offset + 64]);
        offset += 64;

        @memcpy(&bundle.signed_prekey_mlkem, data[offset .. offset + ML_KEM_768.PUBLIC_KEY_SIZE]);
        offset += ML_KEM_768.PUBLIC_KEY_SIZE;

        @memcpy(&bundle.one_time_prekey_x25519, data[offset .. offset + 32]);
        offset += 32;

        @memcpy(&bundle.one_time_prekey_mlkem, data[offset .. offset + ML_KEM_768.PUBLIC_KEY_SIZE]);

        return bundle;
    }
};

// ============================================================================
// PQXDH Initial Message (Alice → Bob)
// ============================================================================
// Sent by Alice when initiating communication with Bob
// Contains ephemeral public keys + ML-KEM ciphertext

pub const PQXDHInitialMessage = struct {
    /// Alice's ephemeral X25519 public key
    ephemeral_x25519: [X25519.PUBLIC_KEY_SIZE]u8,

    /// ML-KEM-768 ciphertext for Bob's signed prekey
    mlkem_ciphertext: [ML_KEM_768.CIPHERTEXT_SIZE]u8,

    /// Serialize for transmission
    /// Size: 32 + 1088 = 1120 bytes (fits in 2 LWF jumbo frames or 3 standard frames)
    pub fn toBytes(self: *const PQXDHInitialMessage, allocator: std.mem.Allocator) ![]u8 {
        const total_size = X25519.PUBLIC_KEY_SIZE + ML_KEM_768.CIPHERTEXT_SIZE;
        var buffer = try allocator.alloc(u8, total_size);

        @memcpy(buffer[0..32], &self.ephemeral_x25519);
        @memcpy(buffer[32..], &self.mlkem_ciphertext);

        return buffer;
    }

    /// Deserialize from bytes
    pub fn fromBytes(data: []const u8) !PQXDHInitialMessage {
        const expected_size = X25519.PUBLIC_KEY_SIZE + ML_KEM_768.CIPHERTEXT_SIZE;
        if (data.len != expected_size) {
            return error.InvalidInitialMessageSize;
        }

        var msg: PQXDHInitialMessage = undefined;
        @memcpy(&msg.ephemeral_x25519, data[0..32]);
        @memcpy(&msg.mlkem_ciphertext, data[32..]);
        return msg;
    }
};

// ============================================================================
// PQXDH Key Agreement (Alice Initiates)
// ============================================================================

pub const PQXDHInitiatorResult = struct {
    /// Root key derived from 5 shared secrets
    /// This becomes the input to Double Ratchet initialization
    root_key: [32]u8,

    /// Initial message sent to Bob
    initial_message: PQXDHInitialMessage,

    /// Ephemeral private key (keep secret until message sent)
    ephemeral_private: [X25519.PRIVATE_KEY_SIZE]u8,
};

/// Alice initiates hybrid key agreement with Bob
///
/// **Ceremony:**
/// 1. Generate ephemeral X25519 keypair (DH1, DH2)
/// 2. ECDH with Bob's signed prekey (DH3)
/// 3. ECDH with Bob's one-time prekey (DH4)
/// 4. ML-KEM encapsulate toward Bob's signed prekey (KEM1)
/// 5. Combine 5 shared secrets: [DH1, DH2, DH3, DH4, KEM1]
/// 6. KDF via HKDF-SHA256
///
/// **Result:** Root key for Double Ratchet + initial message
pub fn initiator(
    alice_identity_private: [32]u8,
    bob_prekey_bundle: *const PrekeyBundle,
    _: std.mem.Allocator,
) !PQXDHInitiatorResult {
    // === Step 1: Generate Alice's ephemeral X25519 keypair ===
    var ephemeral_private: [X25519.PRIVATE_KEY_SIZE]u8 = undefined;
    crypto.random.bytes(&ephemeral_private);

    const ephemeral_public = try crypto.dh.X25519.recoverPublicKey(ephemeral_private);

    // === Step 2-4: Compute three X25519 shared secrets (DH1, DH2, DH3) ===

    // DH1: ephemeral ↔ Bob's signed prekey
    const dh1 = try crypto.dh.X25519.scalarmult(ephemeral_private, bob_prekey_bundle.signed_prekey_x25519);

    // DH2: ephemeral ↔ Bob's one-time prekey
    const dh2 = try crypto.dh.X25519.scalarmult(ephemeral_private, bob_prekey_bundle.one_time_prekey_x25519);

    // DH3: Alice's identity ↔ Bob's signed prekey
    const dh3 = try crypto.dh.X25519.scalarmult(alice_identity_private, bob_prekey_bundle.signed_prekey_x25519);

    // === Step 5: ML-KEM-768 encapsulation ===
    // Alice generates ephemeral keypair and encapsulates toward Bob's ML-KEM key

    var kem_ss: [ML_KEM_768.SHARED_SECRET_SIZE]u8 = undefined;
    var kem_ct: [ML_KEM_768.CIPHERTEXT_SIZE]u8 = undefined;

    // Call liboqs ML-KEM encapsulation
    const kem_result = OQS_KEM_ml_kem_768_encaps(
        @ptrCast(&kem_ct),
        @ptrCast(&kem_ss),
        @ptrCast(&bob_prekey_bundle.signed_prekey_mlkem),
    );

    if (kem_result != 0) {
        return error.MLKEMEncapsError;
    }

    // === Step 6: Combine 5 shared secrets via HKDF-SHA256 ===

    // Concatenate all shared secrets: DH1 || DH2 || DH3 || KEM_SS (padded)
    var combined: [32 * 5]u8 = undefined;
    @memcpy(combined[0..32], &dh1);
    @memcpy(combined[32..64], &dh2);
    @memcpy(combined[64..96], &dh3);
    @memcpy(combined[96..128], &kem_ss);
    @memset(combined[128..160], 0); // Reserved for future extensibility

    // KDF: HKDF-SHA256
    var root_key: [32]u8 = undefined;
    const info = "Libertaria PQXDH v1";

    const hkdf = std.crypto.kdf.hkdf.HkdfSha256;
    const prk = hkdf.extract(info, combined[0..160]);
    @memcpy(&root_key, &prk);

    return PQXDHInitiatorResult{
        .root_key = root_key,
        .initial_message = .{
            .ephemeral_x25519 = ephemeral_public,
            .mlkem_ciphertext = kem_ct,
        },
        .ephemeral_private = ephemeral_private,
    };
}

// ============================================================================
// PQXDH Key Agreement (Bob Responds)
// ============================================================================

pub const PQXDHResponderResult = struct {
    /// Root key (matches Alice's root key)
    /// Becomes input to Double Ratchet initialization
    root_key: [32]u8,
};

/// Bob responds to Alice's PQXDH initial message
///
/// **Ceremony:**
/// 1. ECDH Bob's signed prekey ↔ Alice's ephemeral (DH1)
/// 2. ECDH Bob's one-time prekey ↔ Alice's ephemeral (DH2)
/// 3. ECDH Bob's identity ↔ Alice's identity (DH3)
/// 4. ML-KEM decapsulate using ciphertext from initial message (KEM1)
/// 5. Combine 5 shared secrets (same order as Alice)
/// 6. KDF via HKDF-SHA256
///
/// **Result:** Root key matching Alice's (should be identical)
pub fn responder(
    bob_identity_private: [32]u8,
    bob_signed_prekey_private: [32]u8,
    bob_one_time_prekey_private: [32]u8,
    bob_mlkem_private: [ML_KEM_768.SECRET_KEY_SIZE]u8,
    alice_identity_public: [32]u8,
    alice_initial_message: *const PQXDHInitialMessage,
) !PQXDHResponderResult {
    _ = bob_identity_private; // Not used in current X3DH variant

    // === Step 1-3: Compute three X25519 shared secrets ===

    // DH1: Bob's signed prekey ↔ Alice's ephemeral
    const dh1 = try crypto.dh.X25519.scalarmult(bob_signed_prekey_private, alice_initial_message.ephemeral_x25519);

    // DH2: Bob's one-time prekey ↔ Alice's ephemeral
    const dh2 = try crypto.dh.X25519.scalarmult(bob_one_time_prekey_private, alice_initial_message.ephemeral_x25519);

    // DH3: Bob's signed prekey ↔ Alice's identity
    // This matches Alice's: alice_identity_private ↔ bob_signed_prekey_public
    const dh3 = try crypto.dh.X25519.scalarmult(bob_signed_prekey_private, alice_identity_public);

    // === Step 4: ML-KEM-768 decapsulation ===

    var kem_ss: [ML_KEM_768.SHARED_SECRET_SIZE]u8 = undefined;

    // Call liboqs ML-KEM decapsulation
    const kem_result = OQS_KEM_ml_kem_768_decaps(
        @ptrCast(&kem_ss),
        @ptrCast(&alice_initial_message.mlkem_ciphertext),
        @ptrCast(&bob_mlkem_private),
    );

    if (kem_result != 0) {
        return error.MLKEMDecapsError;
    }

    // === Step 5-6: Combine secrets and KDF (same as Alice) ===

    var combined: [32 * 5]u8 = undefined;
    @memcpy(combined[0..32], &dh1);
    @memcpy(combined[32..64], &dh2);
    @memcpy(combined[64..96], &dh3);
    @memcpy(combined[96..128], &kem_ss);
    @memset(combined[128..160], 0);

    var root_key: [32]u8 = undefined;
    const info = "Libertaria PQXDH v1";

    const hkdf = std.crypto.kdf.hkdf.HkdfSha256;
    const prk = hkdf.extract(info, combined[0..160]);
    @memcpy(&root_key, &prk);

    return PQXDHResponderResult{
        .root_key = root_key,
    };
}

// ============================================================================
// Tests
// ============================================================================

test "pqxdh prekey bundle serialization" {
    const allocator = std.testing.allocator;

    const bundle = PrekeyBundle{
        .identity_key = [_]u8{0xAA} ** 32,
        .signed_prekey_x25519 = [_]u8{0xBB} ** 32,
        .signed_prekey_signature = [_]u8{0xCC} ** 64,
        .signed_prekey_mlkem = [_]u8{0xDD} ** ML_KEM_768.PUBLIC_KEY_SIZE,
        .one_time_prekey_x25519 = [_]u8{0xEE} ** 32,
        .one_time_prekey_mlkem = [_]u8{0xFF} ** ML_KEM_768.PUBLIC_KEY_SIZE,
    };

    const bytes = try bundle.toBytes(allocator);
    defer allocator.free(bytes);

    const deserialized = try PrekeyBundle.fromBytes(allocator, bytes);

    try std.testing.expectEqualSlices(u8, &bundle.identity_key, &deserialized.identity_key);
    try std.testing.expectEqualSlices(u8, &bundle.signed_prekey_x25519, &deserialized.signed_prekey_x25519);
}

test "pqxdh initial message serialization" {
    const allocator = std.testing.allocator;

    const msg = PQXDHInitialMessage{
        .ephemeral_x25519 = [_]u8{0x11} ** 32,
        .mlkem_ciphertext = [_]u8{0x22} ** ML_KEM_768.CIPHERTEXT_SIZE,
    };

    const bytes = try msg.toBytes(allocator);
    defer allocator.free(bytes);

    const deserialized = try PQXDHInitialMessage.fromBytes(bytes);

    try std.testing.expectEqualSlices(u8, &msg.ephemeral_x25519, &deserialized.ephemeral_x25519);
    try std.testing.expectEqualSlices(u8, &msg.mlkem_ciphertext, &deserialized.mlkem_ciphertext);
}
