import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/export.dart';

/// Derives and stores the 256-bit session key from a user-supplied passphrase.
///
/// Key derivation: PBKDF2-HMAC-SHA256 with a random salt.
/// The salt is kept in memory for the session; both peers must use the SAME
/// passphrase and DIFFERENT salts are handled by transmitting the salt
/// in the handshake packet before the session starts.
class KeyManager {
  static const int _keyLen  = 32; // AES-256
  static const int _saltLen = 16;
  static const int _iters   = 100000;

  Uint8List? _key;
  Uint8List? _salt;

  /// True once [deriveKey] has been called successfully.
  bool get hasKey => _key != null;

  /// The derived 32-byte key (throws if not yet derived).
  Uint8List get key {
    final k = _key;
    if (k == null) throw StateError('Key not derived yet');
    return k;
  }

  /// The 16-byte salt used during derivation (for sharing with the peer).
  Uint8List get salt {
    final s = _salt;
    if (s == null) throw StateError('Salt not available');
    return s;
  }

  // ── Derivation ─────────────────────────────────────────────────────

  /// Derive a 256-bit key from [passphrase] using PBKDF2-HMAC-SHA256.
  ///
  /// If [salt] is provided (received from the peer during handshake),
  /// it is used directly. Otherwise a fresh random salt is generated.
  ///
  /// Throws [ArgumentError] if the passphrase is empty.
  void deriveKey(String passphrase, {Uint8List? salt}) {
    if (passphrase.isEmpty) {
      throw ArgumentError('Passphrase must not be empty');
    }

    // Use provided salt or generate one.
    final usedSalt = salt ?? _generateSalt();
    _salt = usedSalt;

    final passwordBytes = Uint8List.fromList(utf8.encode(passphrase));

    // PBKDF2-HMAC-SHA256 via PointyCastle.
    final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64))
      ..init(Pbkdf2Parameters(usedSalt, _iters, _keyLen));

    _key = pbkdf2.process(passwordBytes);
  }

  /// Load a raw 32-byte key directly (for testing or pre-shared key import).
  void loadRawKey(Uint8List rawKey) {
    if (rawKey.length != _keyLen) {
      throw ArgumentError('Raw key must be $_keyLen bytes');
    }
    _key = Uint8List.fromList(rawKey);
    _salt = Uint8List(_saltLen); // zero salt when using raw key
  }

  /// Clear the key from memory (call on session teardown).
  void clear() {
    if (_key != null) {
      _key!.fillRange(0, _key!.length, 0);
      _key = null;
    }
    _salt = null;
  }

  // ── Nonce construction ─────────────────────────────────────────────

  /// Build a 16-byte AES-CTR nonce for a given packet sequence number.
  ///
  /// Layout: [salt[0..7] ∥ sessionId[0..3] ∥ seqHigh ∥ seqLow ∥ 00 00 00 00]
  /// (128-bit nonce; the counter part at the end starts at 0 per-block.)
  ///
  /// Using the salt's first 8 bytes + session ID + seq ensures that
  /// each packet has a unique nonce without transmitting the full nonce.
  Uint8List buildNonce(Uint8List sessionId, int seq) {
    final nonce = Uint8List(16);
    // First 8 bytes from salt (or zeros if salt unavailable).
    final s = _salt;
    if (s != null) {
      for (int i = 0; i < 8 && i < s.length; i++) { nonce[i] = s[i]; }
    }
    // Next 4 bytes: session ID.
    for (int i = 0; i < 4 && i < sessionId.length; i++) {
      nonce[8 + i] = sessionId[i];
    }
    // Last 4 bytes: packet sequence number (big-endian).
    nonce[12] = (seq >> 24) & 0xFF;
    nonce[13] = (seq >> 16) & 0xFF;
    nonce[14] = (seq >>  8) & 0xFF;
    nonce[15] =  seq        & 0xFF;
    return nonce;
  }

  // ── Private helpers ────────────────────────────────────────────────

  static Uint8List _generateSalt() {
    final rng = FortunaRandom();
    // Seed Fortuna with timestamp + hash-spread bytes.
    final seed = Uint8List(32);
    final ts = DateTime.now().microsecondsSinceEpoch;
    for (int i = 0; i < 8; i++) {
      seed[i] = (ts >> (i * 8)) & 0xFF;
    }
    rng.seed(KeyParameter(seed));
    return rng.nextBytes(_saltLen);
  }
}
