import 'dart:async';
import 'dart:io';
import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart' as p;
import 'package:spotiflac_android/services/conversion_library_service.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:path_provider/path_provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:share_plus/share_plus.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/providers/playback_provider.dart';
import 'package:spotiflac_android/providers/music_player_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/services/ffmpeg_service.dart';
import 'package:spotiflac_android/services/replaygain_service.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/audio_conversion_utils.dart';
import 'package:spotiflac_android/utils/audio_format_utils.dart';
import 'package:spotiflac_android/utils/artist_utils.dart';
import 'package:spotiflac_android/utils/cover_art_utils.dart';
import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
import 'package:spotiflac_android/utils/mime_utils.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';

import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/utils/user_facing_error.dart';
import 'package:spotiflac_android/utils/int_utils.dart';
import 'package:spotiflac_android/utils/isrc_utils.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/utils/re_enrich_release_policy.dart';
import 'package:spotiflac_android/utils/saf_display_path.dart';
import 'package:spotiflac_android/theme/cover_palette.dart' show HeaderPalette;
import 'package:spotiflac_android/widgets/album_detail_header.dart'
    show HeaderMetaRow, HeaderMetaItem;
import 'package:spotiflac_android/widgets/audio_analysis_widget.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/widgets/batch_convert_sheet.dart';
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
import 'package:spotiflac_android/widgets/open_on_platform_sheet.dart';
import 'package:spotiflac_android/widgets/metadata_barcode.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/constants/music_services.dart';
import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart';
import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
import 'package:spotiflac_android/screens/local_album_screen.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';

part 'track_metadata_screen_cover.dart';
part 'track_metadata_screen_display.dart';
part 'track_metadata_screen_menu.dart';

part 'track_metadata_edit_sheet.dart';
part 'track_metadata_cards.dart';
part 'track_metadata_lyrics.dart';
part 'track_metadata_convert.dart';
part 'track_metadata_actions.dart';

final _log = AppLogger('TrackMetadata');

class _EmbeddedCoverPreviewCacheEntry {
  final String previewPath;
  final String? sourceValidationToken;
  final ({int width, int height})? dimensions;

  const _EmbeddedCoverPreviewCacheEntry({
    required this.previewPath,
    this.sourceValidationToken,
    this.dimensions,
  });
}

class TrackMetadataScreen extends ConsumerStatefulWidget {
  final DownloadHistoryItem? item;
  final LocalLibraryItem? localItem;
  final List<DownloadHistoryItem>? historyNavigationItems;
  final List<LocalLibraryItem>? localNavigationItems;
  final int? navigationIndex;
  final String? coverHeroTag;

  const TrackMetadataScreen({
    super.key,
    this.item,
    this.localItem,
    this.historyNavigationItems,
    this.localNavigationItems,
    this.navigationIndex,
    this.coverHeroTag,
  }) : assert(
         item != null || localItem != null,
         'Either item or localItem must be provided',
       ),
       assert(
         historyNavigationItems == null || localNavigationItems == null,
         'Provide only one navigation list type',
       ),
       assert(
         navigationIndex == null ||
             ((historyNavigationItems != null &&
                     navigationIndex >= 0 &&
                     navigationIndex < historyNavigationItems.length) ||
                 (localNavigationItems != null &&
                     navigationIndex >= 0 &&
                     navigationIndex < localNavigationItems.length)),
         'navigationIndex must be within the provided navigation list',
       );

  @override
  ConsumerState<TrackMetadataScreen> createState() =>
      _TrackMetadataScreenState();
}

class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
    with CollapsingHeaderScrollMixin<TrackMetadataScreen> {
  static const int _maxCoverPreviewCacheEntries = 96;
  static final Map<String, _EmbeddedCoverPreviewCacheEntry>
  _embeddedCoverPreviewCache = {};

  bool _fileExists = false;
  bool _hasCheckedFile = false;
  int? _fileSize;
  String? _lyrics;
  String? _rawLyrics;
  bool _lyricsLoading = false;
  String? _lyricsError;
  String? _lyricsSource;
  bool _lyricsEmbedded = false;
  bool _isEmbedding = false;
  bool _isInstrumental = false;
  bool _embeddedLyricsChecked = false;
  bool _isConverting = false;
  bool _hasMetadataChanges = false;
  bool _hasLoadedResolvedAudioMetadata = false;
  bool _isTrackSwipeNavigationInFlight = false;
  int _metadataLoadGeneration = 0;
  int _metadataTransitionDirection = 0;
  late DownloadHistoryItem? _currentDownloadItem;
  late LocalLibraryItem? _currentLocalLibraryItem;
  late int? _currentNavigationIndex;
  Map<String, dynamic>? _editedMetadata;
  String? _resolvedAudioFormat;
  String? _embeddedCoverPreviewPath;
  ({int width, int height})? _embeddedCoverDimensions;
  static final RegExp _invalidFileNameChars = RegExp(r'[<>:"/\\|?*\x00-\x1f]');
  static final RegExp _multiUnderscore = RegExp(r'_+');
  static final RegExp _leadingOrTrailingDots = RegExp(r'^\.+|\.+$');
  static const List<String> _months = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'Jun',
    'Jul',
    'Aug',
    'Sep',
    'Oct',
    'Nov',
    'Dec',
  ];

  String get _coverCacheKey => _itemId;

  @override
  void initState() {
    super.initState();
    _currentDownloadItem = widget.item;
    _currentLocalLibraryItem = widget.localItem;
    _currentNavigationIndex = widget.navigationIndex;
    _checkFile();
  }

  @override
  void dispose() {
    unawaited(_cleanupTempFileAndParentIfNotCached(_embeddedCoverPreviewPath));
    super.dispose();
  }

  /// setState is @protected, so the extension part files route rebuilds
  /// through this forwarder.
  void _setState(VoidCallback fn) => setState(fn);

  @override
  double calculateExpandedHeight(BuildContext context) {
    final mediaSize = MediaQuery.sizeOf(context);
    return (mediaSize.height * 0.55).clamp(360.0, 520.0);
  }

  Future<void> _checkFile() async {
    final generation = _metadataLoadGeneration;
    final filePath = cleanFilePath;

    bool exists = false;
    int? size;
    try {
      final stat = await fileStat(filePath);
      if (stat != null) {
        exists = true;
        size = stat.size;
      }
    } catch (_) {}

    if (mounted &&
        generation == _metadataLoadGeneration &&
        filePath == cleanFilePath &&
        (exists != _fileExists || size != _fileSize || !_hasCheckedFile)) {
      setState(() {
        _fileExists = exists;
        _fileSize = size;
        _hasCheckedFile = true;
      });
    }

    if (mounted &&
        generation == _metadataLoadGeneration &&
        filePath == cleanFilePath &&
        exists &&
        _lyrics == null &&
        !_lyricsLoading) {
      _checkEmbeddedLyrics();
    }
    if (mounted &&
        generation == _metadataLoadGeneration &&
        filePath == cleanFilePath &&
        exists &&
        !_isCueVirtualTrack &&
        !_hasLoadedResolvedAudioMetadata) {
      unawaited(_refreshResolvedAudioMetadataFromFile());
    }
    if (mounted &&
        generation == _metadataLoadGeneration &&
        filePath == cleanFilePath &&
        exists &&
        !_hasPath(_embeddedCoverPreviewPath)) {
      final cachedCover = await _getCachedEmbeddedCoverPreviewIfValid(
        _coverCacheKey,
        filePath,
      );
      if (mounted &&
          generation == _metadataLoadGeneration &&
          filePath == cleanFilePath &&
          cachedCover != null) {
        setState(() {
          _embeddedCoverPreviewPath = cachedCover.previewPath;
          _embeddedCoverDimensions = cachedCover.dimensions;
        });
      } else if (mounted &&
          generation == _metadataLoadGeneration &&
          filePath == cleanFilePath) {
        // The information card reports the artwork embedded in the audio
        // file, not a potentially resized Library thumbnail or remote cover.
        // Extraction is cached, so revisiting the same track does not repeat
        // the work.
        unawaited(_refreshEmbeddedCoverPreview());
      }
    }
  }

  bool _hasPath(String? path) => path != null && path.trim().isNotEmpty;

  Future<void> _cleanupTempFileAndParent(String? path) async {
    if (!_hasPath(path)) return;
    final file = File(path!);
    try {
      if (await file.exists()) {
        await file.delete();
      }
    } catch (_) {}
    try {
      final dir = file.parent;
      if (await dir.exists()) {
        await dir.delete(recursive: true);
      }
    } catch (_) {}
  }

  Future<void> _cleanupTempFileAndParentIfNotCached(String? path) async {
    if (_isCacheTrackedPath(path)) return;
    await _cleanupTempFileAndParent(path);
  }

  Future<void> _refreshResolvedAudioMetadataFromFile() async {
    final generation = _metadataLoadGeneration;
    final sourcePath = cleanFilePath;
    if ((_isLocalItem && _localLibraryItem == null) ||
        (!_isLocalItem && _downloadItem == null) ||
        _isCueVirtualTrack ||
        _hasLoadedResolvedAudioMetadata) {
      return;
    }

    _hasLoadedResolvedAudioMetadata = true;

    try {
      final metadata = await PlatformBridge.readDisplayAudioMetadata(
        sourcePath,
      );
      if (!mounted ||
          generation != _metadataLoadGeneration ||
          sourcePath != cleanFilePath) {
        return;
      }
      if (metadata['error'] != null) {
        return;
      }

      final resolvedBitDepth = readPositiveInt(metadata['bit_depth']);
      final resolvedSampleRate = readPositiveInt(metadata['sample_rate']);
      final resolvedFormat = detectedAudioFormatFromMetadata(metadata);
      final storedFormat = _isLocalItem
          ? _localLibraryItem?.format
          : _downloadItem?.format;
      final formatChanged =
          resolvedFormat != null &&
          resolvedFormat != normalizeAudioFormatValue(storedFormat);
      // Lossless files carry an average bitrate too (computed by the backend
      // from file size / duration) — useful for spotting fake 24-bit rips.
      final resolvedBitrate = _readPlausibleBitrateKbps(
        metadata['bitrate'] ?? metadata['bit_rate'],
      );
      final resolvedDuration = readPositiveInt(metadata['duration']);
      final resolvedAlbum = metadata['album']?.toString();
      final resolvedTitle = metadata['title']?.toString();
      final resolvedArtist = metadata['artist']?.toString();
      final resolvedAlbumArtist = metadata['album_artist']?.toString();
      final resolvedDate = metadata['date']?.toString();
      final resolvedGenre = metadata['genre']?.toString();
      final resolvedQuality = _displayQualityForValues(
        format: resolvedFormat ?? _storedAudioFormat,
        bitDepth: resolvedBitDepth ?? bitDepth,
        sampleRate: resolvedSampleRate ?? sampleRate,
        bitrateKbps: resolvedBitrate ?? _audioBitrate,
        storedQuality: _quality,
      );

      final needsDuration =
          resolvedDuration != null &&
          resolvedDuration > 0 &&
          (duration == null || duration == 0);

      // Resolve label/copyright from file when the model doesn't carry them
      // (e.g. local library items, or download history items without these fields).
      final resolvedTrackNumber = readPositiveInt(metadata['track_number']);
      final resolvedTotalTracks = readPositiveInt(metadata['total_tracks']);
      final resolvedDiscNumber = readPositiveInt(metadata['disc_number']);
      final resolvedTotalDiscs = readPositiveInt(metadata['total_discs']);
      final resolvedComposer = metadata['composer']?.toString();
      final resolvedLabel = metadata['label']?.toString();
      final resolvedCopyright = metadata['copyright']?.toString();
      final resolvedISRC = metadata['isrc']?.toString();
      final resolvedAlbumType = metadata['album_type']?.toString();
      final resolvedUPC = (metadata['upc'] ?? metadata['barcode'])?.toString();
      final resolvedComment = metadata['comment']?.toString();
      final resolvedExplicit = parseExplicitFlag(metadata['explicit']);
      final hasResolvedLyricsValue =
          metadata.containsKey('hasLyrics') || metadata.containsKey('lyrics');
      final resolvedHasLyrics =
          metadata['hasLyrics'] == true ||
          hasUsableLyricsContent(metadata['lyrics']?.toString() ?? '');
      final needsExplicit =
          resolvedExplicit != null && resolvedExplicit != isExplicit;
      final needsLyricsAvailability =
          hasResolvedLyricsValue &&
          (_isLocalItem
              ? resolvedHasLyrics != _localLibraryItem?.hasLyrics
              : resolvedHasLyrics != _downloadItem?.hasLyrics ||
                    (_downloadItem?.lyricsMetadataScanVersion ?? 0) < 1);
      final needsTrackNumber =
          resolvedTrackNumber != null &&
          resolvedTrackNumber > 0 &&
          trackNumber == null;
      final needsTotalTracks =
          resolvedTotalTracks != null &&
          resolvedTotalTracks > 0 &&
          totalTracks == null;
      final needsDiscNumber =
          resolvedDiscNumber != null &&
          resolvedDiscNumber > 0 &&
          discNumber == null;
      final needsTotalDiscs =
          resolvedTotalDiscs != null &&
          resolvedTotalDiscs > 0 &&
          totalDiscs == null;
      final needsComposer =
          resolvedComposer != null &&
          resolvedComposer.isNotEmpty &&
          (composer == null || composer!.isEmpty);
      // The file is the source of truth for edited tags: an edit writes the
      // new value into the file, but the cached history item may still carry
      // the old one. Prefer the file value whenever present so re-opening the
      // screen reflects the latest saved tags instead of the stale model.
      // (Empty file values never override the model, so nothing is hidden.)
      bool present(String? v) => v != null && v.trim().isNotEmpty;
      final fileHasTitle = present(resolvedTitle);
      final fileHasArtist = present(resolvedArtist);
      final fileHasAlbumArtist = present(resolvedAlbumArtist);
      final fileHasAlbum = present(resolvedAlbum);
      final fileHasDate = present(resolvedDate);
      final fileHasGenre = present(resolvedGenre);
      final fileHasComposer = present(resolvedComposer);
      final fileHasCopyright = present(resolvedCopyright);
      final fileHasISRC = present(resolvedISRC);
      final fileHasLabel = present(resolvedLabel);
      final fileHasAlbumType = present(resolvedAlbumType);
      final fileHasUPC = present(resolvedUPC);
      final fileHasComment = present(resolvedComment);
      final fileHasTrackNumber =
          resolvedTrackNumber != null && resolvedTrackNumber > 0;
      final fileHasTotalTracks =
          resolvedTotalTracks != null && resolvedTotalTracks > 0;
      final fileHasDiscNumber =
          resolvedDiscNumber != null && resolvedDiscNumber > 0;
      final fileHasTotalDiscs =
          resolvedTotalDiscs != null && resolvedTotalDiscs > 0;

      final shouldPersistResolvedAudioMetadata =
          !_isLocalItem &&
          (resolvedBitDepth != null ||
              resolvedSampleRate != null ||
              resolvedBitrate != null ||
              resolvedFormat != null ||
              needsTrackNumber ||
              needsTotalTracks ||
              needsDiscNumber ||
              needsTotalDiscs ||
              needsDuration ||
              needsComposer ||
              needsExplicit ||
              needsLyricsAvailability ||
              (isPlaceholderQualityLabel(_quality) && resolvedQuality != null));
      final localItem = _localLibraryItem;
      final localAudioMetadataChanged =
          localItem != null &&
          ((resolvedBitDepth != null &&
                  resolvedBitDepth != localItem.bitDepth) ||
              (resolvedSampleRate != null &&
                  resolvedSampleRate != localItem.sampleRate) ||
              (resolvedBitrate != null &&
                  resolvedBitrate != localItem.bitrate) ||
              needsExplicit ||
              needsLyricsAvailability ||
              needsDuration ||
              formatChanged);

      if ((resolvedBitDepth != null ||
              resolvedSampleRate != null ||
              resolvedFormat != null ||
              fileHasTitle ||
              fileHasArtist ||
              fileHasAlbumArtist ||
              fileHasAlbum ||
              fileHasDate ||
              fileHasGenre ||
              needsDuration ||
              fileHasTrackNumber ||
              fileHasTotalTracks ||
              fileHasDiscNumber ||
              fileHasTotalDiscs ||
              fileHasComposer ||
              fileHasISRC ||
              fileHasLabel ||
              fileHasCopyright ||
              fileHasAlbumType ||
              fileHasUPC ||
              fileHasComment ||
              needsExplicit ||
              isPlaceholderQualityLabel(_quality)) &&
          mounted) {
        setState(() {
          _resolvedAudioFormat = resolvedFormat;
          _editedMetadata = {
            ...?_editedMetadata,
            // ignore: use_null_aware_elements
            if (resolvedBitDepth != null) 'bit_depth': resolvedBitDepth,
            // ignore: use_null_aware_elements
            if (resolvedSampleRate != null) 'sample_rate': resolvedSampleRate,
            if (fileHasTitle) 'title': resolvedTitle,
            if (fileHasArtist) 'artist': resolvedArtist,
            if (fileHasAlbumArtist) 'album_artist': resolvedAlbumArtist,
            if (fileHasAlbum) 'album': resolvedAlbum,
            if (fileHasDate) 'date': resolvedDate,
            if (fileHasGenre) 'genre': resolvedGenre,
            if (needsDuration) 'duration': resolvedDuration,
            if (fileHasTrackNumber) 'track_number': resolvedTrackNumber,
            if (fileHasTotalTracks) 'total_tracks': resolvedTotalTracks,
            if (fileHasDiscNumber) 'disc_number': resolvedDiscNumber,
            if (fileHasTotalDiscs) 'total_discs': resolvedTotalDiscs,
            if (fileHasComposer) 'composer': resolvedComposer,
            if (fileHasISRC) 'isrc': resolvedISRC,
            if (fileHasLabel) 'label': resolvedLabel,
            if (fileHasCopyright) 'copyright': resolvedCopyright,
            if (fileHasAlbumType) 'album_type': resolvedAlbumType,
            if (fileHasUPC) 'upc': resolvedUPC,
            if (fileHasComment) 'comment': resolvedComment,
            'explicit': ?resolvedExplicit,
          };
        });
      }

      if (shouldPersistResolvedAudioMetadata) {
        await ref
            .read(downloadHistoryProvider.notifier)
            .updateAudioMetadataForItem(
              id: _downloadItem!.id,
              quality: resolvedQuality,
              bitDepth: resolvedBitDepth,
              sampleRate: resolvedSampleRate,
              bitrate: resolvedBitrate,
              format: resolvedFormat,
              trackNumber: needsTrackNumber ? resolvedTrackNumber : null,
              totalTracks: needsTotalTracks ? resolvedTotalTracks : null,
              discNumber: needsDiscNumber ? resolvedDiscNumber : null,
              totalDiscs: needsTotalDiscs ? resolvedTotalDiscs : null,
              duration: needsDuration ? resolvedDuration : null,
              composer: needsComposer ? resolvedComposer : null,
              explicit: needsExplicit ? resolvedExplicit : null,
              hasLyrics: hasResolvedLyricsValue ? resolvedHasLyrics : null,
              lyricsMetadataScanVersion: hasResolvedLyricsValue ? 1 : null,
            );
        if (mounted && _downloadItem != null) {
          setState(() {
            _currentDownloadItem = _downloadItem!.copyWith(
              quality: resolvedQuality,
              bitDepth: resolvedBitDepth,
              sampleRate: resolvedSampleRate,
              bitrate: resolvedBitrate,
              format: resolvedFormat,
              trackNumber: needsTrackNumber ? resolvedTrackNumber : null,
              totalTracks: needsTotalTracks ? resolvedTotalTracks : null,
              discNumber: needsDiscNumber ? resolvedDiscNumber : null,
              totalDiscs: needsTotalDiscs ? resolvedTotalDiscs : null,
              duration: needsDuration ? resolvedDuration : null,
              composer: needsComposer ? resolvedComposer : null,
              explicit: needsExplicit ? resolvedExplicit : null,
              hasLyrics: hasResolvedLyricsValue ? resolvedHasLyrics : null,
              lyricsMetadataScanVersion: hasResolvedLyricsValue ? 1 : null,
            );
          });
        }
      } else if (_isLocalItem && localAudioMetadataChanged) {
        await LibraryDatabase.instance.updateAudioMetadata(
          localItem.id,
          duration: resolvedDuration,
          bitDepth: resolvedBitDepth,
          sampleRate: resolvedSampleRate,
          bitrate: resolvedBitrate,
          explicit: needsExplicit ? resolvedExplicit : null,
          hasLyrics: hasResolvedLyricsValue ? resolvedHasLyrics : null,
          format: formatChanged ? resolvedFormat : null,
        );
        if (mounted &&
            generation == _metadataLoadGeneration &&
            sourcePath == cleanFilePath) {
          setState(() {
            _currentLocalLibraryItem = localItem.withAudioMetadata(
              duration: resolvedDuration,
              bitDepth: resolvedBitDepth,
              sampleRate: resolvedSampleRate,
              bitrate: resolvedBitrate,
              explicit: needsExplicit ? resolvedExplicit : null,
              hasLyrics: hasResolvedLyricsValue ? resolvedHasLyrics : null,
              format: resolvedFormat,
            );
          });
        }
        await ref.read(localLibraryProvider.notifier).reloadFromStorage();
      }
    } catch (e) {
      _log.w('Failed to resolve audio metadata from file: $e');
    }
  }

  void _markMetadataChanged() {
    _hasMetadataChanges = true;
  }

  void _popWithMetadataResult() {
    Navigator.pop(context, _hasMetadataChanges ? true : null);
  }

  void _handleHorizontalDragEnd(DragEndDetails details) {
    final velocity = details.primaryVelocity;
    if (velocity == null || velocity.abs() < 350) return;
    if (velocity < 0) {
      unawaited(_navigateToAdjacentTrack(1));
    } else {
      unawaited(_navigateToAdjacentTrack(-1));
    }
  }

  Future<void> _navigateToAdjacentTrack(int offset) async {
    if (_isTrackSwipeNavigationInFlight || !_hasTrackSwipeNavigation) return;
    final currentIndex = _navigationIndex;
    if (currentIndex == null) return;
    final targetIndex = currentIndex + offset;
    if (targetIndex < 0 || targetIndex >= _navigationLength) return;

    _isTrackSwipeNavigationInFlight = true;
    final oldPreviewPath = _embeddedCoverPreviewPath;

    try {
      setState(() {
        _metadataLoadGeneration++;
        _metadataTransitionDirection = offset > 0 ? 1 : -1;
        _currentNavigationIndex = targetIndex;
        if (_hasHistoryNavigation) {
          _currentDownloadItem = widget.historyNavigationItems![targetIndex];
          _currentLocalLibraryItem = null;
        } else {
          _currentDownloadItem = null;
          _currentLocalLibraryItem = widget.localNavigationItems![targetIndex];
        }
        _fileExists = false;
        _hasCheckedFile = false;
        _fileSize = null;
        _lyrics = null;
        _rawLyrics = null;
        _lyricsLoading = false;
        _lyricsError = null;
        _lyricsSource = null;
        showTitleInAppBar = false;
        _lyricsEmbedded = false;
        _isInstrumental = false;
        _embeddedLyricsChecked = false;
        _hasLoadedResolvedAudioMetadata = false;
        _editedMetadata = null;
        _resolvedAudioFormat = null;
        _embeddedCoverPreviewPath = null;
        _embeddedCoverDimensions = null;
      });

      if (scrollController.hasClients) {
        scrollController.jumpTo(0);
      }

      if (oldPreviewPath != null) {
        unawaited(_cleanupTempFileAndParentIfNotCached(oldPreviewPath));
      }
      await _checkFile();
    } finally {
      if (mounted) {
        _isTrackSwipeNavigationInFlight = false;
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    final expandedHeight = calculateExpandedHeight(context);
    final bottomInset = context.navBarBottomInset;

    return GestureDetector(
      behavior: HitTestBehavior.translucent,
      onHorizontalDragEnd: _handleHorizontalDragEnd,
      child: Scaffold(
        body: CustomScrollView(
          controller: scrollController,
          slivers: [
            SliverAppBar(
              expandedHeight: expandedHeight,
              pinned: true,
              stretch: true,
              backgroundColor: colorScheme.surface,
              surfaceTintColor: Colors.transparent,
              title: AnimatedOpacity(
                duration: const Duration(milliseconds: 200),
                opacity: showTitleInAppBar ? 1.0 : 0.0,
                child: ExplicitTrackTitle(
                  title: trackName,
                  explicit: isExplicit,
                  style: TextStyle(
                    color: colorScheme.onSurface,
                    fontWeight: FontWeight.w600,
                    fontSize: 16,
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              flexibleSpace: LayoutBuilder(
                builder: (context, constraints) {
                  final collapseRatio =
                      (constraints.maxHeight - kToolbarHeight) /
                      (expandedHeight - kToolbarHeight);
                  final showContent = collapseRatio > 0.3;

                  return FlexibleSpaceBar(
                    collapseMode: CollapseMode.pin,
                    background: _buildHeaderBackground(
                      context,
                      colorScheme,
                      expandedHeight,
                      showContent,
                    ),
                    stretchModes: const [StretchMode.zoomBackground],
                  );
                },
              ),
              leading: IconButton(
                tooltip: MaterialLocalizations.of(context).backButtonTooltip,
                icon: Container(
                  padding: const EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    color: Colors.black.withValues(alpha: 0.4),
                    shape: BoxShape.circle,
                  ),
                  child: const Icon(Icons.arrow_back, color: Colors.white),
                ),
                onPressed: _popWithMetadataResult,
              ),
              actions: [
                IconButton(
                  tooltip: MaterialLocalizations.of(context).showMenuTooltip,
                  icon: Container(
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.black.withValues(alpha: 0.4),
                      shape: BoxShape.circle,
                    ),
                    child: const Icon(Icons.more_vert, color: Colors.white),
                  ),
                  onPressed: () => _showOptionsMenu(context, ref, colorScheme),
                ),
              ],
            ),

            SliverToBoxAdapter(
              child: Center(
                child: ConstrainedBox(
                  constraints: BoxConstraints(
                    maxWidth: adaptiveContentMaxWidth(
                      MediaQuery.sizeOf(context).width,
                    ),
                  ),
                  child: _buildAnimatedTrackContent(context, ref, colorScheme),
                ),
              ),
            ),
            SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
          ],
        ),
      ),
    );
  }

  Future<void> _enqueueThis(WidgetRef ref, {required bool playNext}) async {
    final controller = ref.read(musicPlayerControllerProvider);
    final item = widget.item;
    final localItem = widget.localItem;
    if (item != null) {
      if (playNext) {
        await controller.playNextHistory(item);
      } else {
        await controller.addToQueueHistory(item);
      }
    } else if (localItem != null) {
      if (playNext) {
        await controller.playNextLocal(localItem);
      } else {
        await controller.addToQueueLocal(localItem);
      }
    }
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(
          playNext
              ? context.l10n.snackbarPlayingNext
              : context.l10n.snackbarAddedToQueueGeneric,
        ),
      ),
    );
  }

  Widget _buildActionButtons(
    BuildContext context,
    WidgetRef ref,
    ColorScheme colorScheme,
    bool fileExists,
  ) {
    return Row(
      children: [
        Expanded(
          flex: 2,
          child: FilledButton.icon(
            onPressed: fileExists
                ? () => _openFile(context, rawFilePath)
                : null,
            icon: const Icon(Icons.play_arrow),
            label: Text(context.l10n.trackMetadataPlay),
            style: FilledButton.styleFrom(
              padding: const EdgeInsets.symmetric(vertical: 16),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(16),
              ),
            ),
          ),
        ),
        const SizedBox(width: 12),

        Expanded(
          child: OutlinedButton.icon(
            onPressed: () => _confirmDelete(context, ref, colorScheme),
            icon: Icon(Icons.delete_outline, color: colorScheme.error),
            label: Text(
              context.l10n.trackMetadataDelete,
              style: TextStyle(color: colorScheme.error),
            ),
            style: OutlinedButton.styleFrom(
              padding: const EdgeInsets.symmetric(vertical: 16),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(16),
              ),
              side: BorderSide(color: colorScheme.error.withValues(alpha: 0.5)),
            ),
          ),
        ),
      ],
    );
  }
}

class _MetadataOption {
  final IconData icon;
  final String label;
  final VoidCallback onTap;
  final bool destructive;
  final bool dividerAbove;

  const _MetadataOption({
    required this.icon,
    required this.label,
    required this.onTap,
    this.destructive = false,
    this.dividerAbove = false,
  });
}

class _MetadataOptionTile extends StatelessWidget {
  final _MetadataOption option;
  final ColorScheme colorScheme;
  final VoidCallback onTap;

  const _MetadataOptionTile({
    required this.option,
    required this.colorScheme,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final iconColor = option.destructive
        ? colorScheme.error
        : colorScheme.onSurfaceVariant;
    final titleColor = option.destructive ? colorScheme.error : null;

    return InkWell(
      onTap: onTap,
      splashColor: colorScheme.primary.withValues(alpha: 0.12),
      highlightColor: Colors.transparent,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
        child: Row(
          children: [
            Icon(option.icon, color: iconColor, size: 24),
            const SizedBox(width: 16),
            Expanded(
              child: Text(
                option.label,
                style: Theme.of(
                  context,
                ).textTheme.bodyLarge?.copyWith(color: titleColor),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
