import 'package:dio/dio.dart';

import '../../../shared/utils/app_logger.dart';
import '../../contracts/tmdb.dart';
import '../../domain/ports/tmdb_gateway.dart';
import 'tmdb_rest_api.dart';

class TmdbApiClient implements TmdbGateway {
  static const _tag = 'Tmdb';

  TmdbApiClient({Dio? dio}) : _api = _buildApi(dio);

  static TmdbRestApi _buildApi(Dio? injected) {
    final dio =
        injected ??
        Dio(
          BaseOptions(
            connectTimeout: const Duration(seconds: 8),
            receiveTimeout: const Duration(seconds: 15),
            headers: {'Accept': 'application/json'},
          ),
        );
    dio.interceptors.add(TmdbConfigInterceptor());
    dio.interceptors.add(TmdbAuthRetryInterceptor(dio));
    return TmdbRestApi(dio);
  }

  final TmdbRestApi _api;

  Map<String, dynamic> _extras(TmdbRequestConfig config) => <String, dynamic>{
    kTmdbConfigExtraKey: config,
  };


  Map<String, dynamic> _extrasWithLang(
    TmdbRequestConfig config,
    String language,
  ) => <String, dynamic>{
    kTmdbConfigExtraKey: config,
    kTmdbAcceptLanguageExtraKey: language,
  };

  @override
  Future<TmdbCreditsRes?> getCredits(
    TmdbRequestConfig config,
    String tmdbId,
    String mediaType, {
    required String language,
  }) async {
    final type = _normalizeMediaType(mediaType);
    if (tmdbId.isEmpty || type == null) return null;
    try {
      return await _api.getCredits(type, tmdbId, language, _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /$type/$tmdbId/credits failed', e);
      return null;
    }
  }

  @override
  Future<TmdbRecommendationsRes?> getRecommendations(
    TmdbRequestConfig config,
    String tmdbId,
    String mediaType, {
    required String language,
  }) async {
    final type = _normalizeMediaType(mediaType);
    if (tmdbId.isEmpty || type == null) return null;
    try {
      return await _api.getRecommendations(
        type,
        tmdbId,
        language,
        '1',
        _extras(config),
      );
    } catch (e) {
      AppLogger.warn(_tag, 'GET /$type/$tmdbId/recommendations failed', e);
      return null;
    }
  }

  @override
  Future<TmdbSeasonDetail?> getSeasonDetail(
    TmdbRequestConfig config,
    String tmdbId,
    int seasonNumber, {
    required String language,
  }) async {
    if (tmdbId.isEmpty || seasonNumber < 0) return null;
    try {
      return await _api.getSeasonDetail(
        tmdbId,
        seasonNumber,
        language,
        _extras(config),
      );
    } catch (e) {
      AppLogger.warn(_tag, 'GET /tv/$tmdbId/season/$seasonNumber failed', e);
      return null;
    }
  }

  @override
  Future<TmdbImageSet?> getImages(
    TmdbRequestConfig config,
    String tmdbId,
    String mediaType, {
    required String language,
  }) async {
    final type = _normalizeMediaType(mediaType);
    if (tmdbId.isEmpty || type == null) return null;
    try {
      return await _api.getImages(
        type,
        tmdbId,
        _includeImageLanguage(language),
        _extrasWithLang(config, language),
      );
    } catch (e) {
      AppLogger.warn(_tag, 'GET /$type/$tmdbId/images failed', e);
      return null;
    }
  }

  @override
  Future<TmdbEnrichedDetail?> getEnrichedDetail(
    TmdbRequestConfig config,
    String tmdbId,
    String mediaType, {
    required String language,
    List<String> appendToResponse = const [
      'videos',
      'credits',
      'recommendations',
      'images',
      'external_ids',
    ],
  }) async {
    final type = _normalizeMediaType(mediaType);
    if (tmdbId.isEmpty || type == null) return null;

    final TmdbEnrichedDetail detail;
    try {
      detail = await _api.getEnrichedDetail(
        type,
        tmdbId,
        language,
        appendToResponse.join(','),
        appendToResponse.contains('images')
            ? _includeImageLanguage(language)
            : null,
        _extras(config),
      );
    } catch (e) {
      AppLogger.warn(_tag, 'GET /$type/$tmdbId failed', e);
      return null;
    }

    final shouldFallbackVideos =
        appendToResponse.contains('videos') &&
        !_isEnglishLanguage(language) &&
        (detail.videos?.results.isEmpty ?? true);
    if (!shouldFallbackVideos) return detail;

    final fallbackVideos = await getVideos(
      config,
      tmdbId,
      type,
      language: 'en-US',
    );
    if (fallbackVideos == null || fallbackVideos.results.isEmpty) {
      return detail;
    }
    return detail.copyWith(videos: fallbackVideos);
  }

  @override
  Future<TmdbVideosRes?> getVideos(
    TmdbRequestConfig config,
    String tmdbId,
    String mediaType, {
    required String language,
  }) async {
    final type = _normalizeMediaType(mediaType);
    if (tmdbId.isEmpty || type == null) return null;
    try {
      return await _api.getVideos(type, tmdbId, language, _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /$type/$tmdbId/videos failed', e);
      return null;
    }
  }

  @override
  Future<TmdbPersonDetail?> getPersonDetail(
    TmdbRequestConfig config,
    int personId, {
    required String language,
  }) async {
    if (personId <= 0) return null;
    try {
      return await _api.getPersonDetail(personId, language, _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /person/$personId failed', e);
      return null;
    }
  }

  @override
  Future<TmdbPersonCreditsRes?> getPersonCredits(
    TmdbRequestConfig config,
    int personId, {
    required String language,
  }) async {
    if (personId <= 0) return null;
    try {
      return await _api.getPersonCredits(personId, language, _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /person/$personId/combined_credits failed', e);
      return null;
    }
  }

  @override
  Future<TmdbMediaDetail?> getMediaDetail(
    TmdbRequestConfig config,
    String tmdbId,
    String mediaType, {
    required String language,
  }) async {
    final type = _normalizeMediaType(mediaType);
    if (tmdbId.isEmpty || type == null) return null;
    try {
      return await _api.getMediaDetail(type, tmdbId, language, _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /$type/$tmdbId failed', e);
      return null;
    }
  }

  @override
  Future<TmdbRecommendationsRes?> getTrending(
    TmdbRequestConfig config, {
    required String language,
    String mediaType = 'all',
    String timeWindow = 'day',
  }) async {
    try {
      return await _api.getTrending(
        mediaType,
        timeWindow,
        language,
        '1',
        _extras(config),
      );
    } catch (e) {
      AppLogger.warn(_tag, 'GET /trending/$mediaType/$timeWindow failed', e);
      return null;
    }
  }

  @override
  Future<TmdbRecommendationsRes?> getPopularMovies(
    TmdbRequestConfig config, {
    required String language,
  }) async {
    try {
      return await _api.getPopularMovies(language, '1', _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /movie/popular failed', e);
      return null;
    }
  }

  @override
  Future<TmdbRecommendationsRes?> getPopularTv(
    TmdbRequestConfig config, {
    required String language,
  }) async {
    try {
      return await _api.getPopularTv(language, '1', _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /tv/popular failed', e);
      return null;
    }
  }

  @override
  Future<TmdbRecommendationsRes?> getAiringTodayTv(
    TmdbRequestConfig config, {
    required String language,
  }) async {
    try {
      return await _api.getAiringTodayTv(language, '1', _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /tv/airing_today failed', e);
      return null;
    }
  }

  @override
  Future<TmdbRecommendationsRes?> getTopRatedMovies(
    TmdbRequestConfig config, {
    required String language,
  }) async {
    try {
      return await _api.getTopRatedMovies(language, '1', _extras(config));
    } catch (e) {
      AppLogger.warn(_tag, 'GET /movie/top_rated failed', e);
      return null;
    }
  }

  @override
  Future<TmdbRecommendationsRes?> getDiscover(
    TmdbRequestConfig config,
    String mediaType, {
    required String language,
    String sortBy = 'popularity.desc',
    String? withGenres,
    String? voteCountGte,
  }) async {
    final type = _normalizeMediaType(mediaType);
    if (type == null) return null;
    try {
      return await _api.getDiscover(
        type,
        language,
        '1',
        sortBy,
        withGenres,
        voteCountGte,
        _extras(config),
      );
    } catch (e) {
      AppLogger.warn(_tag, 'GET /discover/$type failed', e);
      return null;
    }
  }

  @override
  Future<bool> testConnection(
    TmdbRequestConfig config, {
    required String language,
  }) async {
    final accessToken = config.accessToken.trim();
    final apiKey = config.apiKey.trim();
    final authMode = accessToken.isNotEmpty
        ? 'bearer(${_maskSecret(accessToken)})'
        : apiKey.isNotEmpty
        ? 'apiKey(${_maskSecret(apiKey)})'
        : 'none';
    AppLogger.info(
      _tag,
      'testConnection → GET ${config.apiBaseUrl}/configuration '
      'auth=$authMode lang=$language',
    );
    try {
      final res = await _api.getConfiguration(
        _extrasWithLang(config, language),
      );
      final data = res.data;
      final ok =
          data is Map && (data['images'] is Map || data['change_keys'] is List);
      AppLogger.info(
        _tag,
        'testConnection ← ${res.response.statusCode} '
        'ok=$ok (body=${data.runtimeType})',
      );
      return ok;
    } on DioException catch (e) {
      final status = e.response?.statusCode;
      final hint = switch (status) {
        401 => '认证失败：token/api_key 被拒绝，检查认证方式(Bearer/ApiKey)是否与凭据类型匹配',
        403 => '权限不足：凭据有效但无访问权限',
        404 => 'baseUrl 路径错误：检查 ${config.apiBaseUrl}',
        null => '网络层失败（${e.type.name}）：检查 baseUrl 可达性/代理',
        _ => '',
      };
      AppLogger.warn(
        _tag,
        'testConnection ← HTTP $status auth=$authMode '
        'url=${config.apiBaseUrl}/configuration'
        '${hint.isEmpty ? '' : ' — $hint'}',
        e,
      );
      return false;
    } catch (e) {
      AppLogger.warn(_tag, 'testConnection ← 非 Dio 异常 auth=$authMode', e);
      return false;
    }
  }


  static String _maskSecret(String secret) {
    if (secret.length <= 4) return '****';
    return 'len=${secret.length},…${secret.substring(secret.length - 4)}';
  }

  static String? _normalizeMediaType(String mediaType) {
    return switch (mediaType) {
      'movie' || 'tv' => mediaType,
      _ => null,
    };
  }

  static String _includeImageLanguage(String language) {
    final imageLanguages = <String>[_languageCode(language), 'en', 'null'];
    return imageLanguages.where((v) => v.isNotEmpty).toSet().join(',');
  }

  static bool _isEnglishLanguage(String language) {
    return _languageCode(language) == 'en';
  }

  static String _languageCode(String language) {
    return language.trim().split(RegExp('[-_]')).first.toLowerCase();
  }
}
