import 'dart:async';

import 'package:flutter/foundation.dart';

import '../../../core/contracts/library.dart';
import '../../../shared/utils/app_logger.dart';
import '../../../shared/utils/format_utils.dart';
import 'playlist.dart';

typedef LoadEpisodesFn =
    Future<List<EmbyRawItem>> Function({
      required String seriesId,
      required String seasonId,
    });

typedef LoadSeasonsFn = Future<List<EmbyRawSeason>> Function(String seriesId);


class SeriesEpisodePlaylist implements Playlist {
  SeriesEpisodePlaylist({
    required this.seriesId,
    required this.loadEpisodes,
    required this.loadSeasons,
  }) : _current = ValueNotifier<PlaylistItem>(_placeholder),
       _currentIndex = ValueNotifier<int>(-1);

  final String seriesId;
  final LoadEpisodesFn loadEpisodes;
  final LoadSeasonsFn loadSeasons;

  final ValueNotifier<PlaylistItem> _current;
  final ValueNotifier<int> _currentIndex;

  String? _currentSeasonId;
  List<EmbyRawItem> _episodes = const [];


  String? _initialMediaSourceId;
  int? _initialStartPositionTicks;
  int? _initialPreferredAudio;
  int? _initialPreferredSub;
  bool _initialPlayFromStart = false;
  bool _initialApplied = false;

  bool _disposed = false;
  bool _userNavigated = false;

  static const PlaylistItem _placeholder = PlaylistItem(itemId: '');


  Future<void> initialize({
    required String seasonId,
    required String episodeId,
    int? initialIndexNumber,
    String? initialMediaSourceId,
    int? initialStartPositionTicks,
    int? initialPreferredAudioStreamIndex,
    int? initialPreferredSubtitleStreamIndex,
    bool initialPlayFromStart = false,
    EmbyRawItem? preloadedItem,
  }) async {
    _initialMediaSourceId = initialMediaSourceId;
    _initialStartPositionTicks = initialStartPositionTicks;
    _initialPreferredAudio = initialPreferredAudioStreamIndex;
    _initialPreferredSub = initialPreferredSubtitleStreamIndex;
    _initialPlayFromStart = initialPlayFromStart;
    _initialApplied = true;
    _currentSeasonId = seasonId;
    _current.value = PlaylistItem(
      itemId: episodeId,
      mediaSourceId: initialMediaSourceId,
      startPositionTicks: initialStartPositionTicks,
      preferredAudioStreamIndex: initialPreferredAudioStreamIndex,
      preferredSubtitleStreamIndex: initialPreferredSubtitleStreamIndex,
      playFromStart: initialPlayFromStart,
      displayLabel: preloadedItem != null ? _labelOf(preloadedItem) : '',
      preloadedItem: preloadedItem,
    );
    unawaited(_resolveEpisodeList(seasonId, episodeId, initialIndexNumber));
  }


  Future<void> _resolveEpisodeList(
    String seasonId,
    String episodeId,
    int? initialIndexNumber,
  ) async {
    try {
      await _loadSeasonEpisodes(seasonId);
    } catch (e) {
      AppLogger.warn(
        'SeriesEpisodePlaylist',
        'background episode list load failed',
        e,
      );
      return;
    }
    if (_disposed || _userNavigated) return;

    var idx = _episodes.indexWhere((e) => e.Id == episodeId);
    if (idx < 0 && initialIndexNumber != null) {
      idx = _episodes.indexWhere((e) => e.IndexNumber == initialIndexNumber);
    }
    AppLogger.debug(
      'SeriesEpisodePlaylist',
      'episodeListReady seriesId=$seriesId seasonId=$seasonId '
          'episodeId=$episodeId idx=$idx '
          'episodes=${_episodes.length} '
          'episodeIds=${_episodes.map((e) => e.Id).toList()}',
    );
    if (_episodes.isEmpty) return;
    _currentIndex.value = idx < 0 ? 0 : idx;
  }

  @override
  ValueListenable<PlaylistItem> get current => _current;
  @override
  ValueListenable<int> get currentIndex => _currentIndex;
  @override
  int get length => _episodes.length;


  @override
  bool get hasNext => _episodes.isNotEmpty;
  @override
  bool get hasPrev => _episodes.isNotEmpty;

  @override
  Future<void> next() async {
    if (_disposed || _episodes.isEmpty) return;
    final cur = _currentIndex.value;
    if (cur < _episodes.length - 1) {
      _markMoved();
      _setCurrent(cur + 1);
      return;
    }
    await _crossSeason(direction: 1);
  }

  @override
  Future<void> prev() async {
    if (_disposed || _episodes.isEmpty) return;
    final cur = _currentIndex.value;
    if (cur > 0) {
      _markMoved();
      _setCurrent(cur - 1);
      return;
    }
    await _crossSeason(direction: -1);
  }

  @override
  Future<void> goTo(int index) async {
    if (_disposed || index < 0 || index >= _episodes.length) return;
    if (index == _currentIndex.value) return;
    _markMoved();
    _setCurrent(index);
  }

  @override
  Future<void> goToItemId(String itemId) async {
    if (_disposed) return;
    final idx = _episodes.indexWhere((e) => e.Id == itemId);
    if (idx < 0) return;
    if (idx == _currentIndex.value) return;
    _markMoved();
    _setCurrent(idx);
  }


  Future<void> goToInSeason({
    required String seasonId,
    required String episodeId,
  }) async {
    if (_disposed) return;
    if (seasonId != _currentSeasonId || _episodes.isEmpty) {
      await _loadSeasonEpisodes(seasonId);
      if (_disposed) return;
    }
    final idx = _episodes.indexWhere((e) => e.Id == episodeId);
    if (idx < 0) return;
    if (_currentSeasonId == seasonId && idx == _currentIndex.value) return;
    _markMoved();
    _setCurrent(idx);
  }

  Future<void> _crossSeason({required int direction}) async {
    final curSeason = _currentSeasonId;
    if (curSeason == null) return;
    final seasons = await loadSeasons(seriesId);
    if (_disposed) return;
    final ordered = [...seasons]
      ..sort((a, b) => (a.IndexNumber ?? 0).compareTo(b.IndexNumber ?? 0));
    final curPos = ordered.indexWhere((s) => s.Id == curSeason);
    if (curPos < 0) return;
    final targetPos = curPos + direction;
    if (targetPos < 0 || targetPos >= ordered.length) return;
    final target = ordered[targetPos];
    await _loadSeasonEpisodes(target.Id);
    if (_disposed || _episodes.isEmpty) return;
    _markMoved();
    _setCurrent(direction > 0 ? 0 : _episodes.length - 1);
  }

  Future<void> _loadSeasonEpisodes(String seasonId) async {
    final eps = await loadEpisodes(seriesId: seriesId, seasonId: seasonId);
    if (_disposed) return;
    _currentSeasonId = seasonId;
    _episodes = List.unmodifiable(eps);
  }

  void _markMoved() {
    _initialApplied = true;
    _userNavigated = true;
  }

  void _setCurrent(int index) {
    _currentIndex.value = index;
    _current.value = _itemFromEpisode(_episodes[index]);
  }

  PlaylistItem _itemFromEpisode(EmbyRawItem ep) {
    final useInitial = !_initialApplied;
    _initialApplied = true;
    return PlaylistItem(
      itemId: ep.Id,
      mediaSourceId: useInitial ? _initialMediaSourceId : null,
      startPositionTicks: useInitial ? _initialStartPositionTicks : null,
      preferredAudioStreamIndex: useInitial ? _initialPreferredAudio : null,
      preferredSubtitleStreamIndex: useInitial ? _initialPreferredSub : null,
      displayLabel: _labelOf(ep),
      playFromStart: useInitial && _initialPlayFromStart,
      preloadedItem: ep,
    );
  }

  String _labelOf(EmbyRawItem ep) {
    final ep0 = ep.IndexNumber;
    final name = ep.Name;
    if (ep0 != null && name.isNotEmpty) {
      return '${formatEpisodeNumber(ep0)} $name';
    }
    if (name.isNotEmpty) return name;
    return ep.Id;
  }

  @override
  void dispose() {
    _disposed = true;
    _current.dispose();
    _currentIndex.dispose();
  }
}
