import 'package:dio/dio.dart';

import '../../contracts/tmdb.dart';
import '../../domain/ports/tmdb_gateway.dart';


const String kTmdbConfigExtraKey = 'tmdbConfig';


const String kTmdbAcceptLanguageExtraKey = 'tmdbAcceptLanguage';


const String kTmdbOverrideAccessTokenKey = 'tmdbOverrideAccessToken';


const String kTmdbAuthRetriedKey = 'tmdbAuthRetried';


class TmdbRestApi {
  TmdbRestApi(this._dio);

  final Dio _dio;

  Future<TmdbCreditsRes> getCredits(
    String mediaType,
    String tmdbId,
    String language,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/$mediaType/$tmdbId/credits',
      queryParameters: {'language': language},
      extras: extras,
      fromJson: TmdbCreditsRes.fromJson,
    );
  }

  Future<TmdbRecommendationsRes> getRecommendations(
    String mediaType,
    String tmdbId,
    String language,
    String page,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/$mediaType/$tmdbId/recommendations',
      queryParameters: {'language': language, 'page': page},
      extras: extras,
      fromJson: TmdbRecommendationsRes.fromJson,
    );
  }

  Future<TmdbSeasonDetail> getSeasonDetail(
    String tmdbId,
    int seasonNumber,
    String language,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/tv/$tmdbId/season/$seasonNumber',
      queryParameters: {'language': language},
      extras: extras,
      fromJson: TmdbSeasonDetail.fromJson,
    );
  }

  Future<TmdbImageSet> getImages(
    String mediaType,
    String tmdbId,
    String includeImageLanguage,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/$mediaType/$tmdbId/images',
      queryParameters: {'include_image_language': includeImageLanguage},
      extras: extras,
      fromJson: TmdbImageSet.fromJson,
    );
  }

  Future<TmdbEnrichedDetail> getEnrichedDetail(
    String mediaType,
    String tmdbId,
    String language,
    String appendToResponse,
    String? includeImageLanguage,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/$mediaType/$tmdbId',
      queryParameters: {
        'language': language,
        'append_to_response': appendToResponse,
        'include_image_language': includeImageLanguage,
      },
      extras: extras,
      fromJson: TmdbEnrichedDetail.fromJson,
    );
  }

  Future<TmdbMediaDetail> getMediaDetail(
    String mediaType,
    String tmdbId,
    String language,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/$mediaType/$tmdbId',
      queryParameters: {'language': language},
      extras: extras,
      fromJson: TmdbMediaDetail.fromJson,
    );
  }

  Future<TmdbVideosRes> getVideos(
    String mediaType,
    String tmdbId,
    String language,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/$mediaType/$tmdbId/videos',
      queryParameters: {'language': language},
      extras: extras,
      fromJson: TmdbVideosRes.fromJson,
    );
  }

  Future<TmdbPersonDetail> getPersonDetail(
    int personId,
    String language,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/person/$personId',
      queryParameters: {'language': language},
      extras: extras,
      fromJson: TmdbPersonDetail.fromJson,
    );
  }

  Future<TmdbPersonCreditsRes> getPersonCredits(
    int personId,
    String language,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/person/$personId/combined_credits',
      queryParameters: {'language': language},
      extras: extras,
      fromJson: TmdbPersonCreditsRes.fromJson,
    );
  }

  Future<TmdbRecommendationsRes> getTrending(
    String mediaType,
    String timeWindow,
    String language,
    String page,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/trending/$mediaType/$timeWindow',
      queryParameters: {'language': language, 'page': page},
      extras: extras,
      fromJson: TmdbRecommendationsRes.fromJson,
    );
  }

  Future<TmdbRecommendationsRes> getPopularMovies(
    String language,
    String page,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/movie/popular',
      queryParameters: {'language': language, 'page': page},
      extras: extras,
      fromJson: TmdbRecommendationsRes.fromJson,
    );
  }

  Future<TmdbRecommendationsRes> getPopularTv(
    String language,
    String page,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/tv/popular',
      queryParameters: {'language': language, 'page': page},
      extras: extras,
      fromJson: TmdbRecommendationsRes.fromJson,
    );
  }

  Future<TmdbRecommendationsRes> getAiringTodayTv(
    String language,
    String page,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/tv/airing_today',
      queryParameters: {'language': language, 'page': page},
      extras: extras,
      fromJson: TmdbRecommendationsRes.fromJson,
    );
  }

  Future<TmdbRecommendationsRes> getTopRatedMovies(
    String language,
    String page,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/movie/top_rated',
      queryParameters: {'language': language, 'page': page},
      extras: extras,
      fromJson: TmdbRecommendationsRes.fromJson,
    );
  }

  Future<TmdbRecommendationsRes> getDiscover(
    String mediaType,
    String language,
    String page,
    String sortBy,
    String? withGenres,
    String? voteCountGte,
    Map<String, dynamic> extras,
  ) {
    return _getJson(
      '/discover/$mediaType',
      queryParameters: {
        'language': language,
        'page': page,
        'sort_by': sortBy,
        'with_genres': withGenres,
        'vote_count.gte': voteCountGte,
      },
      extras: extras,
      fromJson: TmdbRecommendationsRes.fromJson,
    );
  }

  Future<TmdbRawResponse<dynamic>> getConfiguration(
    Map<String, dynamic> extras,
  ) async {
    final response = await _dio.get<dynamic>(
      '/configuration',
      options: Options(extra: extras),
    );
    return TmdbRawResponse(data: response.data, response: response);
  }

  Future<T> _getJson<T>(
    String path, {
    required Map<String, Object?> queryParameters,
    required Map<String, dynamic> extras,
    required T Function(Map<String, dynamic> json) fromJson,
  }) async {
    final response = await _dio.get<dynamic>(
      path,
      queryParameters: _withoutNullValues(queryParameters),
      options: Options(extra: extras),
    );
    final data = response.data;
    if (data is Map<String, dynamic>) return fromJson(data);
    if (data is Map) return fromJson(Map<String, dynamic>.from(data));
    throw FormatException(
      'TMDB response is not a JSON object: ${data.runtimeType}',
    );
  }

  Map<String, dynamic> _withoutNullValues(Map<String, Object?> values) {
    return <String, dynamic>{
      for (final entry in values.entries)
        if (entry.value != null) entry.key: entry.value,
    };
  }
}

class TmdbRawResponse<T> {
  const TmdbRawResponse({required this.data, required this.response});

  final T data;
  final Response<T> response;
}


class TmdbConfigInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    final config = options.extra[kTmdbConfigExtraKey];
    if (config is! TmdbRequestConfig) {
      handler.next(options);
      return;
    }

    options.baseUrl = config.apiBaseUrl;

    final override = options.extra[kTmdbOverrideAccessTokenKey];
    final accessToken = (override is String && override.trim().isNotEmpty)
        ? override.trim()
        : config.accessToken.trim();
    final apiKey = config.apiKey.trim();
    if (accessToken.isNotEmpty) {
      options.headers['Authorization'] = 'Bearer $accessToken';
    } else if (apiKey.isNotEmpty) {
      options.queryParameters['api_key'] = apiKey;
    }

    final extraLang = options.extra[kTmdbAcceptLanguageExtraKey];
    final queryLang = options.queryParameters['language'];
    final lang = extraLang is String && extraLang.trim().isNotEmpty
        ? extraLang.trim()
        : (queryLang is String && queryLang.trim().isNotEmpty
              ? queryLang.trim()
              : null);
    if (lang != null) {
      options.headers['Accept-Language'] = '$lang,en,*';
    }

    handler.next(options);
  }
}


class TmdbAuthRetryInterceptor extends Interceptor {
  TmdbAuthRetryInterceptor(this._dio);

  final Dio _dio;

  @override
  Future<void> onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) async {
    final options = err.requestOptions;
    final config = options.extra[kTmdbConfigExtraKey];
    final refresh = config is TmdbRequestConfig ? config.onUnauthorized : null;
    final alreadyRetried = options.extra[kTmdbAuthRetriedKey] == true;

    if (err.response?.statusCode != 401 || refresh == null || alreadyRetried) {
      handler.next(err);
      return;
    }

    final newToken = await refresh();
    if (newToken == null || newToken.trim().isEmpty) {
      handler.next(err);
      return;
    }

    options.extra[kTmdbOverrideAccessTokenKey] = newToken.trim();
    options.extra[kTmdbAuthRetriedKey] = true;
    try {
      final response = await _dio.fetch<dynamic>(options);
      handler.resolve(response);
    } on DioException catch (e) {
      handler.next(e);
    }
  }
}
