import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:media_kit/media_kit.dart' as mk;
import 'package:media_kit_video/media_kit_video.dart' as mkv;

import '../../contracts/cancellation.dart';
import '../../../shared/utils/app_logger.dart';
import 'player_engine.dart';

const _tag = 'MKPlayer';


@visibleForTesting
bool isMpvNetworkError(String error) {
  final lower = error.toLowerCase();
  if (lower.startsWith('tcp:') || lower.startsWith('tls:')) return true;
  const patterns = [
    'failed to open http',
    'loading failed',
    'connection refused',
    'connection reset',
    'connection timed out',
    'network is unreachable',
    'host not found',
    'name resolution',
    'http error 4',
    'http error 5',
    'tls handshake',
  ];
  for (final p in patterns) {
    if (lower.contains(p)) return true;
  }
  return false;
}

const int _mpvNetworkTimeoutSeconds = 60;

double clampMediaKitVolume(double volume) =>
    volume.clamp(0.0, 200.0).toDouble();


PlayerTracks mapMkTracksToPlayerTracks(mk.Tracks mkTracks) {
  final audio = <AudioTrack>[];
  for (var i = 0; i < mkTracks.audio.length; i++) {
    final t = mkTracks.audio[i];
    if (t.id == 'auto' || t.id == 'no') continue;
    audio.add(mapMkAudioTrack(t, index: i));
  }

  final subtitle = <SubtitleTrack>[];
  for (var i = 0; i < mkTracks.subtitle.length; i++) {
    final t = mkTracks.subtitle[i];
    if (t.id == 'auto' || t.id == 'no') continue;
    subtitle.add(mapMkSubtitleTrack(t, index: i));
  }

  return PlayerTracks(audio: audio, subtitle: subtitle);
}


AudioTrack mapMkAudioTrack(mk.AudioTrack t, {int? index}) {
  if (t.id == 'auto') return const AudioTrack.auto();
  if (t.id == 'no') return const AudioTrack.no();
  return AudioTrack(
    id: t.id,
    index: index,
    title: t.title?.isNotEmpty == true ? t.title : null,
    language: t.language?.isNotEmpty == true ? t.language : null,
    native: t,
  );
}


SubtitleTrack mapMkSubtitleTrack(mk.SubtitleTrack t, {int? index}) {
  if (t.id == 'auto') return const SubtitleTrack.no();
  if (t.id == 'no') return const SubtitleTrack.no();
  return SubtitleTrack(
    id: t.id,
    index: index,
    title: t.title?.isNotEmpty == true ? t.title : null,
    language: t.language?.isNotEmpty == true ? t.language : null,
    native: t,
  );
}


class MediaKitPlayerEngine implements PlayerEngine {
  MediaKitPlayerEngine() {
    AppLogger.debug(_tag, 'construct start');
    _subscribeToStreams();
    _configureNetworkTimeout();
    _configureNetworkReconnect();
    AppLogger.debug(_tag, 'construct done');
  }

  final mk.Player _player = mk.Player(
    configuration: const mk.PlayerConfiguration(
      title: 'smPlayer mpv',
      libass: true,


      logLevel: mk.MPVLogLevel.warn,
    ),
  );
  mkv.VideoController? _controller;

  @override
  final PlayerStreams stream = PlayerStreams();
  @override
  final PlayerState state = PlayerState();

  @override
  final ValueNotifier<int?> textureId = ValueNotifier<int?>(null);

  @override
  final ValueNotifier<int> textureVersion = ValueNotifier<int>(0);

  @override
  bool get usesPlatformSurface => false;

  @override
  bool isNetworkStreamError(String error) => isMpvNetworkError(error);

  @override
  Future<int?> totalRxBytes() async {
    final native = _player.platform;
    if (_disposed || native is! mk.NativePlayer) return null;
    try {
      final raw = await native.getProperty('cache-speed');
      final speed = double.tryParse(raw);
      if (speed == null || speed < 0) return _rxBytesAccum;
      final dtMs = _rxStopwatch.elapsedMilliseconds;
      _rxStopwatch.reset();
      _rxStopwatch.start();

      final clampedDtMs = dtMs.clamp(0, 5000);
      _rxBytesAccum += (speed * clampedDtMs) ~/ 1000;
      return _rxBytesAccum;
    } catch (_) {
      return null;
    }
  }

  @override
  final ValueNotifier<ui.Size?> videoSize = ValueNotifier<ui.Size?>(null);

  bool _disposed = false;
  bool _completedEmitted = false;
  bool _playerReady = false;
  double _lastNonZeroVolume = 100.0;

  final List<StreamSubscription<dynamic>> _subs = [];
  double? _pendingInitialVolume;


  int _rxBytesAccum = 0;
  final Stopwatch _rxStopwatch = Stopwatch();

  void _subscribeToStreams() {
    _subs.addAll([
      _player.stream.playing.listen(_handlePlaying),
      _player.stream.completed.listen(_handleCompleted),
      _player.stream.position.listen(_handlePosition),
      _player.stream.duration.listen(_handleDuration),
      _player.stream.buffer.listen(_handleBuffer),
      _player.stream.volume.listen(_handleVolume),
      _player.stream.tracks.listen(_handleTracks),
      _player.stream.track.listen(_handleTrack),
      _player.stream.width.listen(_handleWidth),
      _player.stream.height.listen(_handleHeight),
      _player.stream.error.listen(_handleError),
      _player.stream.log.listen(_handleMpvLog),
    ]);
  }


  int? _lastHttpErrorStatus;

  static final RegExp _httpErrorStatusPattern = RegExp(r'HTTP error (\d{3})');


  void _handleMpvLog(mk.PlayerLog log) {
    final text = log.text;
    if (text.contains('HTTP error')) {
      final match = _httpErrorStatusPattern.firstMatch(text);
      if (match != null) {
        _lastHttpErrorStatus = int.tryParse(match.group(1)!);
      }
    }
    if (!kDebugMode) return;
    if (text.contains('request:') ||
        text.contains('User-Agent') ||
        text.contains('HTTP error') ||
        text.contains('Location:')) {
      AppLogger.debug(_tag, 'mpv[${log.prefix}] ${text.trim()}');
    }
  }

  void _configureVolumeBoostLimit() {
    _setMpvPropertyWhenReady('volume-max', '200');
  }

  void _configureNetworkTimeout() {
    _setMpvPropertyWhenReady('network-timeout', '$_mpvNetworkTimeoutSeconds');
  }

  void _configureNetworkReconnect() {
    const reconnectOptions =
        'reconnect=1,'
        'reconnect_streamed=1,'
        'reconnect_on_network_error=1,'
        'reconnect_on_http_error=4xx+5xx,'
        'reconnect_delay_max=5';
    _setMpvPropertyWhenReady('stream-lavf-o', reconnectOptions);
  }


  void _setMpvPropertyWhenReady(String propertyName, String propertyValue) {
    final native = _player.platform;
    if (native is! mk.NativePlayer) return;
    AppLogger.debug(_tag, 'set $propertyName=$propertyValue queued');
    unawaited(
      native
          .setProperty(propertyName, propertyValue)
          .then(
            (_) => AppLogger.debug(_tag, 'set $propertyName done'),
            onError: (Object e) =>
                AppLogger.debug(_tag, 'set $propertyName failed', e),
          ),
    );
  }

  void _handlePlaying(bool playing) {
    if (_disposed) return;
    if (state.playing == playing) return;
    state.playing = playing;
    stream.addPlaying(playing);
  }

  void _handleCompleted(bool completed) {
    if (_disposed) return;
    if (!completed) return;
    if (_completedEmitted) return;
    _completedEmitted = true;
    state.completed = true;
    stream.addCompleted(true);
  }

  void _handlePosition(Duration pos) {
    if (_disposed) return;
    state.position = pos;
    stream.addPosition(pos);
  }

  void _handleDuration(Duration dur) {
    if (_disposed) return;
    if (dur == state.duration) return;
    state.duration = dur;
    stream.addDuration(dur);
  }

  void _handleBuffer(Duration buf) {
    if (_disposed) return;
    state.buffer = buf;
    stream.addBuffer(buf);
  }

  void _handleVolume(double vol) {
    if (_disposed) return;
    state.volume = vol;
    if (vol > 0) _lastNonZeroVolume = vol;
    stream.addVolume(vol);
  }

  void _handleTracks(mk.Tracks mkTracks) {
    if (_disposed) return;
    final tracks = mapMkTracksToPlayerTracks(mkTracks);
    if (tracks == state.tracks) return;
    state.tracks = tracks;
    stream.addTracks(tracks);
  }

  void _handleTrack(mk.Track mkTrack) {
    if (_disposed) return;
    final next = PlayerTrack(
      audio: mapMkAudioTrack(mkTrack.audio),
      subtitle: mapMkSubtitleTrack(mkTrack.subtitle),
    );
    if (next == state.track) return;
    state.track = next;
    stream.addTrack(next);
  }

  void _handleWidth(int? w) {
    if (_disposed) return;
    _updateVideoSize(w, null);
  }

  void _handleHeight(int? h) {
    if (_disposed) return;
    _updateVideoSize(null, h);
  }

  void _handleError(String error) {
    if (_disposed) return;


    final status = _lastHttpErrorStatus;
    final enriched = (status != null && !error.contains('(http '))
        ? '$error (http $status)'
        : error;
    state.error = enriched;
    AppLogger.warn(_tag, 'mpv error: $enriched');
    stream.addError(enriched);
  }

  void _updateVideoSize(int? newW, int? newH) {
    final current = videoSize.value;
    final w = newW?.toDouble() ?? current?.width ?? 0;
    final h = newH?.toDouble() ?? current?.height ?? 0;
    if (w > 0 && h > 0) {
      videoSize.value = ui.Size(w, h);
    }
  }

  @override
  String get displayName => 'mpv';

  @override
  void configureHardwareDecoding(String mode) {
    final native = _player.platform;
    if (native is! mk.NativePlayer) return;
    final mpvHwdec = mode == 'no' ? 'no' : mode;
    native.setProperty('hwdec', mpvHwdec).catchError((e) {
      AppLogger.debug(_tag, 'setProperty(hwdec, $mpvHwdec) failed', e);
    });
    AppLogger.debug(_tag, 'hwdec=$mpvHwdec');
  }

  @override
  Future<void> open(
    String url, {
    Duration start = Duration.zero,
    bool play = true,
    Map<String, String>? headers,
    CancellationToken? cancel,
  }) async {
    _ensureNotDisposed();
    cancel?.throwIfCancelled();
    AppLogger.debug(
      _tag,
      'open start start=${start.inMilliseconds}ms play=$play '
      'headers=${headers?.keys.join(',') ?? '<none>'}',
    );

    _rxBytesAccum = 0;
    _rxStopwatch.reset();
    _rxStopwatch.start();
    _completedEmitted = false;
    _lastHttpErrorStatus = null;
    videoSize.value = null;
    state
      ..position = start
      ..buffer = start
      ..duration = Duration.zero
      ..playing = false
      ..completed = false
      ..tracks = const PlayerTracks()
      ..track = const PlayerTrack()
      ..subtitle = const []
      ..error = null;
    _emitStateSnapshot();

    final media = mk.Media(
      url,
      httpHeaders: headers,
      start: start == Duration.zero ? null : start,
    );

    AppLogger.debug(_tag, 'player.open start');
    await _runCancellable(_player.open(media, play: false), cancel);
    AppLogger.debug(_tag, 'player.open done');

    if (_disposed) return;
    cancel?.throwIfCancelled();

    _playerReady = true;
    _configureVolumeBoostLimit();
    final pendingVolume = _pendingInitialVolume;
    if (pendingVolume != null) {
      _pendingInitialVolume = null;
      await setVolume(pendingVolume);
      if (_disposed) return;
      cancel?.throwIfCancelled();
    }

    if (start > Duration.zero) {
      try {
        await _runCancellable(_player.seek(start), cancel);
      } on CancelledException {
        rethrow;
      } catch (e) {
        AppLogger.debug(_tag, 'seek-on-open failed, continuing', e);
      }
      if (_disposed) return;
      cancel?.throwIfCancelled();
    }

    if (play) {
      AppLogger.debug(_tag, 'player.play start');
      await _player.play();
      AppLogger.debug(_tag, 'player.play done');
    }
  }


  Future<T> _runCancellable<T>(Future<T> future, CancellationToken? cancel) {
    if (cancel == null) return future;
    return Future.any<T>([
      future,
      cancel.whenCancelled.then<T>((_) async {
        try {
          if (!_disposed) await _player.stop();
        } catch (e) {
          AppLogger.debug(_tag, 'stop on cancel failed', e);
        }
        throw CancelledException(cancel.reason);
      }),
    ]);
  }

  @override
  Future<void> play() async {
    if (_disposed) return;
    await _player.play();
  }

  @override
  Future<void> pause() async {
    if (_disposed) return;
    await _player.pause();
  }

  @override
  Future<void> stop() async {
    if (_disposed) return;
    await _player.stop();
  }

  @override
  Future<void> seek(Duration position) async {
    if (_disposed) return;
    final durationMs = state.duration.inMilliseconds;
    final maxMs = durationMs <= 0 ? (1 << 62) : durationMs;
    final target = Duration(
      milliseconds: position.inMilliseconds.clamp(0, maxMs).toInt(),
    );
    state.position = target;
    stream.addPosition(target);
    await _player.seek(target);
  }

  @override
  Future<void> setRate(double rate) async {
    if (_disposed) return;
    state.rate = rate;
    await _player.setRate(rate);
  }

  @override
  Future<void> setVolume(double volume) async {
    if (_disposed) return;
    final next = clampMediaKitVolume(volume);
    state.volume = next;
    if (next > 0) _lastNonZeroVolume = next;
    if (!_playerReady) {
      _pendingInitialVolume = next;
    } else {
      await _player.setVolume(next);
    }
    stream.addVolume(next);
  }

  @override
  Future<void> toggleMute() async {
    if (state.volume <= 0) {
      await setVolume(_lastNonZeroVolume <= 0 ? 100 : _lastNonZeroVolume);
    } else {
      await setVolume(0);
    }
  }

  @override
  Future<void> setAudioTrack(AudioTrack track) async {
    if (_disposed) return;
    if (track.no) {
      await _player.setAudioTrack(mk.AudioTrack.no());
    } else if (track.native is mk.AudioTrack) {
      await _player.setAudioTrack(track.native as mk.AudioTrack);
    } else {
      AppLogger.warn(
        _tag,
        'setAudioTrack: no native mk.AudioTrack on ${track.id}',
      );
    }
    state.track = state.track.copyWith(audio: track);
    stream.addTrack(state.track);
  }

  @override
  Future<void> setSubtitleTrack(SubtitleTrack track) async {
    if (_disposed) return;
    if (track.no) {
      await _player.setSubtitleTrack(mk.SubtitleTrack.no());
    } else if (track.native is mk.SubtitleTrack) {
      await _player.setSubtitleTrack(track.native as mk.SubtitleTrack);
    } else {
      AppLogger.warn(
        _tag,
        'setSubtitleTrack: no native mk.SubtitleTrack on ${track.id} '
        '(track globalIdx=${track.index} title=${track.title})',
      );
    }
    state.track = state.track.copyWith(subtitle: track);
    stream.addTrack(state.track);
  }

  @override
  void setNativeSubtitleRendering(bool enabled) {


  }

  @override
  void setSubtitleStyle({
    required double fontSize,
    required double bgOpacity,
    required int position,
  }) {}


  @override
  ValueListenable<PlaybackStats?> get debugStats => kEmptyPlaybackStats;


  @override
  Widget buildVideoView(BuildContext context, ValueListenable<BoxFit> fit) {
    final controller = _ensureVideoController();
    return ValueListenableBuilder<BoxFit>(
      valueListenable: fit,
      builder: (context, fitValue, _) => mkv.Video(
        controller: controller,
        fit: fitValue,
        controls: null,
        wakelock: false,
        pauseUponEnteringBackgroundMode: false,
      ),
    );
  }

  @override
  Future<void> dispose() async {
    if (_disposed) return;
    _disposed = true;
    for (final sub in _subs) {
      await sub.cancel();
    }
    _subs.clear();
    await _player.dispose();
    textureId.dispose();
    textureVersion.dispose();
    videoSize.dispose();
    stream.dispose();
  }

  mkv.VideoController _ensureVideoController() {
    final existing = _controller;
    if (existing != null) return existing;

    AppLogger.debug(
      _tag,
      'video controller create start os=${Platform.operatingSystem}',
    );

    final controller = mkv.VideoController(
      _player,
      configuration: const mkv.VideoControllerConfiguration(
        enableHardwareAcceleration: true,
      ),
    );
    AppLogger.debug(_tag, 'video controller create done');
    _controller = controller;
    return controller;
  }

  void _ensureNotDisposed() {
    if (_disposed) throw StateError('MediaKitPlayerEngine has been disposed');
  }

  void _emitStateSnapshot() {
    stream.addPosition(state.position);
    stream.addDuration(state.duration);
    stream.addBuffer(state.buffer);
    stream.addPlaying(state.playing);
    stream.addCompleted(state.completed);
    stream.addTracks(state.tracks);
    stream.addTrack(state.track);
    stream.addSubtitle(state.subtitle);
    stream.addVolume(state.volume);
  }
}
