// Version: 1.0.0 | Created: 2026-04-10
// E2EE key exchange for LiveKit calls via Matrix to-device events.
// Implements the m.rtc.encryption_keys protocol for interop with Element X.

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:matrix/matrix.dart' show Client, DeviceKeys, ToDeviceEvent;

/// Event type used by Element X / Element Call for key exchange.
const kEncryptionKeysEventType = 'm.rtc.encryption_keys';

/// Manages E2EE key generation, distribution, and reception for LiveKit calls.
///
/// Flow:
/// 1. [init] creates a per-participant [BaseKeyProvider] and generates a local key.
/// 2. After LiveKit connects, call [setLocalKey] with the local participant identity.
/// 3. Call [sendKeyToParticipants] to share our key via encrypted to-device events.
/// 4. Incoming keys from Element X are received via [Client.onToDeviceEvent] and
///    set on the [BaseKeyProvider] automatically.
class CallE2EEManager {
  CallE2EEManager({
    required Client client,
    required String matrixRoomId,
  })  : _client = client,
        _matrixRoomId = matrixRoomId;

  final Client _client;
  final String _matrixRoomId;
  BaseKeyProvider? _keyProvider;
  StreamSubscription<ToDeviceEvent>? _toDeviceSub;
  Uint8List? _localKey;
  final int _localKeyIndex = 0;

  BaseKeyProvider? get keyProvider => _keyProvider;

  /// Create the key provider and start listening for incoming keys.
  Future<BaseKeyProvider> init() async {
    _keyProvider = await BaseKeyProvider.create(sharedKey: false);

    // Generate a random 32-byte encryption key for our media tracks.
    _localKey = _secureRandomBytes(32);

    // Listen for incoming encryption keys from other call participants.
    _toDeviceSub = _client.onToDeviceEvent.stream.listen(_onToDeviceEvent);

    debugPrint('[E2EE] Key provider created, local key generated '
        '(${base64Encode(_localKey!).substring(0, 8)}…)');
    return _keyProvider!;
  }

  /// Set our local key on the provider using the LiveKit participant identity.
  /// Call this AFTER the LiveKit Room is connected and localParticipant is available.
  Future<void> setLocalKey(String localIdentity) async {
    if (_keyProvider == null || _localKey == null) return;
    await _keyProvider!.setRawKey(
      _localKey!,
      participantId: localIdentity,
      keyIndex: _localKeyIndex,
    );
    debugPrint('[E2EE] Local key set for identity=$localIdentity');
  }

  /// Send our encryption key to all other call members via encrypted to-device.
  Future<void> sendKeyToParticipants() async {
    if (_localKey == null) return;

    final room = _client.getRoomById(_matrixRoomId);
    if (room == null) {
      debugPrint('[E2EE] Room $_matrixRoomId not found — cannot send keys');
      return;
    }

    // Find other participants from the MSC3401 call.member state events.
    final memberEvents =
        room.states['org.matrix.msc3401.call.member'] ?? {};

    final content = <String, Object>{
      'keys': [
        {'index': _localKeyIndex, 'key': base64Encode(_localKey!)},
      ],
      'call_id': '',
      'device_id': _client.deviceID ?? '',
      'room_id': _matrixRoomId,
    };

    for (final entry in memberEvents.entries) {
      final userId = entry.key;
      if (userId == _client.userID) continue;

      // Parse device_id from the call.member event to target the correct device.
      final memberships =
          entry.value.content['memberships'] as List<dynamic>? ?? [];
      if (memberships.isEmpty) continue;

      await _client.userDeviceKeysLoading;
      final deviceKeyMap = _client.userDeviceKeys[userId]?.deviceKeys;
      if (deviceKeyMap == null || deviceKeyMap.isEmpty) {
        debugPrint('[E2EE] No device keys for $userId — skipping');
        continue;
      }

      // Send to all devices of this user that are in the call.
      final List<DeviceKeys> targets = [];
      for (final m in memberships) {
        final deviceId = m['device_id'] as String?;
        if (deviceId != null && deviceKeyMap.containsKey(deviceId)) {
          targets.add(deviceKeyMap[deviceId]!);
        }
      }

      // Fallback: if no specific device matched, send to all devices.
      if (targets.isEmpty) {
        targets.addAll(deviceKeyMap.values);
      }

      try {
        await _client.sendToDeviceEncrypted(
          targets,
          kEncryptionKeysEventType,
          content,
        );
        debugPrint(
            '[E2EE] Sent key to $userId (${targets.length} device(s))');
      } catch (e) {
        debugPrint('[E2EE] Failed to send key to $userId: $e');
      }
    }
  }

  /// Handle incoming to-device encryption key events from other participants.
  void _onToDeviceEvent(ToDeviceEvent event) {
    if (event.type != kEncryptionKeysEventType) return;

    // Only process keys for our active call room.
    final eventRoomId = event.content['room_id'] as String?;
    if (eventRoomId != null && eventRoomId != _matrixRoomId) return;

    final keys = event.content['keys'] as List<dynamic>?;
    if (keys == null || keys.isEmpty) {
      debugPrint(
          '[E2EE] Received $kEncryptionKeysEventType with empty keys '
          'from ${event.sender}');
      return;
    }

    final senderId = event.sender;
    debugPrint('[E2EE] Received encryption key(s) from $senderId');

    for (final keyEntry in keys) {
      final index = (keyEntry['index'] as num?)?.toInt() ?? 0;
      final keyB64 = keyEntry['key'] as String?;
      if (keyB64 == null) continue;

      final keyBytes = base64Decode(keyB64);

      // The participant identity in LiveKit should match the Matrix user ID
      // (set by lk-jwt-service). Log both for debugging.
      _keyProvider?.setRawKey(
        keyBytes,
        participantId: senderId,
        keyIndex: index,
      );
      debugPrint(
          '[E2EE] Key set for participant=$senderId index=$index '
          '(${keyB64.substring(0, 8)}…)');
    }
  }

  Future<void> dispose() async {
    await _toDeviceSub?.cancel();
    _toDeviceSub = null;
    _localKey = null;
    _keyProvider = null;
    debugPrint('[E2EE] Disposed');
  }

  static Uint8List _secureRandomBytes(int length) {
    final rng = Random.secure();
    return Uint8List.fromList(
        List<int>.generate(length, (_) => rng.nextInt(256)));
  }
}
