import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../../core/contracts/cancellation.dart';
import '../../../../core/contracts/danmaku.dart';
import '../../../../core/domain/ports/danmaku_gateway.dart';
import '../../../../core/media/danmaku_comment_pipeline.dart';
import '../../../../providers/danmaku_settings_provider.dart';
import '../../../../providers/di_providers.dart';
import '../../../../shared/utils/app_logger.dart';
import 'danmaku_matcher.dart';

const _tag = 'DanmakuSession';

class DanmakuSessionNotifier extends AsyncNotifier<DanmakuSessionState> {
  late final DanmakuGateway _gateway;
  late final DanmakuMatcher _matcher;

  CancellationToken? _token;
  String? _currentItemId;
  List<DanmakuSource> _sources = const [];
  int _chConvert = 0;

  @override
  Future<DanmakuSessionState> build() async {
    _gateway = ref.read(danmakuGatewayProvider);
    _matcher = DanmakuMatcher(gateway: _gateway);
    ref.onDispose(() => _token?.cancel('notifier disposed'));
    return const DanmakuSessionIdle();
  }

  Future<void> loadFor(DanmakuMatchContext context) async {
    final token = _swapToken('loadFor:${context.itemId}');
    _currentItemId = context.itemId;

    final settings = _readSettings();
    final sources = settings.enabledSources;
    if (!settings.enabled || sources.isEmpty) {
      AppLogger.debug(
        _tag,
        'loadFor(${context.itemId}) -> Idle: '
        'enabled=${settings.enabled} sources=${sources.length}',
      );
      _sources = const [];
      state = const AsyncData(DanmakuSessionIdle());
      return;
    }

    _sources = sources;
    _chConvert = _chConvertFromMode(settings.chineseConvert);
    state = const AsyncData(DanmakuSessionMatching());

    try {
      final result = await _matcher.resolve(
        sources: _sources,
        context: context,
        cancelToken: token,
      );
      if (_isStale(token, context.itemId)) return;

      if (result.matches.isEmpty || result.pickedIndex < 0) {
        AppLogger.info(
          _tag,
          'loadFor(${context.itemId}) -> Empty: no matches found',
        );
        state = const AsyncData(DanmakuSessionEmpty());
        return;
      }

      state = AsyncData(
        DanmakuSessionMatched(
          matches: result.matches,
          selectedIndex: result.pickedIndex,
          commentStatus: DanmakuCommentStatus.loading,
        ),
      );

      AppLogger.debug(
        _tag,
        'auto-select index=${result.pickedIndex} '
        '(target ep=${context.episode})',
      );
      await _loadCommentsForCurrent(token: token);
    } on CancelledException {
      return;
    } catch (e, st) {
      if (_isStale(token, context.itemId)) return;
      AppLogger.error(_tag, 'loadFor failed', e, st);
      state = AsyncData(DanmakuSessionFailed(e.toString()));
    }
  }

  Future<void> selectMatch(int index) async {
    final current = state.value;
    if (current is! DanmakuSessionMatched) return;
    if (index < 0 || index >= current.matches.length) return;
    if (index == current.selectedIndex &&
        current.commentStatus == DanmakuCommentStatus.ready) {
      return;
    }

    final token = _swapToken('selectMatch:$index');
    state = AsyncData(
      current.copyWith(
        selectedIndex: index,
        commentStatus: DanmakuCommentStatus.loading,
      ),
    );
    await _loadCommentsForCurrent(token: token);
  }

  Future<void> applyManualSelection({
    required DanmakuSource source,
    required int episodeId,
    required int animeId,
    required String animeTitle,
    required String episodeTitle,
  }) async {
    if (source.url.isEmpty || episodeId <= 0) return;
    final current = state.value;
    final picked = DanmakuMatchCandidate(
      source: source,
      match: DanmakuMatch(
        episodeId: episodeId,
        animeId: animeId,
        animeTitle: animeTitle,
        episodeTitle: episodeTitle,
      ),
    );

    final previous = current is DanmakuSessionMatched ? current : null;
    final newMatches = previous == null
        ? [picked]
        : [
            picked,
            ...previous.matches.where(
              (m) => m.episodeId != episodeId || m.source.url != source.url,
            ),
          ];

    final token = _swapToken('applyManual:$episodeId');
    state = AsyncData(
      DanmakuSessionMatched(
        matches: newMatches,
        selectedIndex: 0,
        commentStatus: DanmakuCommentStatus.loading,
      ),
    );

    try {
      await _loadCommentsForCurrent(token: token);
    } catch (_) {
      if (_isStale(token, _currentItemId)) return;
      if (previous != null) {
        state = AsyncData(previous);
      } else {
        state = const AsyncData(DanmakuSessionEmpty());
      }
      rethrow;
    }
  }

  Future<List<DanmakuMatchCandidate>?> manualSearch(String query) async {
    final trimmed = query.trim();
    if (_sources.isEmpty || trimmed.isEmpty) return const [];
    final token = _token;
    final results = <DanmakuMatchCandidate>[];

    for (final source in _sources) {
      List<DanmakuMatch> matches;
      try {
        matches = await _gateway.match(source.url, trimmed, 0);
      } on CancelledException {
        return null;
      } catch (e) {
        if (token != null && token.isCancelled) return null;
        AppLogger.warn(_tag, 'manualSearch source "${source.url}" failed', e);
        continue;
      }
      if (token != null && token.isCancelled) return null;
      results.addAll(
        matches.map(
          (match) => DanmakuMatchCandidate(source: source, match: match),
        ),
      );
    }

    return results;
  }

  Future<List<DanmakuBangumiEpisode>?> listEpisodesForAnime(
    DanmakuSource source,
    int animeId,
  ) async {
    if (source.url.isEmpty || animeId <= 0) return const [];
    final token = _token;
    final res = await _gateway.getBangumiEpisodes(source.url, animeId);
    if (token != null && token.isCancelled) return null;
    return res;
  }

  Future<void> _loadCommentsForCurrent({
    required CancellationToken token,
  }) async {
    final current = state.value;
    if (current is! DanmakuSessionMatched) return;
    final selected = current.matches[current.selectedIndex];
    final episodeId = selected.episodeId;
    final settings = _readSettings();

    try {
      final raw = await _gateway.fetchComments(
        selected.source.url,
        episodeId,
        chConvert: _chConvert,
      );
      token.throwIfCancelled();
      final comments = processDanmakuComments(
        raw,
        blockedKeywords: settings.blockedKeywords,
        mergeDuplicates: settings.mergeDuplicates,
      );
      if (_isStale(token, _currentItemId)) return;
      AppLogger.debug(
        _tag,
        'comments ready from "${selected.source.displayName}" '
        'for ep=$episodeId: ${comments.length} items',
      );
      final cur = state.value;
      if (cur is DanmakuSessionMatched) {
        state = AsyncData(
          cur.copyWith(
            commentStatus: DanmakuCommentStatus.ready,
            comments: comments,
          ),
        );
      }
    } on CancelledException {
      return;
    } catch (e) {
      if (_isStale(token, _currentItemId)) return;
      AppLogger.warn(_tag, 'load comments failed for ep=$episodeId', e);
      final cur = state.value;
      if (cur is DanmakuSessionMatched) {
        state = AsyncData(
          cur.copyWith(commentStatus: DanmakuCommentStatus.failed),
        );
      }
    }
  }

  CancellationToken _swapToken(String reason) {
    _token?.cancel(reason);
    final t = CancellationToken();
    _token = t;
    return t;
  }

  bool _isStale(CancellationToken token, String? itemIdAtStart) {
    if (token.isCancelled) return true;
    if (token != _token) return true;
    if (itemIdAtStart != null && itemIdAtStart != _currentItemId) return true;
    return false;
  }

  DanmakuSettingsState _readSettings() =>
      ref.read(danmakuSettingsProvider).value ?? const DanmakuSettingsState();

  static int _chConvertFromMode(ChineseConvertMode mode) => switch (mode) {
    ChineseConvertMode.toSimplified => 1,
    ChineseConvertMode.toTraditional => 2,
    ChineseConvertMode.none => 0,
  };
}

final danmakuSessionProvider =
    AsyncNotifierProvider.autoDispose<
      DanmakuSessionNotifier,
      DanmakuSessionState
    >(DanmakuSessionNotifier.new);
