import '../../media/media_backend.dart';
import '../../media/media_item.dart';
import '../../media/media_kind.dart';
import '../../utils/external_ids.dart';

/// External catalog providers that can back the Explore tab.
enum CatalogSourceId { plex, trakt, mal, anilist, simkl, seerr }

/// Normalized airing/production status across providers (Trakt `status`,
/// MAL `status`). Null when unknown or uninteresting (released movies).
enum CatalogAirStatus { airing, ended, canceled, upcoming }

/// External ids identifying a catalog item across providers and media
/// servers. A superset of [ExternalIds] that also carries provider-native
/// ids (Plex rating key, Trakt id/slug, MAL, AniList, and Simkl).
class CatalogItemIds {
  final String? plex;
  final int? trakt;
  final String? slug;
  final int? mal;
  final int? anilist;
  final int? simkl;
  final String? imdb;
  final int? tmdb;
  final int? tvdb;

  const CatalogItemIds({
    this.plex,
    this.trakt,
    this.slug,
    this.mal,
    this.anilist,
    this.simkl,
    this.imdb,
    this.tmdb,
    this.tvdb,
  });

  factory CatalogItemIds.fromExternal(ExternalIds ids) =>
      CatalogItemIds(imdb: ids.imdb, tmdb: ids.tmdb, tvdb: ids.tvdb);

  bool get hasAny =>
      imdb != null ||
      tmdb != null ||
      tvdb != null ||
      mal != null ||
      anilist != null ||
      simkl != null ||
      plex != null ||
      trakt != null ||
      slug != null;

  /// Stable identity key preferring globally-unique ids. Callers must
  /// namespace it by [MediaKind] (tmdb movie/show ids can collide).
  String? get canonicalKey {
    if (imdb != null) return 'imdb:$imdb';
    if (tmdb != null) return 'tmdb:$tmdb';
    if (tvdb != null) return 'tvdb:$tvdb';
    if (mal != null) return 'mal:$mal';
    if (anilist != null) return 'anilist:$anilist';
    if (simkl != null) return 'simkl:$simkl';
    if (plex != null) return 'plex:$plex';
    if (trakt != null) return 'trakt:$trakt';
    if (slug != null) return 'slug:$slug';
    return null;
  }

  /// Every id-form key. Membership checks match on any of these so that two
  /// sides carrying different id subsets (e.g. Jellyfin tmdb-only vs a Trakt
  /// entry keyed by imdb) still intersect.
  List<String> get allKeys => [
    if (imdb != null) 'imdb:$imdb',
    if (tmdb != null) 'tmdb:$tmdb',
    if (tvdb != null) 'tvdb:$tvdb',
    if (mal != null) 'mal:$mal',
    if (anilist != null) 'anilist:$anilist',
    if (simkl != null) 'simkl:$simkl',
    if (plex != null) 'plex:$plex',
    if (trakt != null) 'trakt:$trakt',
    if (slug != null) 'slug:$slug',
  ];

  ExternalIds toExternalIds() => ExternalIds(imdb: imdb, tmdb: tmdb, tvdb: tvdb);

  Map<String, Object?> toJson() => {
    if (plex != null) 'plex': plex,
    if (trakt != null) 'trakt': trakt,
    if (slug != null) 'slug': slug,
    if (mal != null) 'mal': mal,
    if (anilist != null) 'anilist': anilist,
    if (simkl != null) 'simkl': simkl,
    if (imdb != null) 'imdb': imdb,
    if (tmdb != null) 'tmdb': tmdb,
    if (tvdb != null) 'tvdb': tvdb,
  };

  factory CatalogItemIds.fromJson(Map<String, Object?> json) => CatalogItemIds(
    plex: json['plex'] as String?,
    trakt: json['trakt'] as int?,
    slug: json['slug'] as String?,
    mal: json['mal'] as int?,
    anilist: json['anilist'] as int?,
    simkl: json['simkl'] as int?,
    imdb: json['imdb'] as String?,
    tmdb: json['tmdb'] as int?,
    tvdb: json['tvdb'] as int?,
  );
}

/// A movie or show from an external catalog provider (Trakt trending, the
/// user's watchlist, ...). Not a library item: it has no server id and is
/// matched back to the user's libraries on demand.
class CatalogItem {
  /// Key under [MediaItem.raw] where a synthesized rendering item stashes its
  /// backing [CatalogItem] (see [toMediaItem]).
  static const String rawKey = 'plezyCatalog';

  final CatalogSourceId source;

  /// [MediaKind.movie] or [MediaKind.show].
  final MediaKind kind;
  final String title;
  final int? year;
  final String? overview;
  final int? runtimeMinutes;

  /// Provider community rating, 0–10.
  final double? rating;

  /// How many users the rating is based on (Trakt votes, MAL scoring users).
  final int? votes;
  final List<String>? genres;
  final String? certification;
  final String? trailerUrl;
  final CatalogAirStatus? airStatus;

  /// Aired episodes (Trakt) or total episodes (MAL); shows only.
  final int? episodeCount;

  /// TV network (Trakt) or animation studio (MAL).
  final String? network;
  final CatalogItemIds ids;

  /// Absolute https URLs served by the provider's CDN.
  final String? posterUrl;
  final String? backdropUrl;

  const CatalogItem({
    required this.source,
    required this.kind,
    required this.title,
    this.year,
    this.overview,
    this.runtimeMinutes,
    this.rating,
    this.votes,
    this.genres,
    this.certification,
    this.trailerUrl,
    this.airStatus,
    this.episodeCount,
    this.network,
    required this.ids,
    this.posterUrl,
    this.backdropUrl,
  });

  /// Kind-namespaced identity key for caches and dedupe.
  String get identityKey => '${kind.id}/${ids.canonicalKey}';

  /// Synthesize a [MediaItem] so catalog items flow through the existing
  /// shelf/grid/card stack ([MediaHub.items] is `List<MediaItem>`).
  ///
  /// The result is rendering-only and must never be persisted or handed to
  /// server-backed paths: `serverId` stays null and taps are intercepted by
  /// the catalog branch in `navigateToMediaItem`. `backend` is an arbitrary
  /// tag required by the union type. Poster/backdrop are absolute URLs, which
  /// the image pipeline loads directly.
  MediaItem toMediaItem() => MediaItem(
    id: 'catalog:${source.name}:$identityKey',
    backend: MediaBackend.plex,
    kind: kind,
    title: title,
    summary: overview,
    year: year,
    contentRating: certification,
    durationMs: runtimeMinutes == null ? null : Duration(minutes: runtimeMinutes!).inMilliseconds,
    rating: rating,
    genres: genres,
    thumbPath: posterUrl,
    artPath: backdropUrl,
    raw: {rawKey: toJson()},
  );

  Map<String, Object?> toJson() => {
    'source': source.name,
    'kind': kind.id,
    'title': title,
    if (year != null) 'year': year,
    if (overview != null) 'overview': overview,
    if (runtimeMinutes != null) 'runtimeMinutes': runtimeMinutes,
    if (rating != null) 'rating': rating,
    if (votes != null) 'votes': votes,
    if (genres != null) 'genres': genres,
    if (certification != null) 'certification': certification,
    if (trailerUrl != null) 'trailerUrl': trailerUrl,
    if (airStatus != null) 'airStatus': airStatus!.name,
    if (episodeCount != null) 'episodeCount': episodeCount,
    if (network != null) 'network': network,
    'ids': ids.toJson(),
    if (posterUrl != null) 'posterUrl': posterUrl,
    if (backdropUrl != null) 'backdropUrl': backdropUrl,
  };

  factory CatalogItem.fromJson(Map<String, Object?> json) => CatalogItem(
    // Round-trips are same-session toJson output — an unknown source is a
    // bug, and defaulting it would bind watchlist/cast/related calls to the
    // wrong provider. Fail loudly instead.
    source:
        CatalogSourceId.values.asNameMap()[json['source']] ??
        (throw ArgumentError('Unknown catalog source: ${json['source']}')),
    kind: MediaKind.fromString(json['kind'] as String?),
    title: json['title'] as String? ?? '',
    year: json['year'] as int?,
    overview: json['overview'] as String?,
    runtimeMinutes: json['runtimeMinutes'] as int?,
    rating: (json['rating'] as num?)?.toDouble(),
    votes: json['votes'] as int?,
    genres: (json['genres'] as List?)?.cast<String>(),
    certification: json['certification'] as String?,
    trailerUrl: json['trailerUrl'] as String?,
    airStatus: CatalogAirStatus.values.asNameMap()[json['airStatus']],
    episodeCount: json['episodeCount'] as int?,
    network: json['network'] as String?,
    ids: CatalogItemIds.fromJson((json['ids'] as Map?)?.cast<String, Object?>() ?? const {}),
    posterUrl: json['posterUrl'] as String?,
    backdropUrl: json['backdropUrl'] as String?,
  );
}
