part of '../di_providers.dart';

typedef TmdbMediaKey = ({String tmdbId, String mediaType});
typedef TmdbSeasonKey = ({String tmdbId, int seasonNumber});


typedef _TmdbReady = ({TmdbRequestConfig config, String language});


final _tmdbReadyProvider = FutureProvider<_TmdbReady?>((ref) async {
  final settings = await ref.watch(tmdbSettingsProvider.future);
  if (!settings.canRequest) return null;

  if (settings.accessMode == TmdbAccessMode.smAccount) {
    final account = await ref.watch(smAccountProvider.future);
    if (!account.canUseDataPlane) return null;
    final notifier = ref.read(smAccountProvider.notifier);
    final token = await notifier.getAccessToken();
    if (token == null) return null;
    return (
      config: TmdbRequestConfig(
        apiBaseUrl: account.tmdbApiBaseUrl,
        imageBaseUrl: TmdbSettingsState.officialImageBaseUrl,
        accessToken: token,
        apiKey: '',
        onUnauthorized: () => notifier.getAccessToken(force: true),
      ),
      language: settings.language,
    );
  }

  return (
    config: TmdbRequestConfig(
      apiBaseUrl: settings.effectiveApiBaseUrl,
      imageBaseUrl: settings.effectiveImageBaseUrl,
      accessToken: settings.effectiveAccessToken,
      apiKey: settings.effectiveApiKey,
    ),
    language: settings.language,
  );
});


final _tmdbCacheScopeProvider = FutureProvider<DiscoverPageCacheScope?>((
  ref,
) async {
  final settings = await ref.watch(tmdbSettingsProvider.future);
  if (!settings.canRequest) return null;

  if (settings.accessMode == TmdbAccessMode.smAccount) {
    final account = await ref.watch(smAccountProvider.future);
    if (!account.hasActiveSession) return null;
    return DiscoverPageCacheScope(
      apiBaseUrl: account.tmdbApiBaseUrl,
      imageBaseUrl: TmdbSettingsState.officialImageBaseUrl,
      language: settings.language,
    );
  }

  return DiscoverPageCacheScope(
    apiBaseUrl: settings.effectiveApiBaseUrl,
    imageBaseUrl: settings.effectiveImageBaseUrl,
    language: settings.language,
  );
});


final tmdbEnrichedDetailProvider =
    FutureProvider.family<TmdbEnrichedDetail?, TmdbMediaKey>((ref, key) async {
      final ready = await ref.watch(_tmdbReadyProvider.future);
      if (ready == null || key.tmdbId.isEmpty) return null;
      return ref
          .read(tmdbGatewayProvider)
          .getEnrichedDetail(
            ready.config,
            key.tmdbId,
            key.mediaType,
            language: ready.language,
          );
    });


final tmdbImagesProvider = Provider.family<TmdbImageSet?, TmdbMediaKey>((
  ref,
  key,
) {
  return ref.watch(tmdbEnrichedDetailProvider(key)).value?.images;
});


final tmdbMediaDetailProvider = Provider.family<TmdbMediaDetail?, TmdbMediaKey>(
  (ref, key) {
    return ref.watch(tmdbEnrichedDetailProvider(key)).value?.detail;
  },
);


final tmdbSeasonDetailProvider =
    FutureProvider.family<TmdbSeasonDetail?, TmdbSeasonKey>((ref, key) async {
      final ready = await ref.watch(_tmdbReadyProvider.future);
      if (ready == null || key.tmdbId.isEmpty) return null;
      return ref
          .read(tmdbGatewayProvider)
          .getSeasonDetail(
            ready.config,
            key.tmdbId,
            key.seasonNumber,
            language: ready.language,
          );
    });

enum _TmdbRecommendationRowKind {
  trendingDay,
  trendingWeek,
  popularMovies,
  popularTv,
  airingTodayTv,
  topRatedMovies,
}

final _tmdbRecommendationRowProvider =
    AsyncNotifierProvider.family<
      _TmdbRecommendationRowNotifier,
      TmdbRecommendationsRes?,
      _TmdbRecommendationRowKind
    >(_TmdbRecommendationRowNotifier.new);

final tmdbTrendingDayProvider = _tmdbRecommendationRowProvider(
  _TmdbRecommendationRowKind.trendingDay,
);

final tmdbTrendingWeekProvider = _tmdbRecommendationRowProvider(
  _TmdbRecommendationRowKind.trendingWeek,
);

final tmdbPopularMoviesProvider = _tmdbRecommendationRowProvider(
  _TmdbRecommendationRowKind.popularMovies,
);

final tmdbPopularTvProvider = _tmdbRecommendationRowProvider(
  _TmdbRecommendationRowKind.popularTv,
);

final tmdbAiringTodayTvProvider = _tmdbRecommendationRowProvider(
  _TmdbRecommendationRowKind.airingTodayTv,
);

final tmdbTopRatedMoviesProvider = _tmdbRecommendationRowProvider(
  _TmdbRecommendationRowKind.topRatedMovies,
);


typedef TmdbDiscoverKey = ({
  String mediaType,
  String sortBy,
  String? withGenres,
  String? voteCountGte,
});

final tmdbDiscoverProvider =
    AsyncNotifierProvider.family<
      _TmdbDiscoverRowNotifier,
      TmdbRecommendationsRes?,
      TmdbDiscoverKey
    >(_TmdbDiscoverRowNotifier.new);

class _TmdbRecommendationRowNotifier
    extends AsyncNotifier<TmdbRecommendationsRes?> {
  final _TmdbRecommendationRowKind arg;

  _TmdbRecommendationRowNotifier(this.arg);

  @override
  Future<TmdbRecommendationsRes?> build() async {
    final rowKey = _rowKeyForKind(arg);

    final scope = await ref.watch(_tmdbCacheScopeProvider.future);
    if (scope == null) return null;

    final cached = await _readDiscoverRowCacheByScope(ref, scope, rowKey);
    if (cached != null) {
      unawaited(_refreshFromNetwork(arg, rowKey));
      return cached;
    }

    final ready = await ref.watch(_tmdbReadyProvider.future);
    if (ready == null) return null;
    return _fetchAndCache(arg, ready, rowKey);
  }

  Future<void> _refreshFromNetwork(
    _TmdbRecommendationRowKind kind,
    String rowKey,
  ) async {
    try {
      final ready = await ref.read(_tmdbReadyProvider.future);
      if (ready == null) return;
      final fresh = await _fetchAndCache(kind, ready, rowKey);
      if (fresh != null) {
        state = AsyncValue.data(fresh);
      }
    } catch (e) {
      AppLogger.warn('TMDB', 'refresh discover row $rowKey failed', e);
    }
  }

  Future<TmdbRecommendationsRes?> _fetchAndCache(
    _TmdbRecommendationRowKind kind,
    _TmdbReady ready,
    String rowKey,
  ) async {
    final fresh = await _fetchRow(kind, ready);
    if (fresh != null) {
      await _writeDiscoverRowCache(ref, ready, rowKey, fresh);
    }
    return fresh;
  }

  Future<TmdbRecommendationsRes?> _fetchRow(
    _TmdbRecommendationRowKind kind,
    _TmdbReady ready,
  ) {
    final gateway = ref.read(tmdbGatewayProvider);
    return switch (kind) {
      _TmdbRecommendationRowKind.trendingDay => gateway.getTrending(
        ready.config,
        language: ready.language,
        mediaType: 'all',
        timeWindow: 'day',
      ),
      _TmdbRecommendationRowKind.trendingWeek => gateway.getTrending(
        ready.config,
        language: ready.language,
        mediaType: 'all',
        timeWindow: 'week',
      ),
      _TmdbRecommendationRowKind.popularMovies => gateway.getPopularMovies(
        ready.config,
        language: ready.language,
      ),
      _TmdbRecommendationRowKind.popularTv => gateway.getPopularTv(
        ready.config,
        language: ready.language,
      ),
      _TmdbRecommendationRowKind.airingTodayTv => gateway.getAiringTodayTv(
        ready.config,
        language: ready.language,
      ),
      _TmdbRecommendationRowKind.topRatedMovies => gateway.getTopRatedMovies(
        ready.config,
        language: ready.language,
      ),
    };
  }
}

class _TmdbDiscoverRowNotifier extends AsyncNotifier<TmdbRecommendationsRes?> {
  final TmdbDiscoverKey arg;

  _TmdbDiscoverRowNotifier(this.arg);

  @override
  Future<TmdbRecommendationsRes?> build() async {
    final rowKey = _rowKeyForDiscover(arg);

    final scope = await ref.watch(_tmdbCacheScopeProvider.future);
    if (scope == null) return null;

    final cached = await _readDiscoverRowCacheByScope(ref, scope, rowKey);
    if (cached != null) {
      unawaited(_refreshFromNetwork(arg, rowKey));
      return cached;
    }

    final ready = await ref.watch(_tmdbReadyProvider.future);
    if (ready == null) return null;
    return _fetchAndCache(arg, ready, rowKey);
  }

  Future<void> _refreshFromNetwork(TmdbDiscoverKey key, String rowKey) async {
    try {
      final ready = await ref.read(_tmdbReadyProvider.future);
      if (ready == null) return;
      final fresh = await _fetchAndCache(key, ready, rowKey);
      if (fresh != null) {
        state = AsyncValue.data(fresh);
      }
    } catch (e) {
      AppLogger.warn('TMDB', 'refresh discover row $rowKey failed', e);
    }
  }

  Future<TmdbRecommendationsRes?> _fetchAndCache(
    TmdbDiscoverKey key,
    _TmdbReady ready,
    String rowKey,
  ) async {
    final fresh = await ref
        .read(tmdbGatewayProvider)
        .getDiscover(
          ready.config,
          key.mediaType,
          language: ready.language,
          sortBy: key.sortBy,
          withGenres: key.withGenres,
          voteCountGte: key.voteCountGte,
        );
    if (fresh != null) {
      await _writeDiscoverRowCache(ref, ready, rowKey, fresh);
    }
    return fresh;
  }
}

DiscoverPageCacheScope _cacheScopeForReady(_TmdbReady ready) {
  return DiscoverPageCacheScope(
    apiBaseUrl: ready.config.apiBaseUrl,
    imageBaseUrl: ready.config.imageBaseUrl,
    language: ready.language,
  );
}

Future<TmdbRecommendationsRes?> _readDiscoverRowCacheByScope(
  Ref ref,
  DiscoverPageCacheScope scope,
  String rowKey,
) async {
  final store = await ref.read(discoverPageCacheStoreProvider.future);
  final entry = await store.readRow(scope, rowKey);
  return entry?.data;
}

Future<void> _writeDiscoverRowCache(
  Ref ref,
  _TmdbReady ready,
  String rowKey,
  TmdbRecommendationsRes data,
) async {
  final store = await ref.read(discoverPageCacheStoreProvider.future);
  await store.writeRow(_cacheScopeForReady(ready), rowKey, data);
}

String _rowKeyForKind(_TmdbRecommendationRowKind kind) {
  return switch (kind) {
    _TmdbRecommendationRowKind.trendingDay => 'trending:day',
    _TmdbRecommendationRowKind.trendingWeek => 'trending:week',
    _TmdbRecommendationRowKind.popularMovies => 'popular:movie',
    _TmdbRecommendationRowKind.popularTv => 'popular:tv',
    _TmdbRecommendationRowKind.airingTodayTv => 'airing_today:tv',
    _TmdbRecommendationRowKind.topRatedMovies => 'top_rated:movie',
  };
}

String _rowKeyForDiscover(TmdbDiscoverKey key) {
  return [
    'discover',
    key.mediaType,
    key.sortBy,
    key.withGenres ?? '',
    key.voteCountGte ?? '',
  ].join(':');
}
