import 'dart:async';
import 'dart:io' show Platform, ProcessInfo;

import 'package:flutter/scheduler.dart';

import '../../../../mpv/mpv.dart';
import '../../../../mpv/player/platform/player_android.dart';
import '../../../../utils/app_logger.dart';
import 'performance_stats.dart';

/// Service that polls player properties and provides performance stats via a stream.
///
/// Supports both MPV (desktop/iOS) and ExoPlayer (Android) backends.
/// Handles runtime backend switching (e.g., ExoPlayer -> MPV fallback on Android).
///
/// Usage:
/// ```dart
/// final service = PerformanceStatsService(player);
/// service.startPolling();
/// service.statsStream.listen((stats) => print(stats.resolution));
/// service.stopPolling();
/// service.dispose();
/// ```
class PerformanceStatsService {
  final Player player;
  Timer? _pollingTimer;
  final _statsController = StreamController<PerformanceStats>.broadcast();

  /// The interval between stats updates.
  static const pollInterval = Duration(milliseconds: 500);

  // FPS tracking
  int _frameCount = 0;
  DateTime _lastFpsUpdate = DateTime.now();
  double? _currentUiFps;
  bool _fpsTrackingActive = false;
  bool _fpsCallbackRegistered = false;

  // Track runtime player type for logging (can differ from Dart object type after fallback)
  // Values: 'exoplayer', 'mpv', or 'unknown'
  String _runtimePlayerType = 'unknown';
  StreamSubscription<void>? _backendSwitchedSubscription;

  PerformanceStatsService(this.player);

  /// Stream of performance stats updates.
  Stream<PerformanceStats> get statsStream => _statsController.stream;

  /// Start polling for stats at regular intervals.
  void startPolling() {
    _pollingTimer?.cancel();

    // Listen for backend switches on Android (ExoPlayer -> MPV fallback)
    if (player is PlayerAndroid) {
      _backendSwitchedSubscription?.cancel();
      _backendSwitchedSubscription = player.streams.backendSwitched.listen((_) {
        _updateRuntimePlayerType();
      });
    }

    // Start FPS tracking
    _startFpsTracking();
    // Fetch immediately, then poll
    _fetchStats();
    _pollingTimer = Timer.periodic(pollInterval, (_) => _fetchStats());
  }

  /// Update the runtime player type by querying the native layer.
  Future<void> _updateRuntimePlayerType() async {
    if (player is PlayerAndroid) {
      _runtimePlayerType = await (player as PlayerAndroid).getPlayerType();
      appLogger.d('Performance stats: runtime player type updated to $_runtimePlayerType');
    } else {
      _runtimePlayerType = 'mpv'; // Non-Android always uses MPV
    }
  }

  /// Start tracking UI frame rate.
  void _startFpsTracking() {
    _frameCount = 0;
    _lastFpsUpdate = DateTime.now();
    _fpsTrackingActive = true;
    if (!_fpsCallbackRegistered) {
      _fpsCallbackRegistered = true;
      SchedulerBinding.instance.addPersistentFrameCallback(_onFrame);
    }
  }

  /// Called every frame to count FPS.
  void _onFrame(Duration timestamp) {
    if (!_fpsTrackingActive) return;
    _frameCount++;
    final now = DateTime.now();
    final elapsed = now.difference(_lastFpsUpdate);
    if (elapsed.inMilliseconds >= 1000) {
      _currentUiFps = _frameCount * 1000 / elapsed.inMilliseconds;
      _frameCount = 0;
      _lastFpsUpdate = now;
    }
  }

  /// Stop polling for stats.
  void stopPolling() {
    _pollingTimer?.cancel();
    _pollingTimer = null;
    _fpsTrackingActive = false;
    _currentUiFps = null;
  }

  /// Fetch all performance stats from the player.
  Future<void> _fetchStats() async {
    try {
      // Ensure we know the runtime type on first fetch
      if (_runtimePlayerType == 'unknown') {
        await _updateRuntimePlayerType();
      }

      if (player is PlayerAndroid) {
        // For Android (ExoPlayer or MPV fallback), always use getStats()
        // The native side returns appropriate stats based on which backend is active
        await _fetchAndroidStats();
      } else {
        // For non-Android platforms, use MPV property queries
        await _fetchMpvStats();
      }
    } catch (e) {
      appLogger.w('Failed to fetch performance stats', error: e);
    }
  }

  /// Fetch stats from Android player (ExoPlayer or MPV fallback).
  /// The native side returns appropriate stats based on the active backend.
  Future<void> _fetchAndroidStats() async {
    final androidPlayer = player as PlayerAndroid;
    final statsMap = await androidPlayer.getStats();
    final playerType = statsMap['playerType'] as String? ?? 'unknown';

    // Get app memory usage
    int? appMemory;
    try {
      appMemory = ProcessInfo.currentRss;
    } catch (_) {}

    if (playerType == 'mpv') {
      // Parse MPV stats format (returned when in fallback mode)
      final stats = PerformanceStats(
        playerType: 'mpv',
        videoCodec: _formatCodecName(statsMap['video-codec'] as String?),
        videoWidth: _parseInt(statsMap['video-params/w'] as String?),
        videoHeight: _parseInt(statsMap['video-params/h'] as String?),
        videoFps: _parseDouble(statsMap['container-fps'] as String?),
        actualFps: _parseDouble(statsMap['estimated-vf-fps'] as String?),
        videoBitrate: _parseInt(statsMap['video-bitrate'] as String?),
        hwdecCurrent: statsMap['hwdec-current'] as String?,
        audioCodec: _formatCodecName(statsMap['audio-codec-name'] as String?),
        audioSamplerate: _parseInt(statsMap['audio-params/samplerate'] as String?),
        audioChannels: statsMap['audio-params/hr-channels'] as String?,
        audioBitrate: _parseInt(statsMap['audio-bitrate'] as String?),
        avsyncChange: _parseDouble(statsMap['total-avsync-change'] as String?),
        cacheUsed: _parseInt(statsMap['cache-used'] as String?),
        cacheSpeed: _parseDouble(statsMap['cache-speed'] as String?),
        displayFps: _parseDouble(statsMap['display-fps'] as String?),
        frameDropCount: _parseInt(statsMap['frame-drop-count'] as String?),
        decoderFrameDropCount: _parseInt(statsMap['decoder-frame-drop-count'] as String?),
        cacheDuration: _parseDouble(statsMap['demuxer-cache-duration'] as String?),
        // Color/Format properties
        pixelformat: statsMap['video-params/pixelformat'] as String?,
        hwPixelformat: statsMap['video-params/hw-pixelformat'] as String?,
        colormatrix: statsMap['video-params/colormatrix'] as String?,
        primaries: statsMap['video-params/primaries'] as String?,
        gamma: statsMap['video-params/gamma'] as String?,
        // HDR metadata
        maxLuma: _parseDouble(statsMap['video-params/max-luma'] as String?),
        minLuma: _parseDouble(statsMap['video-params/min-luma'] as String?),
        maxCll: _parseDouble(statsMap['video-params/max-cll'] as String?),
        maxFall: _parseDouble(statsMap['video-params/max-fall'] as String?),
        // Other
        aspectName: statsMap['video-params/aspect-name'] as String?,
        rotate: _parseInt(statsMap['video-params/rotate'] as String?),
        appMemoryBytes: appMemory,
        uiFps: _currentUiFps,
      );
      _statsController.add(stats);
    } else {
      // Parse ExoPlayer stats format
      final stats = PerformanceStats(
        playerType: 'exoplayer',
        // Video metrics
        videoCodec: _formatCodecName(statsMap['videoCodec'] as String?),
        videoWidth: statsMap['videoWidth'] as int?,
        videoHeight: statsMap['videoHeight'] as int?,
        videoFps: (statsMap['videoFps'] as num?)?.toDouble(),
        videoBitrate: statsMap['videoBitrate'] as int?,
        videoDecoderName: statsMap['videoDecoderName'] as String?,
        // Audio metrics
        audioCodec: _formatCodecName(statsMap['audioCodec'] as String?),
        audioSamplerate: statsMap['audioSampleRate'] as int?,
        audioChannels: _formatChannels(statsMap['audioChannels'] as int?),
        audioBitrate: statsMap['audioBitrate'] as int?,
        audioDecoderName: statsMap['audioDecoderName'] as String?,
        // Tunneling
        tunneledPlayback: statsMap['tunneledPlayback'] == true,
        tunnelingStatus: statsMap['tunnelingStatus'] as String?,
        // Performance metrics
        frameDropCount: statsMap['videoDroppedFrames'] as int?,
        // Buffer metrics - convert ms to seconds for duration
        cacheDuration: ((statsMap['totalBufferedDurationMs'] as int?) ?? 0) / 1000.0,
        // DV conversion
        dvConversionActive: statsMap['dvConversionActive'] == true,
        dvConversionMode: statsMap['dvConversionMode'] as String? ?? '',
        dvConvertedRpus: (statsMap['dvConvertedRpus'] as num?)?.toInt(),
        dvRpuConversionFailures: (statsMap['dvRpuConversionFailures'] as num?)?.toInt(),
        dvRpuOutputTooSmall: (statsMap['dvRpuOutputTooSmall'] as num?)?.toInt(),
        dvAvgRpuConversionUs: (statsMap['dvAvgRpuConversionUs'] as num?)?.toInt(),
        dvAvgSampleProcessingUs: (statsMap['dvAvgSampleProcessingUs'] as num?)?.toInt(),
        // App metrics
        appMemoryBytes: appMemory,
        uiFps: _currentUiFps,
      );
      _statsController.add(stats);
    }
  }

  /// Format channel count to string (e.g., "2" -> "Stereo", "6" -> "5.1")
  String? _formatChannels(int? channels) {
    if (channels == null) return null;
    return switch (channels) {
      1 => 'Mono',
      2 => 'Stereo',
      6 => '5.1',
      8 => '7.1',
      _ => '$channels ch',
    };
  }

  /// Fetch stats from MPV via property queries.
  Future<void> _fetchMpvStats() async {
    // Fetch core properties in parallel
    final results = await Future.wait([
      player.getProperty('video-codec'), // 0
      player.getProperty('video-params/w'), // 1
      player.getProperty('video-params/h'), // 2
      player.getProperty('container-fps'), // 3
      player.getProperty('estimated-vf-fps'), // 4
      player.getProperty('video-bitrate'), // 5
      player.getProperty('hwdec-current'), // 6
      player.getProperty('audio-codec-name'), // 7
      player.getProperty('audio-params/samplerate'), // 8
      player.getProperty('audio-params/hr-channels'), // 9
      player.getProperty('audio-bitrate'), // 10
      player.getProperty('total-avsync-change'), // 11
      player.getProperty('cache-speed'), // 12
      player.getProperty('frame-drop-count'), // 13
      player.getProperty('decoder-frame-drop-count'), // 14
      player.getProperty('demuxer-cache-duration'), // 15
    ]);

    final hasVideo = results[1] != null;

    // Only query video-dependent properties when a video track is active.
    // On Android, skip properties that are typically unavailable (display-fps,
    // hw-pixelformat, HDR metadata) — MPV's native layer logs errors for these
    // that we cannot suppress.
    List<String?>? videoResults;
    if (hasVideo) {
      final isAndroid = Platform.isAndroid;
      videoResults = await Future.wait([
        isAndroid ? Future.value(null) : player.getProperty('display-fps'), // 0
        player.getProperty('video-params/pixelformat'), // 1
        isAndroid ? Future.value(null) : player.getProperty('video-params/hw-pixelformat'), // 2
        player.getProperty('video-params/colormatrix'), // 3
        player.getProperty('video-params/primaries'), // 4
        player.getProperty('video-params/gamma'), // 5
        isAndroid ? Future.value(null) : player.getProperty('video-params/max-luma'), // 6
        isAndroid ? Future.value(null) : player.getProperty('video-params/min-luma'), // 7
        isAndroid ? Future.value(null) : player.getProperty('video-params/max-cll'), // 8
        isAndroid ? Future.value(null) : player.getProperty('video-params/max-fall'), // 9
        player.getProperty('video-params/aspect-name'), // 10
        player.getProperty('video-params/rotate'), // 11
      ]);
    }

    // Get app memory usage
    int? appMemory;
    try {
      appMemory = ProcessInfo.currentRss;
    } catch (_) {
      // ProcessInfo not available on all platforms
    }

    final stats = PerformanceStats(
      playerType: 'mpv',
      videoCodec: _formatCodecName(results.first),
      videoWidth: _parseInt(results[1]),
      videoHeight: _parseInt(results[2]),
      videoFps: _parseDouble(results[3]),
      actualFps: _parseDouble(results[4]),
      videoBitrate: _parseInt(results[5]),
      hwdecCurrent: results[6],
      audioCodec: _formatCodecName(results[7]),
      audioSamplerate: _parseInt(results[8]),
      audioChannels: results[9],
      audioBitrate: _parseInt(results[10]),
      avsyncChange: _parseDouble(results[11]),
      cacheSpeed: _parseDouble(results[12]),
      frameDropCount: _parseInt(results[13]),
      decoderFrameDropCount: _parseInt(results[14]),
      cacheDuration: _parseDouble(results[15]),
      // Video-dependent properties
      displayFps: _parseDouble(videoResults?.first),
      pixelformat: videoResults?[1],
      hwPixelformat: videoResults?[2],
      colormatrix: videoResults?[3],
      primaries: videoResults?[4],
      gamma: videoResults?[5],
      maxLuma: _parseDouble(videoResults?[6]),
      minLuma: _parseDouble(videoResults?[7]),
      maxCll: _parseDouble(videoResults?[8]),
      maxFall: _parseDouble(videoResults?[9]),
      aspectName: videoResults?[10],
      rotate: _parseInt(videoResults?[11]),
      appMemoryBytes: appMemory,
      uiFps: _currentUiFps,
    );

    _statsController.add(stats);
  }

  /// Parse a string to int, returning null if parsing fails.
  int? _parseInt(String? value) {
    if (value == null || value.isEmpty) return null;
    return int.tryParse(value);
  }

  /// Parse a string to double, returning null if parsing fails.
  double? _parseDouble(String? value) {
    if (value == null || value.isEmpty) return null;
    return double.tryParse(value);
  }

  /// Format codec name for display (uppercase common codecs).
  String? _formatCodecName(String? codec) {
    if (codec == null || codec.isEmpty) return null;
    // Common codec name mappings
    final upper = codec.toUpperCase();
    if (upper.contains('HEVC') || upper.contains('H265')) return 'HEVC';
    if (upper.contains('H264') || upper.contains('AVC')) return 'H.264';
    if (upper.contains('AV1')) return 'AV1';
    if (upper.contains('VP9')) return 'VP9';
    if (upper.contains('AAC')) return 'AAC';
    if (upper.contains('AC3') || upper.contains('AC-3')) return 'AC3';
    if (upper.contains('EAC3') || upper.contains('E-AC-3')) return 'EAC3';
    if (upper.contains('DTS')) return 'DTS';
    if (upper.contains('TRUEHD')) return 'TrueHD';
    if (upper.contains('FLAC')) return 'FLAC';
    if (upper.contains('OPUS')) return 'Opus';
    if (upper.contains('VORBIS')) return 'Vorbis';
    if (upper.contains('MP3')) return 'MP3';
    return codec;
  }

  /// Dispose of the service and release resources.
  void dispose() {
    _backendSwitchedSubscription?.cancel();
    _backendSwitchedSubscription = null;
    stopPolling();
    _statsController.close();
  }
}
