import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:webrtc_interface/webrtc_interface.dart';
import 'package:pyramid/core/ice_servers.dart';
import 'package:pyramid/widgets/screen_share_picker.dart';

class PyramidVoipManager extends ChangeNotifier implements WebRTCDelegate {
  static PyramidVoipManager? _instance;
  static PyramidVoipManager get instance => _instance!;

  final Client client;
  late final VoIP voip;
  CallSession? currentCall;
  
  final rtc.RTCVideoRenderer localRenderer = rtc.RTCVideoRenderer();
  final rtc.RTCVideoRenderer remoteRenderer = rtc.RTCVideoRenderer();
  bool _renderersInitialized = false;

  bool _isMicMuted = false;
  bool _isCameraMuted = true;
  bool _isScreensharing = false;
  bool _isRemoteScreensharing = false;
  bool _isDeafened = false;
  bool _isUpdating = false;
  String? _selectedSourceId;
  String _currentQualityKey = 'hd';

  bool get isMicMuted => _isMicMuted;
  bool get isCameraMuted => _isCameraMuted;
  bool get isScreensharing => _isScreensharing;
  bool get isRemoteScreensharing => _isRemoteScreensharing;
  bool get isDeafened => _isDeafened;
  String get currentQualityKey => _currentQualityKey;

  PyramidVoipManager(this.client) {
    _instance = this;
    voip = VoIP(client, this);
    // Suppress speakerphone call — not supported on Windows/Desktop
    try {
      (voip as dynamic).onConfigSpeakerphone = (bool _) async {};
    } catch (_) {}
    _ensureRenderers();
  }

  Future<void> _ensureRenderers() async {
    if (_renderersInitialized) return;
    try {
      await localRenderer.initialize();
      await remoteRenderer.initialize();
      _renderersInitialized = true;
    } catch (e) {
      debugPrint('[VOIP] Renderer init error: $e');
    }
  }

  @override
  MediaDevices get mediaDevices => MediaDevicesWrapper(rtc.navigator.mediaDevices, this);

  @override
  bool get isWeb => kIsWeb;

  @override
  Future<RTCPeerConnection> createPeerConnection(
    Map<String, dynamic> configuration, [
    Map<String, dynamic> constraints = const {},
  ]) async {
    // Zusätzliche ICE-Server (STUN/TURN aus ice.json) zu den vom Homeserver
    // gelieferten mergen — ohne STUN scheitern Calls außerhalb des LANs,
    // weil der Server hinter CGNAT hängt.
    try {
      final extra = await IceServers.get();
      final merged = Map<String, dynamic>.from(configuration);
      final existing =
          (merged['iceServers'] as List?)?.cast<Map<String, dynamic>>() ?? [];
      merged['iceServers'] = [...existing, ...extra];
      configuration = merged;
    } catch (e) {
      debugPrint('[VOIP] ICE server merge error: $e');
    }
    return rtc.createPeerConnection(configuration, constraints);
  }

  @override
  Future<void> handleNewCall(CallSession call) async {
    debugPrint('[VOIP] Incoming call ${call.callId} (type: ${call.type})');
    await _ensureRenderers();
    currentCall = call;
    _isCameraMuted = call.type == CallType.kVoice;
    _setupCallListeners(call);
    _updateInternalStates();
    notifyListeners();
  }

  Timer? _renderRefreshTimer;
  void _setupCallListeners(CallSession call) {
    call.onCallStateChanged.stream.listen((state) {
      debugPrint('[VOIP] State: $state');
      _updateInternalStates();

      if (state == CallState.kConnected) {
        if (!_isCameraMuted) {
          debugPrint('[VOIP] Ensuring camera is unmuted on connect');
          call.setLocalVideoMuted(false);
        }
      }
      _updateRenderers();
    });

    _renderRefreshTimer?.cancel();
    _renderRefreshTimer = Timer.periodic(
      const Duration(milliseconds: 1000), // Faster polling to catch feeds
      (_) => _updateRenderers(),
    );

    _updateRenderers();
  }

  void _updateInternalStates() {
    if (currentCall == null) return;
    final dynamic c = currentCall;
    try {
      _isMicMuted = c.isMicrophoneMuted == true;
      _isCameraMuted = c.isLocalVideoMuted == true;
    } catch (_) {}
    notifyListeners();
  }

  Future<void> _updateRenderers() async {
    if (currentCall == null || _isUpdating) return;
    _isUpdating = true;

    try {
      await _ensureRenderers();
      final dynamic c = currentCall;
      rtc.MediaStream? remote;
      rtc.MediaStream? local;

      rtc.MediaStream? extract(dynamic obj) {
        if (obj == null) return null;
        if (obj is rtc.MediaStream) return obj;
        try { return obj.stream as rtc.MediaStream?; } catch (_) {}
        return null;
      }

      final List<dynamic> feeds = [];
      try { 
        final dynamic sdkFeeds = c.getFeeds();
        if (sdkFeeds is Iterable) feeds.addAll(sdkFeeds.toList()); 
      } catch (_) {}
      
      bool remoteScreenFound = false;
      bool localScreenFound = false;

      for (final dynamic f in feeds) {
        final s = extract(f);
        if (s == null) continue;
        
        bool isLocal = false;
        try { isLocal = f.isLocal == true || f.userId == client.userID; } catch (_) {}
        
        String purpose = 'm.usermedia';
        try { purpose = f.purpose ?? 'm.usermedia'; } catch (_) {}
        
        if (isLocal) {
          if (local == null || purpose == 'm.screenshare') {
            local = s;
            if (purpose == 'm.screenshare') localScreenFound = true;
          }
        } else {
          if (remote == null || purpose == 'm.screenshare') {
            remote = s;
            if (purpose == 'm.screenshare') remoteScreenFound = true;
          }
        }
      }

      _isScreensharing = localScreenFound;
      _isRemoteScreensharing = remoteScreenFound;

      if (remote == null) {
        try { remote = extract(c.remoteUserMediaStream); } catch (_) {}
      }
      if (remote == null) {
        try { remote = extract(c.remoteStream); } catch (_) {}
      }
      if (local == null) {
        try { local = extract(c.localUserMediaStream); } catch (_) {}
      }

      // Ausgabelautstärke (Settings-Slider) auf den Remote-Stream anwenden —
      // nur einmal pro Stream, _updateRenderers läuft auch per Timer.
      if (remote != null && remote.id != _volumeAppliedStreamId) {
        _volumeAppliedStreamId = remote.id;
        _remoteAudioStream = remote;
        applyOutputVolume();
      }

      // Final check for tracks before assigning
      bool changed = false;
      if (remote != null && remote.getVideoTracks().isNotEmpty) {
        if (remoteRenderer.srcObject?.id != remote.id) {
          debugPrint('[VOIP] Assigning Remote Renderer: ${remote.id} (${remote.getVideoTracks().length} video tracks)');
          remoteRenderer.srcObject = remote;
          changed = true;
        }
      } else if (remoteRenderer.srcObject != null) {
        remoteRenderer.srcObject = null;
        changed = true;
      }

      if (local != null && local.getVideoTracks().isNotEmpty) {
        if (localRenderer.srcObject?.id != local.id) {
          debugPrint('[VOIP] Assigning Local Renderer: ${local.id} (${local.getVideoTracks().length} video tracks)');
          localRenderer.srcObject = local;
          changed = true;
        }
      } else if (localRenderer.srcObject != null) {
        localRenderer.srcObject = null;
        changed = true;
      }
      
      if (changed) notifyListeners();
    } catch (e) {
      debugPrint('[VOIP] Error in _updateRenderers: $e');
    } finally {
      _isUpdating = false;
    }
  }

  @override
  Future<void> handleCallEnded(CallSession session) async {
    if (currentCall?.callId == session.callId) {
      _renderRefreshTimer?.cancel();
      _renderRefreshTimer = null;
      localRenderer.srcObject = null;
      remoteRenderer.srcObject = null;
      _remoteAudioStream = null;
      _volumeAppliedStreamId = null;
      currentCall = null;
      _isScreensharing = false;
      _isRemoteScreensharing = false;
      notifyListeners();
    }
  }

  // ── Ausgabelautstärke (Settings-Slider "voice_output_volume") ─────────────
  rtc.MediaStream? _remoteAudioStream;
  String? _volumeAppliedStreamId;

  /// Wendet die in den Einstellungen gewählte Ausgabelautstärke auf den
  /// Remote-Audio-Stream an. Ohne [value] wird der Pref-Wert gelesen.
  Future<void> applyOutputVolume([double? value]) async {
    var v = value;
    if (v == null) {
      try {
        final prefs = await SharedPreferences.getInstance();
        v = prefs.getDouble('voice_output_volume') ?? 1.0;
      } catch (_) {
        v = 1.0;
      }
    }
    final stream = _remoteAudioStream;
    if (stream == null) return;
    for (final track in stream.getAudioTracks()) {
      try {
        await rtc.Helper.setVolume(v.clamp(0.0, 2.0), track);
      } catch (_) {}
    }
  }

  @override
  Future<void> handleMissedCall(CallSession session) async {
    if (currentCall?.callId == session.callId) {
      currentCall = null;
      notifyListeners();
    }
  }

  @override
  Future<void> handleNewGroupCall(GroupCallSession groupCall) async {}
  @override
  Future<void> handleGroupCallEnded(GroupCallSession groupCall) async {}
  @override
  bool get canHandleNewCall => currentCall == null;
  @override
  Future<void> playRingtone() async {}
  @override
  Future<void> stopRingtone() async {}
  @override
  EncryptionKeyProvider? get keyProvider => null;
  @override
  Future<void> registerListeners(CallSession session) async {}

  Future<void> safeAction(Future<dynamic> Function() action) async {
    try {
      await action();
      await Future.delayed(const Duration(milliseconds: 1000));
    } catch (e) {
      debugPrint('[VOIP] Action error: $e');
    }
    _updateInternalStates();
    _updateRenderers();
  }

  Future<void> toggleMic() async {
    if (currentCall == null) return;
    _isMicMuted = !_isMicMuted;
    notifyListeners();
    try {
      await currentCall!.setMicrophoneMuted(_isMicMuted);
    } catch (e) {
      _isMicMuted = !_isMicMuted;
      notifyListeners();
    }
  }

  Future<void> toggleCamera() async {
    if (currentCall == null) return;
    _isCameraMuted = !_isCameraMuted;
    notifyListeners();
    try {
      await currentCall!.setLocalVideoMuted(_isCameraMuted);
      for (var i = 0; i < 5; i++) {
        await Future.delayed(Duration(milliseconds: 300 + (i * 200)));
        await _updateRenderers();
      }
    } catch (e) {
      debugPrint('[VOIP] toggleCamera error: $e');
      _isCameraMuted = !_isCameraMuted;
      notifyListeners();
    }
    _updateInternalStates();
    await _updateRenderers();
  }

  Future<void> toggleScreenSharing([BuildContext? context]) async {
    if (currentCall == null) return;
    final call = currentCall!;
    
    if (_isScreensharing) {
      await safeAction(() => call.setScreensharingEnabled(false));
      return;
    }

    if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows && context != null) {
      final selectedSource = await ScreenSharePicker.show(context);
      if (selectedSource == null) return;
      _selectedSourceId = selectedSource.id;
    } else {
      _selectedSourceId = null;
    }

    await safeAction(() async {
      await call.setScreensharingEnabled(true);
    });
  }

  Future<void> toggleDeafen() async {
    _isDeafened = !_isDeafened;
    notifyListeners();
  }

  Future<void> hangup() async {
    if (currentCall == null) return;
    await safeAction(() async {
      await currentCall!.hangup(reason: CallErrorCode.userHangup);
      currentCall = null;
    });
  }

  Future<void> startCall(String roomId, {bool video = false}) async {
    final room = client.getRoomById(roomId);
    if (room == null) return;
    await _ensureRenderers();
    _isCameraMuted = !video; 
    
    debugPrint('[VOIP] Starting call in room $roomId (video=$video)');
    final call = await voip.inviteToCall(room, video ? CallType.kVideo : CallType.kVoice);
    currentCall = call;
    
    _setupCallListeners(call);
    
    if (video) {
      await Future.delayed(const Duration(milliseconds: 800));
      await call.setLocalVideoMuted(false);
      await _updateRenderers();
    }
    
    _updateInternalStates();
    notifyListeners();
  }

  Future<void> changeQuality(String key) async {
    _currentQualityKey = key;
    if (currentCall == null) {
      notifyListeners();
      return;
    }

    if (!_isCameraMuted) {
      await toggleCamera();
      await Future.delayed(const Duration(milliseconds: 300));
      await toggleCamera();
    }
    
    if (_isScreensharing) {
      final String? oldSource = _selectedSourceId;
      await safeAction(() => currentCall!.setScreensharingEnabled(false));
      await Future.delayed(const Duration(milliseconds: 300));
      _selectedSourceId = oldSource;
      await safeAction(() => currentCall!.setScreensharingEnabled(true));
    }
    
    notifyListeners();
  }
}

class MediaDevicesWrapper extends MediaDevices {
  final MediaDevices _delegate;
  final PyramidVoipManager _manager;
  MediaDevicesWrapper(this._delegate, this._manager);

  @override
  Future<rtc.MediaStream> getUserMedia(Map<String, dynamic> constraints) async {
    final Map<String, dynamic> improvedConstraints = Map.from(constraints);
    
    final String? sourceId = _manager._selectedSourceId;
    if (sourceId != null) {
      if (improvedConstraints['video'] is! Map) {
        improvedConstraints['video'] = {};
      }
      
      final v = Map<String, dynamic>.from(improvedConstraints['video']);
      v['mandatory'] ??= {};
      v['mandatory']['chromeMediaSource'] = 'desktop';
      v['mandatory']['chromeMediaSourceId'] = sourceId;
      improvedConstraints['video'] = v;
    }

    // In den Einstellungen gewählte Geräte (voice_*_device Prefs) anwenden —
    // nur wenn kein Screenshare-Source gesetzt ist (sonst nicht anfassen).
    try {
      final prefs = await SharedPreferences.getInstance();
      final mic = prefs.getString('voice_mic_device');
      final cam = prefs.getString('voice_camera_device');
      final speaker = prefs.getString('voice_speaker_device');
      if (mic != null &&
          improvedConstraints['audio'] != null &&
          improvedConstraints['audio'] != false) {
        final a = improvedConstraints['audio'] is Map
            ? Map<String, dynamic>.from(improvedConstraints['audio'])
            : <String, dynamic>{};
        a['deviceId'] = mic;
        a['optional'] = [
          ...?(a['optional'] as List?),
          {'sourceId': mic},
        ];
        improvedConstraints['audio'] = a;
      }
      if (cam != null &&
          sourceId == null &&
          improvedConstraints['video'] != null &&
          improvedConstraints['video'] != false) {
        final v = improvedConstraints['video'] is Map
            ? Map<String, dynamic>.from(improvedConstraints['video'])
            : <String, dynamic>{};
        v['deviceId'] = cam;
        improvedConstraints['video'] = v;
      }
      if (speaker != null) {
        rtc.Helper.selectAudioOutput(speaker).catchError((_) {});
      }
    } catch (_) {}

    if (improvedConstraints['video'] != null && improvedConstraints['video'] != false) {
      final String q = _manager.currentQualityKey;
      int width = 1280;
      int height = 720;
      
      if (q == 'sd') { width = 640; height = 360; }
      else if (q == 'fhd') { width = 1920; height = 1080; }
      else if (q == '4k') { width = 3840; height = 2160; }

      if (improvedConstraints['video'] is bool) {
        improvedConstraints['video'] = {
          'width': {'ideal': width},
          'height': {'ideal': height},
          'frameRate': {'ideal': 30},
        };
      } else if (improvedConstraints['video'] is Map) {
        final v = Map<String, dynamic>.from(improvedConstraints['video']);
        v['width'] = {'ideal': width};
        v['height'] = {'ideal': height};
        improvedConstraints['video'] = v;
      }
    }
    return _delegate.getUserMedia(improvedConstraints);
  }

  @override
  Future<rtc.MediaStream> getDisplayMedia(Map<String, dynamic> constraints) async {
    final String? sourceId = _manager._selectedSourceId;
    if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows && sourceId != null) {
      final desktopConstraints = {
        'video': {
          'mandatory': {
            'chromeMediaSource': 'desktop',
            'chromeMediaSourceId': sourceId,
          },
          'optional': [],
        },
        'audio': false,
      };
      return _delegate.getDisplayMedia(desktopConstraints);
    }
    return _delegate.getDisplayMedia(constraints);
  }

  @override
  Future<List<MediaDeviceInfo>> enumerateDevices() => _delegate.enumerateDevices();
  @override
  MediaTrackSupportedConstraints getSupportedConstraints() => _delegate.getSupportedConstraints();
  @override
  set ondevicechange(Function(dynamic event)? callback) => _delegate.ondevicechange = callback;
  @override
  Function(dynamic event)? get ondevicechange => _delegate.ondevicechange;
  // Erzwungener Override der abstrakten MediaDevices-Klasse, die getSources()
  // selbst als deprecated markiert (Ersatz: enumerateDevices()) aber weiterhin verlangt.
  @override
  // ignore: deprecated_member_use
  Future<List<dynamic>> getSources() => _delegate.getSources();
  @override
  Future<MediaDeviceInfo> selectAudioOutput([AudioOutputOptions? options]) => _delegate.selectAudioOutput(options);
}
