import '../../../../core/contracts/cancellation.dart';
import '../../../../core/contracts/danmaku.dart';
import '../../../../core/domain/ports/danmaku_gateway.dart';
import '../../../../core/infra/danmaku_api/danmaku_client.dart';
import '../../../../core/media/danmaku_episode_parser.dart';
import '../../../../core/media/danmaku_match_picker.dart';
import '../../../../shared/utils/app_logger.dart';

const _tag = 'DanmakuMatcher';

typedef _SingleSourceMatcherResult = ({
  List<DanmakuMatch> matches,
  int pickedIndex,
});

const _emptySingleSourceResult = (matches: <DanmakuMatch>[], pickedIndex: -1);

class DanmakuMatcherResult {
  final List<DanmakuMatchCandidate> matches;
  final int pickedIndex;

  const DanmakuMatcherResult({
    required this.matches,
    required this.pickedIndex,
  });

  static const empty = DanmakuMatcherResult(matches: [], pickedIndex: -1);
}

class DanmakuMatcher {
  DanmakuMatcher({required this._gateway});

  final DanmakuGateway _gateway;

  Future<DanmakuMatcherResult> resolve({
    required List<DanmakuSource> sources,
    required DanmakuMatchContext context,
    CancellationToken? cancelToken,
  }) async {
    if (sources.isEmpty) return DanmakuMatcherResult.empty;

    final allMatches = <DanmakuMatchCandidate>[];
    var pickedIndex = -1;

    for (final source in sources) {
      cancelToken?.throwIfCancelled();
      _SingleSourceMatcherResult result;
      try {
        result = await _resolveSingle(
          sourceUrl: source.url,
          context: context,
          cancelToken: cancelToken,
        );
      } on CancelledException {
        rethrow;
      } catch (e) {
        AppLogger.warn(
          _tag,
          'source "${source.url}" match failed, skipping',
          e,
        );
        continue;
      }
      cancelToken?.throwIfCancelled();
      if (result.matches.isEmpty) continue;

      final baseIndex = allMatches.length;
      allMatches.addAll(
        result.matches.map(
          (match) => DanmakuMatchCandidate(source: source, match: match),
        ),
      );
      if (pickedIndex < 0 && result.pickedIndex >= 0) {
        pickedIndex = baseIndex + result.pickedIndex;
      }
    }

    if (allMatches.isEmpty) return DanmakuMatcherResult.empty;
    return DanmakuMatcherResult(
      matches: allMatches,
      pickedIndex: pickedIndex < 0 ? 0 : pickedIndex,
    );
  }

  Future<_SingleSourceMatcherResult> _resolveSingle({
    required String sourceUrl,
    required DanmakuMatchContext context,
    CancellationToken? cancelToken,
  }) async {
    final fileName = buildDanmakuFileName(
      name: context.name,
      type: context.type,
      seriesName: context.seriesName,
      season: context.season,
      episode: context.episode,
    );
    final episodeIndex = context.episode;

    AppLogger.debug(
      _tag,
      'match request source="$sourceUrl" fileName="$fileName" '
      'durationSec=${context.durationSec} targetEp=$episodeIndex',
    );

    var matches = await _gateway.match(
      sourceUrl,
      fileName,
      context.durationSec,
    );
    cancelToken?.throwIfCancelled();

    AppLogger.debug(
      _tag,
      'match received from "$sourceUrl": ${matches.length} entries',
    );

    if (matches.isEmpty &&
        context.type == 'Episode' &&
        context.seriesName != null &&
        context.seriesName!.isNotEmpty) {
      AppLogger.info(
        _tag,
        'exact match empty, falling back to searchEpisodes source="$sourceUrl" '
        'seriesName="${context.seriesName}" ep=$episodeIndex',
      );
      matches = await _gateway.searchEpisodes(
        sourceUrl,
        context.seriesName!,
        episodeIndex,
      );
      cancelToken?.throwIfCancelled();
      AppLogger.debug(
        _tag,
        'searchEpisodes fallback: ${matches.length} entries',
      );
    }

    if (matches.isEmpty) return _emptySingleSourceResult;

    final pickIndex = pickBestMatchIndex(matches, episodeIndex);

    if (episodeIndex != null) {
      final pickedEp = extractEpisodeNumberFromTitle(
        matches[pickIndex].episodeTitle,
      );
      if (pickedEp != episodeIndex) {
        final corrected = await _correctViaBangumi(
          sourceUrl: sourceUrl,
          animeId: matches[pickIndex].animeId,
          targetEpisode: episodeIndex,
          animeTitle: matches[pickIndex].animeTitle,
          cancelToken: cancelToken,
        );
        if (corrected != null) {
          AppLogger.info(
            _tag,
            'bangumi correction applied: episodeId=${corrected.episodeId} '
            'title="${corrected.episodeTitle}"',
          );
          return (matches: [corrected, ...matches], pickedIndex: 0);
        }
      }
    }

    return (matches: matches, pickedIndex: pickIndex);
  }

  Future<DanmakuMatch?> _correctViaBangumi({
    required String sourceUrl,
    required int animeId,
    required int targetEpisode,
    required String animeTitle,
    CancellationToken? cancelToken,
  }) async {
    if (animeId <= 0) {
      AppLogger.warn(
        _tag,
        'skip bangumi correction: animeId missing in match response',
      );
      return null;
    }
    AppLogger.debug(
      _tag,
      'bangumi correction probing: animeId=$animeId targetEp=$targetEpisode',
    );
    final episodes = await _gateway.getBangumiEpisodes(sourceUrl, animeId);
    cancelToken?.throwIfCancelled();
    if (episodes.isEmpty) {
      AppLogger.warn(_tag, 'bangumi $animeId returned no episodes');
      return null;
    }
    for (final ep in episodes) {
      final n =
          int.tryParse(ep.episodeNumber) ??
          extractEpisodeNumberFromTitle(ep.episodeTitle);
      if (n == targetEpisode) {
        return DanmakuMatch(
          episodeId: ep.episodeId,
          animeId: animeId,
          animeTitle: animeTitle,
          episodeTitle: ep.episodeTitle,
        );
      }
    }
    AppLogger.warn(
      _tag,
      'bangumi $animeId has ${episodes.length} episodes but none matches '
      'targetEp=$targetEpisode',
    );
    return null;
  }
}
