import 'dart:async';

import 'package:dio/dio.dart';

import '../../../shared/utils/app_logger.dart';
import '../../../shared/utils/url_utils.dart';
import '../../contracts/contracts.dart';
import '../../domain/errors.dart';
import '../../domain/ports/emby_gateway.dart';
import 'emby_headers.dart';

class EmbyApiClient implements EmbyGateway {
  static const _detailFields =
      'BasicSyncInfo,RunTimeTicks,CommunityRating,ProductionYear,ChildCount,'
      'Container,Overview,OfficialRating,Genres,Status,People,Studios,'
      'ProviderIds,AlternateMediaSources,UserData,UserDataLastPlayedDate';
  static const _seasonFields =
      'BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ChildCount,'
      'ProductionYear,IndexNumber,Status,EndDate,Overview,'
      'OfficialRating,Genres,CommunityRating,ProviderIds';
  static const _episodeFields =
      'BasicSyncInfo,RunTimeTicks,CommunityRating,Container,'
      'People,PrimaryImageAspectRatio,DateCreated,Genres,'
      'MediaStreams,MediaSources,UserData,UserDataLastPlayedDate,Path,ParentId,'
      'Overview,Studios,ProviderIds,AlternateMediaSources,Chapters';
  static const _similarFields =
      'BasicSyncInfo,CommunityRating,ProductionYear,ChildCount,Container,'
      'Overview,OfficialRating,Genres,People,Studios,ProviderIds';

  final Dio _dio;
  final String _deviceId;
  final String _deviceName;

  EmbyApiClient({
    Dio? dio,
    required this._deviceId,
    this._deviceName = 'iPad',
  }) : _dio = dio ?? _createDio();

  static Dio _createDio() => Dio(
    BaseOptions(
      connectTimeout: const Duration(seconds: 15),
      receiveTimeout: const Duration(seconds: 30),
    ),
  );


  Future<dynamic> _get(
    String url, {
    String? token,
    String? userId,
    Map<String, dynamic>? query,
    CancellationToken? cancelToken,
  }) async {
    return _request(
      method: 'GET',
      url: url,
      token: token,
      userId: userId,
      query: query,
      cancelToken: cancelToken,
    );
  }

  Future<dynamic> _post(
    String url, {
    String? token,
    String? userId,
    Map<String, dynamic>? query,
    Object? body,
    CancellationToken? cancelToken,
    bool quiet = false,
  }) async {
    return _request(
      method: 'POST',
      url: url,
      token: token,
      userId: userId,
      query: query,
      body: body,
      cancelToken: cancelToken,
      quiet: quiet,
    );
  }

  Future<dynamic> _delete(
    String url, {
    String? token,
    String? userId,
    Map<String, dynamic>? query,
    CancellationToken? cancelToken,
  }) async {
    return _request(
      method: 'DELETE',
      url: url,
      token: token,
      userId: userId,
      query: query,
      cancelToken: cancelToken,
    );
  }

  static const _tag = 'EmbyApi';

  static String _extractPath(String url) {
    final uri = Uri.tryParse(url);
    if (uri == null) return url;
    return uri.path;
  }

  Future<dynamic> _request({
    required String method,
    required String url,
    String? token,
    String? userId,
    Map<String, dynamic>? query,
    Object? body,
    CancellationToken? cancelToken,
    bool quiet = false,
  }) async {
    final path = _extractPath(url);
    final queryLog = query != null && query.isNotEmpty
        ? ' query=${query.keys.join(',')}'
        : '';
    if (!quiet) {
      AppLogger.debug(_tag, '--> $method $path$queryLog');
    }
    final stopwatch = Stopwatch()..start();

    final headers = EmbyRequestHeaders.json(
      deviceId: _deviceId,
      deviceName: _deviceName,
      token: token,
      userId: userId,
      hasBody: body != null,
    );

    CancelToken? dioCancelToken;
    if (cancelToken != null) {
      dioCancelToken = CancelToken();
      cancelToken.whenCancelled.then((_) {
        if (!dioCancelToken!.isCancelled) {
          dioCancelToken.cancel(cancelToken.reason ?? 'cancelled');
        }
      });
    }

    try {
      final response = await _dio.request<dynamic>(
        url,
        queryParameters: query,
        data: body,
        cancelToken: dioCancelToken,
        options: Options(
          method: method,
          headers: headers,
          responseType: ResponseType.json,
          validateStatus: (s) => true,
        ),
      );

      stopwatch.stop();
      final status = response.statusCode ?? 0;
      final ms = stopwatch.elapsedMilliseconds;
      if (!quiet) {
        AppLogger.debug(_tag, '<-- $status $method $path (${ms}ms)');
      }

      if (status == 401) {
        throw DomainError(
          ErrorCode.authInvalidCredentials,
          '用户名或密码错误',
          details: {'status': status},
        );
      }
      if (status == 403) {
        throw DomainError(
          ErrorCode.authForbidden,
          '权限不足',
          details: {'status': status},
        );
      }
      if (status >= 400) {
        AppLogger.warn(_tag, '<-- $status $method $path (${ms}ms)');
        throw DomainError(
          ErrorCode.serverHttpError,
          'HTTP 请求失败（$status）',
          retryable: status >= 500,
          details: {'status': status},
        );
      }
      return response.data;
    } on DomainError {
      rethrow;
    } on DioException catch (e) {
      stopwatch.stop();
      final ms = stopwatch.elapsedMilliseconds;
      if (CancelToken.isCancel(e)) {
        AppLogger.debug(_tag, '<-- CANCELLED $method $path (${ms}ms)');
        throw CancelledException(e.message);
      }
      if (e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.receiveTimeout ||
          e.type == DioExceptionType.sendTimeout) {
        AppLogger.warn(_tag, '<-- TIMEOUT $method $path (${ms}ms)');
        throw DomainError(
          ErrorCode.serverTimeout,
          '请求超时',
          retryable: true,
          details: {'message': e.message},
        );
      }
      AppLogger.warn(_tag, '<-- NETWORK_ERROR $method $path (${ms}ms)', e);
      throw DomainError(
        ErrorCode.serverUnreachable,
        '无法连接服务器',
        retryable: true,
        details: {'message': e.message},
      );
    } catch (e) {
      stopwatch.stop();
      AppLogger.warn(
        _tag,
        '<-- ERROR $method $path (${stopwatch.elapsedMilliseconds}ms)',
        e,
      );
      throw DomainError(
        ErrorCode.internalError,
        '内部错误：$e',
        details: {'message': e.toString()},
      );
    }
  }

  @override
  Future<ProbePublicInfoRes> probePublicInfo(String baseUrl) async {
    final url = '${stripTrailingSlash(baseUrl)}/System/Info/Public';
    final json = await _get(url);
    return ProbePublicInfoRes(
      serverName: (json['ServerName'] ?? '') as String,
      version: (json['Version'] ?? '') as String,
      productName: (json['ProductName'] ?? 'Emby Server') as String,
    );
  }

  @override
  Future<EmbyAuthResult> authenticateByName({
    required String baseUrl,
    required String username,
    required String password,
  }) async {
    final url = '${stripTrailingSlash(baseUrl)}/Users/AuthenticateByName';
    final body = {'Username': username, 'Pw': password, 'Password': password};
    final json = await _post(url, body: body);
    final data = json as Map<String, dynamic>;
    final user = data['User'] as Map<String, dynamic>?;
    if (user == null) {
      throw DomainError(ErrorCode.authInvalidCredentials, '认证响应格式错误');
    }
    return EmbyAuthResult(
      accessToken: data['AccessToken'] as String,
      userId: user['Id'] as String,
      userName: user['Name'] as String,
    );
  }

  @override
  Future<void> changePassword({
    required AuthedRequestContext ctx,
    required String currentPassword,
    required String newPassword,
  }) async {
    final url =
        '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Password';
    await _post(
      url,
      token: ctx.token,
      userId: ctx.userId,
      body: {'CurrentPw': currentPassword, 'NewPw': newPassword},
    );
  }

  @override
  Future<EmbyRawLibraries> getUserViews(AuthedRequestContext ctx) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Views';
    final json = await _get(url, token: ctx.token, userId: ctx.userId);
    return EmbyRawLibraries.fromJson(json);
  }

  @override
  Future<LibraryLatestRes> getLatestItems(
    AuthedRequestContext ctx,
    String parentId, {
    int? limit,
    CancellationToken? cancelToken,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Items';
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'ParentId': parentId,
        'Fields':
            'BasicSyncInfo,CollectionType,PrimaryImageAspectRatio,UserData,CommunityRating,ProviderIds,ProductionYear,ChildCount,Container,CanDelete',
        'Recursive': 'true',
        'IncludeItemTypes': 'Series,Movie,Video,MusicVideo,MusicAlbum',
        'Limit': '${limit ?? 30}',
        'EnableImageTypes': 'Primary,Backdrop,Thumb',
        'SortBy': 'DateLastContentAdded,DateCreated,SortName',
        'SortOrder': 'Descending',
        'StartIndex': '0',
      },
      cancelToken: cancelToken,
    );
    final totalCount = (json['TotalRecordCount'] as num?)?.toInt() ?? 0;
    return LibraryLatestRes(
      parentId: parentId,
      items: _parseItems(json),
      totalCount: totalCount,
    );
  }

  @override
  Future<LibraryResumeRes> getResumeItems(
    AuthedRequestContext ctx, {
    int? limit,
  }) async {
    final url =
        '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Items/Resume';
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'Fields':
            'BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,ProviderIds,UserDataLastPlayedDate',
        'Recursive': 'true',
        'MediaTypes': 'Video',
        'Limit': '${limit ?? 20}',
        'ImageTypeLimit': '1',
        'EnableImageTypes': 'Primary,Backdrop,Thumb',
      },
    );
    return LibraryResumeRes(Items: _parseItems(json));
  }


  static List<EmbyRawItem> _parseItems(dynamic json) {
    final items = json is List<dynamic>
        ? json
        : (json['Items'] as List<dynamic>?) ?? <dynamic>[];
    return items
        .map((e) => EmbyRawItem.fromJson(e as Map<String, dynamic>))
        .toList();
  }

  @override
  Future<EmbyRawItem> getItemDetail(
    AuthedRequestContext ctx,
    String itemId,
  ) async {
    final url =
        '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Items/$itemId';
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'Fields': _detailFields,
        'EnableImageTypes': 'Primary,Backdrop,Thumb,Logo',
      },
    );
    return EmbyRawItem.fromJson(json);
  }

  @override
  Future<void> reportPlaybackStarted({
    required AuthedRequestContext ctx,
    required PlaybackReportPayload payload,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Sessions/Playing';
    await _post(
      url,
      token: ctx.token,
      userId: ctx.userId,
      body: payload.toJson(includeNowPlayingQueue: true),
    );
  }

  @override
  Future<void> reportPlaybackProgress({
    required AuthedRequestContext ctx,
    required PlaybackReportPayload payload,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Sessions/Playing/Progress';
    await _post(
      url,
      token: ctx.token,
      userId: ctx.userId,
      body: payload.toJson(),
      quiet: true,
    );
  }

  @override
  Future<void> reportPlaybackStopped({
    required AuthedRequestContext ctx,
    required PlaybackReportPayload payload,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Sessions/Playing/Stopped';
    await _post(
      url,
      token: ctx.token,
      userId: ctx.userId,
      body: payload.toJson(),
    );
  }

  @override
  Future<List<EmbyRawItem>> getSeriesNextUp(
    AuthedRequestContext ctx,
    String seriesId, {
    int? limit,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Shows/NextUp';
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'UserId': ctx.userId,
        'SeriesId': seriesId,
        'Limit': '${limit ?? 1}',
        'Fields':
            'BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,DateCreated,Genres,Path,ParentId,Overview',
      },
    );
    return _parseItems(json);
  }

  @override
  Future<List<EmbyRawSeason>> getSeriesSeasons(
    AuthedRequestContext ctx,
    String seriesId, {
    CancellationToken? cancelToken,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Shows/$seriesId/Seasons';
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'UserId': ctx.userId,
        'EnableTotalRecordCount': 'false',
        'Fields': _seasonFields,
      },
      cancelToken: cancelToken,
    );
    final items = (json['Items'] as List<dynamic>?) ?? <dynamic>[];
    return items
        .map((e) => EmbyRawSeason.fromJson(e as Map<String, dynamic>))
        .toList();
  }

  @override
  Future<List<EmbyRawItem>> getSeriesEpisodes(
    AuthedRequestContext ctx,
    String seriesId,
    String seasonId, {
    CancellationToken? cancelToken,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Shows/$seriesId/Episodes';
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'UserId': ctx.userId,
        'EnableTotalRecordCount': 'false',
        'Fields': _episodeFields,
        'EnableImageTypes': 'Primary,Backdrop,Thumb',
        'SeasonId': seasonId,
      },
      cancelToken: cancelToken,
    );
    return _parseItems(json);
  }

  @override
  Future<bool> setFavoriteStatus({
    required AuthedRequestContext ctx,
    required String itemId,
    required bool isFavorite,
  }) async {
    final url =
        '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/FavoriteItems/$itemId';
    if (isFavorite) {
      await _post(url, token: ctx.token, userId: ctx.userId);
    } else {
      await _delete(url, token: ctx.token, userId: ctx.userId);
    }
    return isFavorite;
  }

  @override
  Future<bool> setPlayedStatus({
    required AuthedRequestContext ctx,
    required String itemId,
    required bool played,
  }) async {
    final url =
        '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/PlayedItems/$itemId';
    if (played) {
      await _post(url, token: ctx.token, userId: ctx.userId);
    } else {
      await _delete(url, token: ctx.token, userId: ctx.userId);
    }
    return played;
  }

  @override
  Future<bool> hideFromResume({
    required AuthedRequestContext ctx,
    required String itemId,
    required bool hide,
  }) async {
    final url =
        '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Items/$itemId/HideFromResume';
    await _post(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {'Hide': hide ? 'true' : 'false'},
    );
    return hide;
  }

  @override
  Future<List<EmbyRawItem>> getSimilarItems(
    AuthedRequestContext ctx,
    String itemId, {
    int? limit,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Items/$itemId/Similar';
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'Fields': _similarFields,
        'Limit': '${limit ?? 10}',
        'ImageTypes': 'Logo',
        'SortBy': 'ProductionYear,CommunityRating,PlayCount',
        'Recursive': 'true',
        'EnableImageTypes': 'Primary,Backdrop,Thumb,Logo',
        'SortOrder': 'Descending',
        'IncludeItemTypes': 'Series,Movie,Video',
        'ImageTypeLimit': '1',
      },
    );
    return _parseItems(json);
  }

  @override
  Future<List<EmbyRawItem>> getAdditionalParts(
    AuthedRequestContext ctx,
    String itemId,
  ) async {
    final url =
        '${stripTrailingSlash(ctx.baseUrl)}/Videos/$itemId/AdditionalParts';
    final json = await _get(url, token: ctx.token, userId: ctx.userId);
    return _parseItems(json);
  }

  @override
  Future<List<EmbyRawItem>> getSpecialFeatures(
    AuthedRequestContext ctx,
    String itemId,
  ) async {
    final url =
        '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Items/$itemId/SpecialFeatures';
    final json = await _get(url, token: ctx.token, userId: ctx.userId);
    return _parseItems(json);
  }

  @override
  Future<List<EmbyRawItem>> getLibraryItems({
    required AuthedRequestContext ctx,
    String? parentId,
    String? includeItemTypes,
    List<String>? genreIds,
    String? searchTerm,
    String? anyProviderIdEquals,
    List<String>? personIds,
    String? fields,
    String? enableImageTypes,
    bool? groupProgramsBySeries,
    int? imageTypeLimit,
    int? startIndex,
    int? limit,
    bool? recursive,
    String? sortBy,
    SortOrder? sortOrder,
    String? filters,
    CancellationToken? cancelToken,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Items';
    final query = <String, dynamic>{
      if (parentId != null) 'ParentId': parentId,
      if (includeItemTypes != null) 'IncludeItemTypes': includeItemTypes,
      if (genreIds != null && genreIds.isNotEmpty)
        'GenreIds': genreIds.join(','),
      if (searchTerm != null) 'SearchTerm': searchTerm,
      if (anyProviderIdEquals != null)
        'AnyProviderIdEquals': anyProviderIdEquals,
      if (personIds != null && personIds.isNotEmpty)
        'PersonIds': personIds.join(','),
      if (filters != null) 'Filters': filters,
      'Fields':
          fields ??
          'BasicSyncInfo,CollectionType,PrimaryImageAspectRatio,UserData,CommunityRating,ProviderIds,ProductionYear,ChildCount,Container,CanDelete',
      'EnableImageTypes': enableImageTypes ?? 'Primary,Backdrop,Thumb',
      if (groupProgramsBySeries != null)
        'GroupProgramsBySeries': groupProgramsBySeries ? 'true' : 'false',
      if (imageTypeLimit != null) 'ImageTypeLimit': '$imageTypeLimit',
      'StartIndex': '${startIndex ?? 0}',
      'Limit': '${limit ?? 50}',
      'Recursive': (recursive ?? true) ? 'true' : 'false',
      'SortBy': sortBy ?? 'SortName',
      'SortOrder': sortOrder?.value ?? 'Ascending',
    };
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: query,
      cancelToken: cancelToken,
    );
    return _parseItems(json);
  }

  @override
  Future<List<EmbyRawItem>> getFavoriteItems({
    required AuthedRequestContext ctx,
    required String includeItemTypes,
    required String sortBy,
    SortOrder? sortOrder,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Users/${ctx.userId}/Items';
    final json = await _get(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'SortOrder': sortOrder?.value ?? 'Ascending',
        'Filters': 'IsFavorite',
        'Recursive': 'true',
        'Fields':
            'BasicSyncInfo,CommunityRating,ProductionYear,ChildCount,Container,'
            'Overview,UserDataLastPlayedDate,OfficialRating,Genres,Status,'
            'People,Studios,ProviderIds',
        'EnableImageTypes': 'Primary,Backdrop,Thumb,Logo',
        'CollapseBoxSetItems': 'false',
        'IncludeItemTypes': includeItemTypes,
        'SortBy': sortBy,
      },
    );
    return _parseItems(json);
  }

  @override
  Future<ItemCountsRes> getItemCounts(AuthedRequestContext ctx) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Items/Counts';
    final json = await _get(url, token: ctx.token, userId: ctx.userId);
    return ItemCountsRes.fromJson(json as Map<String, dynamic>);
  }

  @override
  Future<Map<String, dynamic>> fetchPlaybackInfo({
    required AuthedRequestContext ctx,
    required String itemId,
    String? mediaSourceId,
    int startTimeTicks = 0,
    bool isPlayback = false,
    required Map<String, dynamic> deviceProfile,
  }) async {
    final url = '${stripTrailingSlash(ctx.baseUrl)}/Items/$itemId/PlaybackInfo';
    final json = await _post(
      url,
      token: ctx.token,
      userId: ctx.userId,
      query: {
        'AutoOpenLiveStream': 'false',
        'reqformat': 'json',
        'StartTimeTicks': '$startTimeTicks',
        'UserId': ctx.userId,
        'MaxStreamingBitrate': '200000000',
        'IsPlayback': isPlayback ? 'true' : 'false',
        if (mediaSourceId != null) 'MediaSourceId': mediaSourceId,
      },
      body: {'DeviceProfile': deviceProfile},
    );
    return (json as Map<String, dynamic>);
  }
}
