import 'package:flutter/foundation.dart';

import '../../../shared/utils/app_logger.dart';
import '../../../shared/utils/emby_ticks.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';

const _tag = 'PlaybackResolver';

class PlaybackResolver {
  final EmbyGateway gateway;
  final String _deviceId;
  final String _deviceName;

  PlaybackResolver({
    required this.gateway,
    required this._deviceId,
    this._deviceName = 'iPad',
  });


  Future<PlaybackRequestResult> prepare({
    required AuthedRequestContext ctx,
    required PlaybackRequestPayload payload,
    EmbyRawItem? preloadedItem,
    Map<String, dynamic>? preloadedPlaybackInfo,
  }) async {
    var item = preloadedItem?.Id == payload.itemId
        ? preloadedItem!
        : await gateway.getItemDetail(ctx, payload.itemId);

    if (item.Type == 'Series') {
      preloadedPlaybackInfo = null;
      final seasons = await gateway.getSeriesSeasons(ctx, item.Id);
      if (seasons.isEmpty) {
        throw DomainError(
          ErrorCode.storeNotFound,
          '剧集没有可用季',
          details: {'seriesId': item.Id},
        );
      }
      final episodes = await gateway.getSeriesEpisodes(
        ctx,
        item.Id,
        seasons.first.Id,
      );
      if (episodes.isEmpty) {
        throw DomainError(
          ErrorCode.storeNotFound,
          '剧集没有可用分集',
          details: {'seriesId': item.Id},
        );
      }
      item = episodes.first;
    }

    final resumePositionTicks = _startPositionTicksFor(payload, item);

    final playbackInfo =
        (preloadedPlaybackInfo != null && preloadedPlaybackInfo.isNotEmpty)
        ? preloadedPlaybackInfo
        : await gateway.fetchPlaybackInfo(
            ctx: ctx,
            itemId: item.Id,
            mediaSourceId: payload.mediaSourceId,
            startTimeTicks: resumePositionTicks,
            isPlayback: true,
            deviceProfile: deviceProfile(),
          );

    final mediaSources =
        (playbackInfo['MediaSources'] as List<dynamic>?) ?? <dynamic>[];
    if (mediaSources.isEmpty) {
      final serverErrorCode = playbackInfo['ErrorCode'] as String?;
      final responseKeys = playbackInfo.keys.take(10).toList();
      AppLogger.warn(
        _tag,
        'PlaybackInfo returned empty MediaSources for itemId=${item.Id} '
        'name=${item.Name} serverErrorCode=$serverErrorCode '
        'responseKeys=$responseKeys',
      );
      throw DomainError(
        ErrorCode.storeNotFound,
        serverErrorCode != null
            ? '没有可播放的媒体源（服务端：$serverErrorCode）'
            : '没有可播放的媒体源',
        details: {
          'itemId': item.Id,
          'itemName': item.Name,
          if (serverErrorCode != null) 'serverErrorCode': serverErrorCode,
          if (payload.mediaSourceId != null)
            'requestedMediaSourceId': payload.mediaSourceId,
        },
      );
    }

    Map<String, dynamic> sourceJson;
    if (payload.mediaSourceId != null) {
      sourceJson = mediaSources.cast<Map<String, dynamic>>().firstWhere(
        (s) => s['Id'] == payload.mediaSourceId,
        orElse: () => mediaSources.first as Map<String, dynamic>,
      );
    } else {
      sourceJson = mediaSources.first as Map<String, dynamic>;
    }

    final source = EmbyRawMediaSource.fromJson(sourceJson);
    final playSessionId =
        (playbackInfo['PlaySessionId'] as String?) ??
        (sourceJson['PlaySessionId'] as String?);
    final streamUrl = _resolveStreamUrl(
      ctx: ctx,
      itemId: item.Id,
      sourceJson: sourceJson,
      playSessionId: playSessionId,
    );

    final subtitles = resolveSubtitles(
      source.MediaStreams ?? [],
      baseUrl: ctx.baseUrl,
      token: ctx.token,
      itemId: item.Id,
      mediaSourceId: source.Id,
    );
    final audioTracks = _resolveAudioTracks(streams: source.MediaStreams ?? []);

    final positionTicks = resumePositionTicks;
    final startSeconds = positionTicks / kTicksPerSecond;
    final durationTicks = item.RunTimeTicks;
    final durationSeconds = durationTicks != null
        ? durationTicks / kTicksPerSecond
        : null;

    final playMethod = _resolvePlayMethod(sourceJson);

    AppLogger.debug(
      _tag,
      'prepared item=${item.Name} (${item.Id}) source=${source.Id} '
      'container=${source.Container} method=$playMethod '
      'startSeconds=${startSeconds.toStringAsFixed(1)} '
      'subs=${subtitles.length} audio=${audioTracks.length}',
    );

    return PlaybackRequestResult(
      streamUrl: streamUrl,
      container: _normalizeContainer(source.Container),
      mimeType: _guessMimeType(source.Container),
      directStream: sourceJson['SupportsDirectStream'] as bool? ?? false,
      item: item,
      mediaSource: source,
      startPositionSeconds: startSeconds,
      durationSeconds: durationSeconds,
      subtitles: subtitles,
      defaultSubtitleIndex: source.DefaultSubtitleStreamIndex,
      audioTracks: audioTracks,
      defaultAudioStreamIndex: source.DefaultAudioStreamIndex,
      playSessionId: playSessionId,
      playMethod: playMethod,
    );
  }

  int _startPositionTicksFor(PlaybackRequestPayload payload, EmbyRawItem item) {
    if (payload.playFromStart) return 0;
    final explicitTicks = payload.startPositionTicks;
    if (explicitTicks != null && explicitTicks > 0) return explicitTicks;
    return item.UserData?.PlaybackPositionTicks ?? 0;
  }


  String _resolveStreamUrl({
    required AuthedRequestContext ctx,
    required String itemId,
    required Map<String, dynamic> sourceJson,
    String? playSessionId,
  }) {
    final base = stripTrailingSlash(ctx.baseUrl);
    final mediaSourceId = sourceJson['Id'] as String?;
    final container = _normalizeContainer(sourceJson['Container'] as String?);

    final directStreamUrl = sourceJson['DirectStreamUrl'] as String?;
    if (directStreamUrl != null && directStreamUrl.isNotEmpty) {
      return _buildPlayableUrl(
        base,
        directStreamUrl,
        ctx.token,
        appendTokenToCrossOrigin: false,
      );
    }

    final transcodingUrl = sourceJson['TranscodingUrl'] as String?;
    if (transcodingUrl != null && transcodingUrl.isNotEmpty) {
      return _buildPlayableUrl(
        base,
        transcodingUrl,
        ctx.token,
        appendTokenToCrossOrigin: false,
      );
    }

    final ext = container ?? 'mkv';
    final params = _staticStreamQueryParameters(
      ctx: ctx,
      mediaSourceId: mediaSourceId,
      playSessionId: playSessionId,
    );
    final query = params.entries
        .map((e) => '${e.key}=${Uri.encodeComponent(e.value)}')
        .join('&');
    return '$base/Videos/$itemId/stream.$ext?$query';
  }

  Map<String, String> _staticStreamQueryParameters({
    required AuthedRequestContext ctx,
    String? mediaSourceId,
    String? playSessionId,
  }) {
    return {
      'UserId': ctx.userId,
      'IsPlayback': 'true',
      'MaxStreamingBitrate': '500000000',
      'Static': 'true',
      'api_key': ctx.token,
      'X-Emby-Authorization': EmbyAuthHeader.build(
        deviceId: _deviceId,
        deviceName: _deviceName,
      ),
      'X-Emby-Client': EmbyRequestHeaders.clientName,
      'X-Emby-Client-Version': EmbyRequestHeaders.version,
      'X-Emby-Device-Id': _deviceId,
      'X-Emby-Device-Name': _deviceName,
      'X-Emby-Language': 'zh-cn',
      'X-Emby-Token': ctx.token,
      if (mediaSourceId != null) 'MediaSourceId': mediaSourceId,
      if (playSessionId != null) 'PlaySessionId': playSessionId,
    };
  }


  String _resolvePlayMethod(Map<String, dynamic> sourceJson) {
    final directStreamUrl = sourceJson['DirectStreamUrl'] as String?;
    if (directStreamUrl != null && directStreamUrl.isNotEmpty) {
      return 'DirectStream';
    }
    final transcodingUrl = sourceJson['TranscodingUrl'] as String?;
    if (transcodingUrl != null && transcodingUrl.isNotEmpty) {
      return 'Transcode';
    }
    return 'DirectStream';
  }


  static String _buildPlayableUrl(
    String base,
    String rawUrl,
    String token, {
    bool appendTokenToCrossOrigin = true,
  }) {
    final baseUri = Uri.parse(base);
    final rawUri = Uri.parse(rawUrl);
    final playableUri = rawUri.hasScheme
        ? rawUri
        : Uri.parse('$base${rawUrl.startsWith('/') ? '' : '/'}$rawUrl');
    if (!appendTokenToCrossOrigin && playableUri.origin != baseUri.origin) {
      return playableUri.toString();
    }
    if (playableUri.queryParameters.containsKey('api_key')) {
      return playableUri.toString();
    }

    return playableUri
        .replace(
          queryParameters: {
            ...playableUri.queryParametersAll,
            'api_key': token,
          },
        )
        .toString();
  }


  @visibleForTesting
  static List<EmbySubtitleTrack> resolveSubtitles(
    List<EmbyRawMediaStream> streams, {
    String? baseUrl,
    String? token,
    String? itemId,
    String? mediaSourceId,
  }) {
    final subtitles = <EmbySubtitleTrack>[];

    for (final stream in streams) {
      if (stream.Type != 'Subtitle') continue;
      final isExternal =
          (stream.IsExternal ?? false) ||
          stream.DeliveryMethod == 'External' ||
          (stream.DeliveryUrl != null && stream.DeliveryUrl!.isNotEmpty);
      if (isExternal) {
        if (!isTextSubtitleCodec(stream.Codec)) continue;
        final rawDeliveryUrl = stream.DeliveryUrl;
        if (rawDeliveryUrl == null ||
            rawDeliveryUrl.isEmpty ||
            baseUrl == null ||
            token == null) {
          continue;
        }
        subtitles.add(
          EmbySubtitleTrack(
            index: stream.Index ?? 0,
            title: stream.Title ?? stream.DisplayTitle,
            language: stream.Language,
            isDefault: stream.IsDefault ?? false,
            isForced: stream.IsForced ?? false,
            isExternal: true,
            deliveryUrl: _buildPlayableUrl(
              stripTrailingSlash(baseUrl),
              rawDeliveryUrl,
              token,
            ),
            codec: stream.Codec,
          ),
        );
        continue;
      }
      subtitles.add(
        EmbySubtitleTrack(
          index: stream.Index ?? 0,
          title: stream.Title ?? stream.DisplayTitle,
          language: stream.Language,
          isDefault: stream.IsDefault ?? false,
          isForced: stream.IsForced ?? false,
          codec: stream.Codec,
          deliveryUrl: _embeddedTextSubtitleUrl(
            baseUrl: baseUrl,
            token: token,
            itemId: itemId,
            mediaSourceId: mediaSourceId,
            index: stream.Index,
            codec: stream.Codec,
          ),
        ),
      );
    }
    return subtitles;
  }


  static String? _embeddedTextSubtitleUrl({
    required String? baseUrl,
    required String? token,
    required String? itemId,
    required String? mediaSourceId,
    required int? index,
    required String? codec,
  }) {
    if (!isTextSubtitleCodec(codec)) return null;
    if (baseUrl == null ||
        token == null ||
        itemId == null ||
        mediaSourceId == null ||
        index == null) {
      return null;
    }
    final extension = _subtitleDeliveryExtension(codec);
    final path =
        '/Videos/$itemId/$mediaSourceId/Subtitles/$index/0/Stream.$extension';
    return _buildPlayableUrl(stripTrailingSlash(baseUrl), path, token);
  }


  static String _subtitleDeliveryExtension(String? codec) {
    final normalized = (codec ?? 'srt').toLowerCase();
    return normalized == 'webvtt' ? 'vtt' : normalized;
  }


  static bool isTextSubtitleCodec(String? codec) {
    if (codec == null) return false;
    const textCodecs = {'srt', 'subrip', 'vtt', 'webvtt', 'ass', 'ssa'};
    return textCodecs.contains(codec.toLowerCase());
  }

  List<EmbyAudioTrack> _resolveAudioTracks({
    required List<EmbyRawMediaStream> streams,
  }) {
    final tracks = <EmbyAudioTrack>[];
    for (final stream in streams) {
      if (stream.Type != 'Audio') continue;
      tracks.add(
        EmbyAudioTrack(
          index: stream.Index ?? 0,
          title: stream.Title,
          language: stream.Language,
          codec: stream.Codec,
          channels: stream.Channels,
          displayTitle: stream.DisplayTitle,
          isDefault: stream.IsDefault ?? false,
        ),
      );
    }
    return tracks;
  }


  static Map<String, dynamic> deviceProfile() {
    return {
      'MaxStaticBitrate': 200000000,
      'MaxStreamingBitrate': 200000000,
      'MusicStreamingTranscodingBitrate': 200000000,
      'DirectPlayProfiles': [
        {'Type': 'Video'},
        {'Type': 'Audio'},
      ],
      'TranscodingProfiles': [
        {
          'Container': 'ts',
          'Type': 'Video',
          'AudioCodec': 'aac,mp3,wav,ac3,eac3,flac,opus',
          'VideoCodec': 'hevc,h264,h265,mpeg4',
          'Context': 'Streaming',
          'Protocol': 'hls',
          'MaxAudioChannels': '6',
          'MinSegments': '1',
          'BreakOnNonKeyFrames': true,
          'ManifestSubtitles': 'vtt',
        },
      ],
      'ContainerProfiles': <Map<String, dynamic>>[],
      'SubtitleProfiles': [
        {'Format': 'vtt', 'Method': 'External'},
        {'Format': 'ass', 'Method': 'External'},
        {'Format': 'ssa', 'Method': 'External'},
        {'Format': 'srt', 'Method': 'External'},
        {'Format': 'sub', 'Method': 'External'},
        {'Format': 'subrip', 'Method': 'External'},
        {'Format': 'smi', 'Method': 'External'},
        {'Format': 'ttml', 'Method': 'External'},
        {'Format': 'webvtt', 'Method': 'External'},
        {'Format': 'dvdsub', 'Method': 'External'},
        {'Format': 'sup', 'Method': 'External'},
        {'Format': 'dvdsub', 'Method': 'Embed'},
        {'Format': 'vobsub', 'Method': 'Embed'},
        {'Format': 'vtt', 'Method': 'Embed'},
        {'Format': 'ass', 'Method': 'Embed'},
        {'Format': 'ssa', 'Method': 'Embed'},
        {'Format': 'srt', 'Method': 'Embed'},
        {'Format': 'sub', 'Method': 'Embed'},
        {'Format': 'pgssub', 'Method': 'Embed'},
        {'Format': 'pgs', 'Method': 'Embed'},
        {'Format': 'subrip', 'Method': 'Embed'},
        {'Format': 'smi', 'Method': 'Embed'},
        {'Format': 'ttml', 'Method': 'Embed'},
        {'Format': 'webvtt', 'Method': 'Embed'},
        {'Format': 'mov_text', 'Method': 'Embed'},
        {'Format': 'dvb_teletext', 'Method': 'Embed'},
        {'Format': 'dvb_subtitle', 'Method': 'Embed'},
        {'Format': 'dvbsub', 'Method': 'Embed'},
        {'Format': 'idx', 'Method': 'Embed'},
        {'Format': 'sup', 'Method': 'Embed'},
        {'Format': 'vtt', 'Method': 'Hls'},
      ],
    };
  }


  String? _normalizeContainer(String? raw) {
    if (raw == null) return null;
    final container = raw.split(',').first.trim().toLowerCase();
    if (container.isEmpty) return null;
    if (container == 'matroska') return 'mkv';
    return container;
  }

  String? _guessMimeType(String? container) {
    if (container == null) return null;
    return switch (_normalizeContainer(container)) {
      'mp4' || 'm4v' => 'video/mp4',
      'mkv' => 'video/x-matroska',
      'webm' => 'video/webm',
      'avi' => 'video/x-msvideo',
      'mov' => 'video/quicktime',
      'ts' || 'm2ts' || 'mpegts' => 'video/mp2t',
      _ => null,
    };
  }
}
