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

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

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

const _tag = 'Media3Player';


@visibleForTesting
bool isMedia3NetworkError(String code) => code.startsWith('ERROR_CODE_IO_');


class Media3PlayerEngine implements PlayerEngine {
  Media3PlayerEngine({bool? isAndroid}) {
    final onAndroid = isAndroid ?? Platform.isAndroid;
    if (!onAndroid) {
      throw UnsupportedError('Media3PlayerEngine is Android-only');
    }
    AppLogger.debug(_tag, 'construct');
  }

  static const MethodChannel _mc = MethodChannel('smplayer/media3_engine');
  static const EventChannel _events = EventChannel(
    'smplayer/media3_engine/events',
  );

  @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
  final ValueNotifier<ui.Size?> videoSize = ValueNotifier<ui.Size?>(null);

  StreamSubscription<dynamic>? _eventSub;


  final ValueNotifier<int?> _playerIdNotifier = ValueNotifier<int?>(null);
  int? get _playerId => _playerIdNotifier.value;
  set _playerId(int? v) => _playerIdNotifier.value = v;


  int _generation = 0;

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

  @override
  String get displayName => 'media3';

  @override
  bool get usesPlatformSurface => true;

  @override
  Future<int?> totalRxBytes() async {
    if (_disposed || _playerId == null) return null;
    final value = await _mc.invokeMethod<dynamic>(
      'getProperty',
      <String, dynamic>{'playerId': _playerId, 'name': 'totalRxBytes'},
    );
    return (value as num?)?.toInt();
  }

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

  @override
  void configureHardwareDecoding(String mode) {}

  @override
  Future<void> open(
    String url, {
    Duration start = Duration.zero,
    bool play = true,
    Map<String, String>? headers,
    CancellationToken? cancel,
  }) async {
    _ensureNotDisposed();
    cancel?.throwIfCancelled();

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

    final generation = ++_generation;

    final openFuture = _mc.invokeMethod<void>('open', <String, dynamic>{
      'playerId': _playerId,
      'url': url,
      'headers': headers ?? const <String, String>{},
      'startMs': start.inMilliseconds,
      'play': play,
      'generation': generation,
    });

    await _runCancellable(openFuture, cancel, generation);
    if (_disposed) return;
    cancel?.throwIfCancelled();

    _completedEmitted = false;
    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();
    await _invoke('replayKeyState');
  }


  Future<void> _runCancellable(
    Future<void> future,
    CancellationToken? cancel,
    int generation,
  ) {
    if (cancel == null) return future;
    return Future.any<void>([
      future,
      cancel.whenCancelled.then<void>((_) async {
        if (!_disposed && _playerId != null) {
          ++_generation;
          try {
            await _mc.invokeMethod<void>('stop', <String, dynamic>{
              'playerId': _playerId,
              'generation': _generation,
            });
          } catch (e) {
            AppLogger.debug(_tag, 'stop on cancel failed', e);
          }
        }
        throw CancelledException(cancel.reason);
      }),
    ]);
  }

  @override
  Future<void> play() async {
    if (_disposed || _playerId == null) return;
    await _invoke('play');
  }

  @override
  Future<void> pause() async {
    if (_disposed || _playerId == null) return;
    await _invoke('pause');
  }

  @override
  Future<void> stop() async {
    if (_disposed || _playerId == null) return;
    ++_generation;
    await _invoke('stop', <String, dynamic>{'generation': _generation});
  }

  @override
  Future<void> seek(Duration position) async {
    if (_disposed || _playerId == null) 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 _invoke('seek', <String, dynamic>{
      'positionMs': target.inMilliseconds,
    });
  }

  @override
  Future<void> setRate(double rate) async {
    if (_disposed || _playerId == null) return;
    state.rate = rate;
    await _invoke('setRate', <String, dynamic>{'rate': rate});
  }

  @override
  Future<void> setVolume(double volume) async {
    if (_disposed) return;
    final next = volume.clamp(0.0, 200.0).toDouble();
    state.volume = next;
    if (next > 0) _lastNonZeroVolume = next;
    stream.addVolume(next);
    if (_playerId == null) return;
    await _invoke('setVolume', <String, dynamic>{'volume': 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 || _playerId == null) return;
    await _invoke('setAudioTrack', <String, dynamic>{'id': track.id});
    state.track = state.track.copyWith(audio: track);
    stream.addTrack(state.track);
  }

  @override
  Future<void> setSubtitleTrack(SubtitleTrack track) async {
    if (_disposed || _playerId == null) return;
    await _invoke('setSubtitleTrack', <String, dynamic>{'id': track.id});
    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,
  }) {
    if (_disposed || _playerId == null) return;
    _invoke('setSubtitleStyle', <String, dynamic>{
      'fontSize': fontSize,
      'bgOpacity': bgOpacity,
      'position': position,
    });
  }


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

  @override
  Widget buildVideoView(BuildContext context, ValueListenable<BoxFit> fit) {
    return _Media3PlatformVideoView(
      playerId: _playerIdNotifier,
      fit: fit,
      onResizeMode: _setResizeMode,
    );
  }

  Future<void> _setResizeMode(String mode) async {
    if (_disposed || _playerId == null) return;
    try {
      await _mc.invokeMethod<void>('setResizeMode', <String, dynamic>{
        'playerId': _playerId,
        'mode': mode,
      });
    } catch (e) {
      AppLogger.debug(_tag, 'setResizeMode($mode) failed', e);
    }
  }

  @override
  Future<void> dispose() async {
    if (_disposed) return;
    _disposed = true;
    ++_generation;
    await _eventSub?.cancel();
    _eventSub = null;
    final id = _playerId;
    _playerId = null;
    if (id != null) {
      try {
        await _mc.invokeMethod<void>('dispose', <String, dynamic>{
          'playerId': id,
        });
      } catch (e) {
        AppLogger.warn(_tag, 'native dispose failed', e);
      }
    }
    textureId.dispose();
    textureVersion.dispose();
    videoSize.dispose();
    _playerIdNotifier.dispose();
    stream.dispose();
  }


  Future<void> prewarmNative() async {
    if (_disposed) return;
    await _ensureCreated();
  }


  Future<void>? _createFuture;


  Future<void> _ensureCreated() {
    if (_playerId != null) return Future<void>.value();
    return _createFuture ??= _doCreateNative();
  }

  Future<void> _doCreateNative() async {
    try {
      final result = await _mc.invokeMapMethod<String, dynamic>('create');
      if (result == null) {
        throw StateError('Media3 create returned null');
      }
      final createdPlayerId = (result['playerId'] as num).toInt();

      if (_disposed) {
        AppLogger.debug(
          _tag,
          'disposed during create, releasing orphan playerId=$createdPlayerId',
        );
        try {
          await _mc.invokeMethod<void>('dispose', <String, dynamic>{
            'playerId': createdPlayerId,
          });
        } catch (e) {
          AppLogger.warn(_tag, 'orphan dispose failed', e);
        }
        return;
      }

      _playerId = createdPlayerId;
      AppLogger.debug(
        _tag,
        'created playerId=$_playerId '
        'ffmpeg=${result['ffmpegAvailable']} v=${result['ffmpegVersion']}',
      );
      _eventSub = _events.receiveBroadcastStream().listen(
        _handleEvent,
        onError: (Object e) {
          if (_disposed) return;
          AppLogger.warn(_tag, 'event stream error', e);
        },
      );
      try {
        await _mc.invokeMethod<void>('setVolume', <String, dynamic>{
          'playerId': _playerId,
          'volume': state.volume,
        });
      } catch (e) {
        AppLogger.debug(_tag, 'initial setVolume replay failed', e);
      }
    } catch (e) {
      _createFuture = null;
      rethrow;
    }
  }


  void _handleEvent(dynamic event) {
    if (_disposed) return;
    if (event is! Map) return;
    if (event['playerId'] != _playerId) return;
    final gen = (event['generation'] as num?)?.toInt();
    if (gen == null || gen != _generation) return;

    final type = event['type'] as String?;
    switch (type) {
      case 'position':
        final ms = (event['positionMs'] as num?)?.toInt() ?? 0;
        final pos = Duration(milliseconds: ms);
        state.position = pos;
        stream.addPosition(pos);
        final bufMs = (event['bufferMs'] as num?)?.toInt();
        if (bufMs != null) {
          final buf = Duration(milliseconds: bufMs);
          state.buffer = buf;
          stream.addBuffer(buf);
        }
      case 'duration':
        final ms = (event['durationMs'] as num?)?.toInt() ?? 0;
        final dur = Duration(milliseconds: ms);
        if (dur == state.duration) return;
        state.duration = dur;
        stream.addDuration(dur);
      case 'playing':
        final playing = event['playing'] == true;
        if (state.playing == playing) return;
        state.playing = playing;
        stream.addPlaying(playing);
      case 'completed':
        if (_completedEmitted) return;
        _completedEmitted = true;
        state.completed = true;
        stream.addCompleted(true);
      case 'videoSize':
        final w = (event['width'] as num?)?.toDouble() ?? 0;
        final h = (event['height'] as num?)?.toDouble() ?? 0;
        if (w > 0 && h > 0) {
          videoSize.value = ui.Size(w, h);
        }
      case 'tracks':
        final tracks = _decodeTracks(event);
        if (tracks == state.tracks) return;
        state.tracks = tracks;
        stream.addTracks(tracks);
      case 'subtitle':
        final lines =
            (event['lines'] as List?)?.cast<String>() ?? const <String>[];
        state.subtitle = lines;
        stream.addSubtitle(lines);
      case 'volume':
        final vol = (event['volume'] as num?)?.toDouble();
        if (vol != null) {
          state.volume = vol;
          if (vol > 0) _lastNonZeroVolume = vol;
          stream.addVolume(vol);
        }
      case 'error':
        final code = event['code'] as String? ?? 'unknown';
        state.error = code;
        AppLogger.warn(_tag, 'media3 error: $code ${event['message'] ?? ''}');
        stream.addError(code);
      default:
        AppLogger.debug(_tag, 'unknown event type: $type');
    }
  }


  PlayerTracks _decodeTracks(Map<dynamic, dynamic> event) {
    final audio = <AudioTrack>[];
    for (final raw in (event['audio'] as List? ?? const [])) {
      final m = raw as Map;
      audio.add(
        AudioTrack(
          id: m['id'] as String,
          index: (m['index'] as num?)?.toInt(),
          title: (m['title'] as String?)?.isNotEmpty == true
              ? m['title'] as String
              : null,
          language: (m['language'] as String?)?.isNotEmpty == true
              ? m['language'] as String
              : null,
        ),
      );
    }
    final subtitle = <SubtitleTrack>[];
    for (final raw in (event['subtitle'] as List? ?? const [])) {
      final m = raw as Map;
      subtitle.add(
        SubtitleTrack(
          id: m['id'] as String,
          index: (m['index'] as num?)?.toInt(),
          title: (m['title'] as String?)?.isNotEmpty == true
              ? m['title'] as String
              : null,
          language: (m['language'] as String?)?.isNotEmpty == true
              ? m['language'] as String
              : null,
        ),
      );
    }
    return PlayerTracks(audio: audio, subtitle: subtitle);
  }

  Future<void> _invoke(String method, [Map<String, dynamic>? args]) {
    return _mc.invokeMethod<void>(method, <String, dynamic>{
      'playerId': _playerId,
      ...?args,
    });
  }

  void _ensureNotDisposed() {
    if (_disposed) throw StateError('Media3PlayerEngine 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);
  }
}


class _Media3PlatformVideoView extends StatefulWidget {
  const _Media3PlatformVideoView({
    required this.playerId,
    required this.fit,
    required this.onResizeMode,
  });

  static const String _viewType = 'smplayer/media3_surface';

  final ValueListenable<int?> playerId;
  final ValueListenable<BoxFit> fit;
  final Future<void> Function(String mode) onResizeMode;

  @override
  State<_Media3PlatformVideoView> createState() =>
      _Media3PlatformVideoViewState();
}

class _Media3PlatformVideoViewState extends State<_Media3PlatformVideoView> {
  int? _lastSentPlayerId;
  String? _lastSentMode;

  @override
  void initState() {
    super.initState();
    widget.playerId.addListener(_syncResizeMode);
    widget.fit.addListener(_syncResizeMode);
    _syncResizeMode();
  }

  @override
  void didUpdateWidget(covariant _Media3PlatformVideoView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.playerId != widget.playerId) {
      oldWidget.playerId.removeListener(_syncResizeMode);
      widget.playerId.addListener(_syncResizeMode);
    }
    if (oldWidget.fit != widget.fit) {
      oldWidget.fit.removeListener(_syncResizeMode);
      widget.fit.addListener(_syncResizeMode);
    }
    _syncResizeMode();
  }

  @override
  void dispose() {
    widget.playerId.removeListener(_syncResizeMode);
    widget.fit.removeListener(_syncResizeMode);
    super.dispose();
  }

  void _syncResizeMode() {
    final id = widget.playerId.value;
    if (id == null) return;
    final mode = _resizeModeForFit(widget.fit.value);
    if (_lastSentPlayerId == id && _lastSentMode == mode) return;
    _lastSentPlayerId = id;
    _lastSentMode = mode;
    unawaited(widget.onResizeMode(mode));
  }

  String _resizeModeForFit(BoxFit fit) {
    return switch (fit) {
      BoxFit.fill => 'fill',
      BoxFit.cover => 'zoom',
      _ => 'fit',
    };
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<int?>(
      valueListenable: widget.playerId,
      builder: (context, id, _) {
        if (id == null) {
          return const SizedBox.expand();
        }
        return PlatformViewLink(
          key: ValueKey<int>(id),
          viewType: _Media3PlatformVideoView._viewType,
          surfaceFactory: (context, controller) {
            return AndroidViewSurface(
              controller: controller as AndroidViewController,
              gestureRecognizers:
                  const <Factory<OneSequenceGestureRecognizer>>{},
              hitTestBehavior: PlatformViewHitTestBehavior.transparent,
            );
          },
          onCreatePlatformView: (params) {
            final controller =
                PlatformViewsService.initExpensiveAndroidView(
                    id: params.id,
                    viewType: _Media3PlatformVideoView._viewType,
                    layoutDirection: TextDirection.ltr,
                    creationParams: <String, dynamic>{'playerId': id},
                    creationParamsCodec: const StandardMessageCodec(),
                    onFocus: () => params.onFocusChanged(true),
                  )
                  ..addOnPlatformViewCreatedListener(
                    params.onPlatformViewCreated,
                  )
                  ..create();
            return controller;
          },
        );
      },
    );
  }
}
