import '../../../shared/utils/app_logger.dart';
import '../../contracts/library.dart';
import '../../contracts/trakt/trakt_ids.dart';
import '../../contracts/trakt/trakt_response.dart';
import '../../contracts/trakt/trakt_scrobble_target.dart';
import '../../contracts/trakt/trakt_sync_job.dart';
import '../../domain/ports/authed_trakt_gateway.dart';
import 'trakt_media_matcher.dart';
import 'trakt_pending_sync_queue.dart';

const _tag = 'TraktSync';


class TraktSyncService {
  TraktSyncService({required this._traktGateway, required this._queue});

  final AuthedTraktGateway _traktGateway;
  final TraktPendingSyncQueue _queue;


  Future<void> syncEmbyPlayedChange({
    required EmbyRawItem item,
    required bool played,
    EmbyRawItem? seriesItem,
  }) async {
    try {
      final target = TraktMediaMatcher.match(
        itemType: item.Type,
        itemName: item.Name,
        productionYear: item.ProductionYear,
        providerIds: providerIdsOfItem(item),
        seriesProviderIds: seriesItem == null
            ? const {}
            : providerIdsOfItem(seriesItem),
        seriesName: item.SeriesName ?? seriesItem?.Name,
        seriesProductionYear: seriesItem?.ProductionYear,
        parentIndexNumber: (item.extra['ParentIndexNumber'] as num?)?.toInt(),
        itemIndexNumber: item.IndexNumber,
      );
      if (target == null) return;

      switch (target.kind) {
        case TraktMediaKind.movie:
          if (played) {
            await markWatched(
              ids: target.ids,
              title: target.title,
              year: target.year,
            );
          } else {
            await markUnwatched(
              ids: target.ids,
              title: target.title,
              year: target.year,
            );
          }
        case TraktMediaKind.episode:
          if (played) {
            await markWatched(
              ids: target.episodeIds,
              title: item.Name,
              year: item.ProductionYear,
              season: target.season,
              episode: target.number,
              showIds: target.ids,
              showTitle: target.title,
              showYear: target.year,
            );
          } else {
            await markUnwatched(
              ids: target.episodeIds,
              title: item.Name,
              year: item.ProductionYear,
              season: target.season,
              episode: target.number,
              showIds: target.ids,
              showTitle: target.title,
              showYear: target.year,
            );
          }
      }
    } catch (e) {
      AppLogger.warn(_tag, 'syncEmbyPlayedChange failed', e);
    }
  }


  Future<void> markWatched({
    required TraktIds ids,
    required String title,
    required int? year,
    DateTime? watchedAt,
    int? season,
    int? episode,
    TraktIds? showIds,
    String? showTitle,
    int? showYear,
  }) async {
    final body = _buildHistoryBody(
      ids: ids,
      title: title,
      year: year,
      watchedAt: watchedAt ?? DateTime.now().toUtc(),
      season: season,
      episode: episode,
      showIds: showIds,
      showTitle: showTitle,
      showYear: showYear,
    );

    AppLogger.debug(_tag, 'markWatched: $title');

    await _sendHistory(TraktSyncJobType.historyAdd, body, label: 'markWatched');
  }


  Future<void> markUnwatched({
    required TraktIds ids,
    required String title,
    required int? year,
    int? season,
    int? episode,
    TraktIds? showIds,
    String? showTitle,
    int? showYear,
  }) async {
    final body = _buildHistoryBody(
      ids: ids,
      title: title,
      year: year,
      watchedAt: null,
      season: season,
      episode: episode,
      showIds: showIds,
      showTitle: showTitle,
      showYear: showYear,
    );

    AppLogger.debug(_tag, 'markUnwatched: $title');

    await _sendHistory(
      TraktSyncJobType.historyRemove,
      body,
      label: 'markUnwatched',
    );
  }


  Future<void> _sendHistory(
    TraktSyncJobType type,
    Map<String, dynamic> body, {
    required String label,
  }) async {
    TraktScrobbleOutcome outcome;
    try {
      outcome = await _traktGateway.syncHistory(type, body);
    } catch (e) {
      AppLogger.debug(_tag, '$label threw; enqueue', e);
      await _enqueue(type, body);
      return;
    }

    switch (outcome.cls) {
      case TraktResponseClass.success:
      case TraktResponseClass.duplicate:
        AppLogger.debug(_tag, '$label ok (${outcome.cls.name})');
        return;
      case TraktResponseClass.invalid:
        AppLogger.debug(_tag, '$label invalid → drop');
        return;
      case TraktResponseClass.rateLimited:
      case TraktResponseClass.unauthorized:
      case TraktResponseClass.transient:
        await _enqueue(type, body, retryAfter: outcome.retryAfter);
    }
  }

  Map<String, dynamic> _buildHistoryBody({
    required TraktIds ids,
    required String title,
    required int? year,
    required DateTime? watchedAt,
    int? season,
    int? episode,
    TraktIds? showIds,
    String? showTitle,
    int? showYear,
  }) {
    final isEpisode = season != null && episode != null;

    if (!isEpisode) {
      final movie = <String, dynamic>{'title': title, 'ids': ids.toJson()};
      if (year != null) movie['year'] = year;
      if (watchedAt != null) movie['watched_at'] = watchedAt.toIso8601String();
      return {
        'movies': [movie],
        'shows': [],
        'episodes': [],
      };
    }

    final show = <String, dynamic>{
      'title': showTitle ?? title,
      'ids': (showIds ?? const TraktIds()).toJson(),
    };
    if (showYear != null) show['year'] = showYear;

    final epNode = <String, dynamic>{'number': episode};
    if (watchedAt != null) epNode['watched_at'] = watchedAt.toIso8601String();

    show['seasons'] = [
      {
        'number': season,
        'episodes': [epNode],
      },
    ];

    return {
      'movies': [],
      'shows': [show],
      'episodes': [],
    };
  }

  Future<void> _enqueue(
    TraktSyncJobType type,
    Map<String, dynamic> body, {
    Duration? retryAfter,
  }) async {
    try {
      await _queue.enqueue(type: type, body: body, retryAfter: retryAfter);
    } catch (e) {
      AppLogger.warn(_tag, 'enqueue failed', e);
    }
  }
}
