import '../media/media_file_info.dart';
import '../media/media_source_info.dart';
import '../media/media_version.dart';
import '../models/plex/plex_video_playback_data.dart';
import '../utils/app_logger.dart';
import '../utils/json_utils.dart';
import '../utils/plex_url_helper.dart';
import 'file_info_parser.dart';
import 'plex_mappers.dart';

const _streamReader = PlexFileInfoStreamReader();

void _logMalformedStream(Object error, StackTrace stackTrace, Map<String, dynamic> _) {
  appLogger.w('Skipping malformed Plex stream metadata', error: error, stackTrace: stackTrace);
}

List<Map> _mapList(Object? raw) {
  final values = flexibleList(raw);
  if (values == null || values.isEmpty) return const [];
  return [
    for (final value in values)
      if (value is Map) value,
  ];
}

int _firstPlayablePartIndex(MediaVersion version) {
  final parts = version.parts;
  if (parts.isEmpty) return 0;
  final playable = parts.indexWhere((part) => part.isPlayable);
  return playable >= 0 ? playable : 0;
}

void _logPartSelection(
  List<Map> mediaList,
  List<MediaVersion> versions,
  int selectedMediaIndex,
  int selectedPartIndex,
) {
  final candidateCount = mediaList.fold<int>(0, (count, media) => count + _mapList(media['Part']).length);
  if (candidateCount <= 1) return;

  final entries = <String>[];
  for (var mediaIndex = 0; mediaIndex < mediaList.length; mediaIndex++) {
    final partList = _mapList(mediaList[mediaIndex]['Part']);
    for (var partIndex = 0; partIndex < partList.length; partIndex++) {
      final part = partList[partIndex];
      final versionPart = mediaIndex < versions.length && partIndex < versions[mediaIndex].parts.length
          ? versions[mediaIndex].parts[partIndex]
          : null;
      final selected = mediaIndex == selectedMediaIndex && partIndex == selectedPartIndex ? ' selected' : '';
      entries.add(
        'Media[$mediaIndex].Part[$partIndex] '
        'id=${part['id']} key=${part['key']} '
        'exists=${versionPart?.exists} accessible=${versionPart?.accessible} '
        'playable=${versionPart?.isPlayable}$selected',
      );
    }
  }

  appLogger.d('Plex playback part selection: ${entries.join('; ')}');
}

PlexVideoPlaybackData parsePlexVideoPlaybackDataFromJson(
  Map<String, dynamic>? metadataJson, {
  required String baseUrl,
  required String? token,
  int mediaIndex = 0,
  String? selectedMediaSourceId,
  String? preferredVersionSignature,
  void Function(int requestedIndex, int fallbackIndex)? onVersionFallback,
}) {
  String? videoUrl;
  MediaSourceInfo? mediaInfo;
  List<MediaVersion> availableVersions = [];
  var selectedMediaIndex = 0;
  var selectedPartIndex = 0;
  final markers = plexMarkersFromCacheJson(metadataJson);

  if (metadataJson != null) {
    final mediaList = _mapList(metadataJson['Media']);
    if (mediaList.isNotEmpty) {
      availableVersions = mediaList
          .map((media) => PlexMappers.mediaVersionFromJson(Map<String, dynamic>.from(media)))
          .toList();

      // Re-resolve version evidence against this (authoritative) Media list:
      // stable id first, then signature. The positional index is the last
      // resort — and all an explicit user pick carries besides its id, so a
      // saved-preference signature can never override one.
      final requestedSourceId = selectedMediaSourceId?.trim();
      var resolvedBySourceId = false;
      if (requestedSourceId != null && requestedSourceId.isNotEmpty) {
        final byId = availableVersions.indexWhere((v) => v.id == requestedSourceId);
        if (byId >= 0) {
          mediaIndex = byId;
          resolvedBySourceId = true;
        }
      }
      if (!resolvedBySourceId && preferredVersionSignature != null && preferredVersionSignature.isNotEmpty) {
        final bySignature = MediaVersion.findMatchingIndex(availableVersions, {preferredVersionSignature});
        if (bySignature != null) mediaIndex = bySignature;
      }

      if (mediaIndex < 0 || mediaIndex >= mediaList.length) {
        mediaIndex = 0;
      }

      if (!availableVersions[mediaIndex].isPlayable) {
        final fallback = availableVersions.indexWhere((v) => v.isPlayable);
        if (fallback >= 0) {
          onVersionFallback?.call(mediaIndex, fallback);
          mediaIndex = fallback;
        }
      }

      selectedMediaIndex = mediaIndex;
      final media = mediaList[mediaIndex];
      final partList = _mapList(media['Part']);
      if (partList.isNotEmpty) {
        selectedPartIndex = _firstPlayablePartIndex(availableVersions[mediaIndex]);
        if (selectedPartIndex < 0 || selectedPartIndex >= partList.length) selectedPartIndex = 0;
        _logPartSelection(mediaList, availableVersions, selectedMediaIndex, selectedPartIndex);
        final part = partList[selectedPartIndex];
        final partKey = part['key']?.toString();

        if (partKey != null) {
          videoUrl = '$baseUrl$partKey'.withPlexToken(token);

          final streams = walkStreams(flexibleList(part['Stream']), _streamReader, onMalformed: _logMalformedStream);
          final chapters = plexChaptersFromCacheJson(metadataJson);

          mediaInfo = MediaSourceInfo(
            videoUrl: videoUrl,
            audioTracks: streams.audioTracks,
            subtitleTracks: streams.subtitleTracks,
            chapters: chapters,
            partId: flexibleInt(part['id']),
            mediaSourceId: availableVersions[mediaIndex].id,
            displayCriteria: PlexMappers.displayCriteriaFromJson(Map<String, dynamic>.from(media), streams.videoStream),
            videoAspectRatio: flexibleDouble(media['aspectRatio']),
          );
        }
      }
    }
  }

  return PlexVideoPlaybackData(
    videoUrl: videoUrl,
    mediaInfo: mediaInfo,
    availableVersions: availableVersions,
    markers: markers,
    selectedMediaIndex: selectedMediaIndex,
    selectedPartIndex: selectedPartIndex,
  );
}

MediaFileInfo? parsePlexFileInfoFromJson(Map<String, dynamic>? metadataJson) {
  final mediaList = _mapList(metadataJson?['Media']);
  if (mediaList.isNotEmpty) {
    final media = mediaList.first;
    final partList = _mapList(media['Part']);
    final version = PlexMappers.mediaVersionFromJson(Map<String, dynamic>.from(media));
    final partIndex = partList.isEmpty ? 0 : _firstPlayablePartIndex(version).clamp(0, partList.length - 1).toInt();
    final part = partList.isNotEmpty ? partList[partIndex] : null;

    // One pass over the streams array, capturing both the raw video / audio
    // map pointers (for fields the parsed track classes don't carry —
    // colorSpace, bitDepth, …) and the parsed track lists.
    final parsedTracks = walkStreams(flexibleList(part?['Stream']), _streamReader, onMalformed: _logMalformedStream);
    final videoStream = parsedTracks.videoStream;
    final audioStream = parsedTracks.audioStream;

    return MediaFileInfo(
      // Media level properties
      container: media['container'] as String?,
      videoCodec: media['videoCodec'] as String?,
      videoResolution: media['videoResolution'] as String?,
      videoFrameRate: media['videoFrameRate'] as String?,
      videoProfile: media['videoProfile'] as String?,
      width: flexibleInt(media['width']),
      height: flexibleInt(media['height']),
      aspectRatio: flexibleDouble(media['aspectRatio']),
      bitrate: flexibleInt(media['bitrate']),
      duration: flexibleInt(media['duration']),
      audioCodec: media['audioCodec'] as String?,
      audioProfile: media['audioProfile'] as String?,
      audioChannels: flexibleInt(media['audioChannels']),
      optimizedForStreaming: flexibleBool(media['optimizedForStreaming']),
      has64bitOffsets: flexibleBool(media['has64bitOffsets']),
      // Part level properties (file)
      filePath: part?['file'] as String?,
      fileSize: flexibleInt(part?['size']),
      // Video stream details
      colorSpace: videoStream?['colorSpace'] as String?,
      colorRange: videoStream?['colorRange'] as String?,
      colorPrimaries: videoStream?['colorPrimaries'] as String?,
      chromaSubsampling: videoStream?['chromaSubsampling'] as String?,
      frameRate: flexibleDouble(videoStream?['frameRate']),
      bitDepth: flexibleInt(videoStream?['bitDepth']),
      videoBitrate: flexibleInt(videoStream?['bitrate']),
      // Audio stream details
      audioChannelLayout: audioStream?['audioChannelLayout'] as String?,
      // All audio and subtitle tracks
      audioTracks: parsedTracks.audioTracks,
      subtitleTracks: parsedTracks.subtitleTracks,
    );
  }

  return null;
}
