part of 'track_metadata_screen.dart';

extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState {
  /// Whether the current file format supports conversion
  bool get _isConvertibleFormat {
    final lower = cleanFilePath.toLowerCase();
    return lower.endsWith('.flac') ||
        lower.endsWith('.m4a') ||
        lower.endsWith('.aac') ||
        lower.endsWith('.wav') ||
        lower.endsWith('.aiff') ||
        lower.endsWith('.aif') ||
        lower.endsWith('.mp3') ||
        lower.endsWith('.opus') ||
        lower.endsWith('.ogg');
  }

  /// Whether the current file is a CUE sheet (or CUE-referenced)
  bool get _isCueFile {
    if (isCueVirtualPath(rawFilePath)) return true;
    final lower = cleanFilePath.toLowerCase();
    if (lower.endsWith('.cue')) return true;
    if (_isLocalItem && _localLibraryItem != null) {
      final format = _localLibraryItem!.format ?? '';
      if (format.startsWith('cue+')) return true;
    }
    return false;
  }

  String get _currentFileFormat {
    // For CUE tracks, use the format from the library item (e.g. "cue+flac")
    if (_isCueFile && _isLocalItem && _localLibraryItem != null) {
      final format = _localLibraryItem!.format ?? '';
      if (format.startsWith('cue+')) {
        final audioFmt = format.substring(4).toUpperCase();
        return 'CUE+$audioFmt';
      }
    }
    if (_isLocalItem && _localLibraryItem != null) {
      final format = normalizeOptionalString(
        _localLibraryItem!.format,
      )?.toLowerCase().replaceAll('-', '_');
      switch (format) {
        case 'flac':
          return 'FLAC';
        case 'alac':
          return 'ALAC';
        case 'm4a':
          return 'M4A';
        case 'aac':
        case 'mp4a':
          return 'AAC';
        case 'mp3':
          return 'MP3';
        case 'opus':
        case 'ogg':
          return 'Opus';
        case 'wav':
        case 'wave':
          return 'WAV';
        case 'aiff':
        case 'aif':
        case 'aifc':
          return 'AIFF';
      }
    }
    final lower = cleanFilePath.toLowerCase();
    if (lower.endsWith('.flac')) return 'FLAC';
    if (lower.endsWith('.m4a')) return 'M4A';
    if (lower.endsWith('.aac')) return 'AAC';
    if (lower.endsWith('.wav')) return 'WAV';
    if (lower.endsWith('.aiff') || lower.endsWith('.aif')) return 'AIFF';
    if (lower.endsWith('.mp3')) return 'MP3';
    if (lower.endsWith('.opus') || lower.endsWith('.ogg')) return 'Opus';
    if (lower.endsWith('.cue')) return 'CUE';
    return 'Unknown';
  }

  Map<String, String> _buildFallbackMetadata() {
    String formatIndexTag(int number, int? total) {
      if (total != null && total > 0) {
        return '$number/$total';
      }
      return number.toString();
    }

    return {
      'TITLE': trackName,
      'ARTIST': artistName,
      'ALBUM': albumName,
      if (albumArtist != null && albumArtist!.isNotEmpty)
        'ALBUMARTIST': albumArtist!,
      if (trackNumber != null)
        'TRACKNUMBER': formatIndexTag(trackNumber!, totalTracks),
      if (discNumber != null)
        'DISCNUMBER': formatIndexTag(discNumber!, totalDiscs),
      if (releaseDate != null && releaseDate!.isNotEmpty) 'DATE': releaseDate!,
      if (isrc != null && isrc!.isNotEmpty) 'ISRC': isrc!,
      if (genre != null && genre!.isNotEmpty) 'GENRE': genre!,
      if (label != null && label!.isNotEmpty) 'LABEL': label!,
      if (copyright != null && copyright!.isNotEmpty) 'COPYRIGHT': copyright!,
      if (composer != null && composer!.isNotEmpty) 'COMPOSER': composer!,
      if (isExplicit) 'ITUNESADVISORY': '1',
    };
  }

  Map<String, String> _mapMetadataForTagEmbed(Map<String, dynamic> source) {
    final mapped = <String, String>{};

    void put(String key, dynamic value) {
      final normalized = value?.toString().trim();
      if (normalized == null || normalized.isEmpty) return;
      mapped[key] = normalized;
    }

    put('TITLE', source['title']);
    put('ARTIST', source['artist']);
    put('ALBUM', source['album']);
    put('ALBUMARTIST', source['album_artist']);
    put('DATE', source['date']);
    put('ISRC', source['isrc']);
    put('GENRE', source['genre']);
    put('ORGANIZATION', source['label']);
    put('COPYRIGHT', source['copyright']);
    put('COMPOSER', source['composer']);
    put('COMMENT', source['comment']);
    put('LYRICS', source['lyrics']);
    put('UNSYNCEDLYRICS', source['lyrics']);
    if (parseExplicitFlag(source['explicit']) == true) {
      mapped['ITUNESADVISORY'] = '1';
    }

    final trackNumber = source['track_number'];
    final totalTracks = source['total_tracks'];
    if (trackNumber != null && trackNumber.toString() != '0') {
      final trackTag =
          totalTracks != null &&
              totalTracks.toString().isNotEmpty &&
              totalTracks.toString() != '0'
          ? '${trackNumber.toString()}/${totalTracks.toString()}'
          : trackNumber;
      put('TRACKNUMBER', trackTag);
    }
    final discNumber = source['disc_number'];
    final totalDiscs = source['total_discs'];
    if (discNumber != null && discNumber.toString() != '0') {
      final discTag =
          totalDiscs != null &&
              totalDiscs.toString().isNotEmpty &&
              totalDiscs.toString() != '0'
          ? '${discNumber.toString()}/${totalDiscs.toString()}'
          : discNumber;
      put('DISCNUMBER', discTag);
    }

    return mapped;
  }

  String? _extractLossyBitrateLabel(String? quality) {
    if (quality == null || quality.isEmpty) return null;
    final match = RegExp(
      r'(\d+)\s*k(?:bps)?',
      caseSensitive: false,
    ).firstMatch(quality);
    if (match == null) return null;
    return '${match.group(1)}kbps';
  }

  String _extractFileNameFromPathOrUri(String pathOrUri) {
    if (pathOrUri.isEmpty) return '';
    try {
      if (pathOrUri.startsWith('content://')) {
        final uri = Uri.parse(pathOrUri);
        if (uri.pathSegments.isNotEmpty) {
          var last = Uri.decodeComponent(uri.pathSegments.last);
          if (last.contains('/')) {
            last = last.split('/').last;
          }
          if (last.contains(':')) {
            last = last.split(':').last;
          }
          if (last.isNotEmpty) return last;
        }
      }
    } catch (_) {}

    final normalized = pathOrUri.replaceAll('\\', '/');
    if (normalized.contains('/')) {
      return normalized.split('/').last;
    }
    return normalized;
  }

  Future<void> _rescanReplayGain() async {
    if (!_fileExists) return;
    final messenger = ScaffoldMessenger.of(context);
    messenger.clearSnackBars();
    messenger.showSnackBar(
      SnackBar(
        content: Text(context.l10n.trackReplayGainScanning),
        duration: const Duration(seconds: 30),
      ),
    );
    bool ok = false;
    try {
      ok = await ReplayGainService.applyToFile(cleanFilePath);
    } catch (e) {
      _log.w('ReplayGain rescan failed: $e');
    }
    if (!mounted) return;
    messenger.hideCurrentSnackBar();
    messenger.showSnackBar(
      SnackBar(
        content: Text(
          ok
              ? context.l10n.trackReplayGainSuccess
              : context.l10n.trackReplayGainFailed,
        ),
      ),
    );
  }

  void _showConvertSheet(BuildContext context) {
    final currentFormat = _currentFileFormat;
    final isLosslessSource = isLosslessConversionSource(currentFormat);

    final formats = audioConversionTargetFormats
        .where(
          (target) => canConvertAudioFormat(
            sourceFormat: currentFormat,
            targetFormat: target,
          ),
        )
        .toList(growable: false);

    final labels = context.l10n.losslessConversionLabels;

    showModalBottomSheet<void>(
      context: context,
      useRootNavigator: true,
      isScrollControlled: true,
      builder: (sheetContext) => BatchConvertSheet(
        formats: formats,
        title: context.l10n.trackConvertTitle,
        subtitle: currentFormat,
        sourceIsLossless: isLosslessSource,
        sourceBitDepth: bitDepth,
        sourceSampleRate: sampleRate,
        confirmLabelBuilder:
            (format, bitrate, isLosslessTarget, losslessQuality) {
              return isLosslessTarget
                  ? context.l10n.trackConvertActionLabelLossless(
                      currentFormat,
                      format,
                      losslessQualityLabel(
                        losslessQuality,
                        originalLabel: labels.original,
                        originalQualityLabel: labels.originalQuality,
                      ),
                    )
                  : context.l10n.trackConvertActionLabelLossy(
                      currentFormat,
                      format,
                      bitrate,
                    );
            },
        onConvert:
            (
              format,
              bitrate,
              losslessQuality,
              losslessProcessing,
              keepOriginal,
            ) {
              Navigator.pop(sheetContext);
              _confirmAndConvert(
                context: this.context,
                sourceFormat: currentFormat,
                targetFormat: format,
                bitrate: bitrate,
                losslessQuality: losslessQuality,
                losslessProcessing: losslessProcessing,
                keepOriginal: keepOriginal,
              );
            },
      ),
    );
  }

  void _showCueSplitSheet(BuildContext context) async {
    var cuePath = cleanFilePath;
    final unknownAlbum = context.l10n.unknownAlbum;
    final unknownArtist = context.l10n.unknownArtist;
    final trackSuffix = RegExp(r'#track\d+$');
    if (trackSuffix.hasMatch(cuePath)) {
      cuePath = cuePath.replaceFirst(trackSuffix, '');
    }

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(context.l10n.snackbarLoadingCueSheet)),
    );

    try {
      final cueInfo = await PlatformBridge.parseCueSheet(cuePath);

      if (!mounted) return;
      _hideCurrentSnackBar();

      if (cueInfo.containsKey('error')) {
        _showSnackBarMessage(_l10nCueSplitNoAudioFile);
        return;
      }

      final album = cueInfo['album'] as String? ?? unknownAlbum;
      final artist = cueInfo['artist'] as String? ?? unknownArtist;
      final audioPath = cueInfo['audio_path'] as String? ?? '';
      final genre = cueInfo['genre'] as String? ?? '';
      final date = cueInfo['date'] as String? ?? '';
      final tracksRaw = cueInfo['tracks'] as List<dynamic>? ?? [];

      if (audioPath.isEmpty) {
        _showSnackBarMessage(_l10nCueSplitNoAudioFile);
        return;
      }

      final tracks = tracksRaw
          .map((t) => CueSplitTrackInfo.fromJson(t as Map<String, dynamic>))
          .toList();

      if (tracks.isEmpty) {
        _showSnackBarMessage(_l10nCueSplitFailed);
        return;
      }

      if (!mounted) return;

      showModalBottomSheet<void>(
        context: this.context,
        useRootNavigator: true,
        isScrollControlled: true,
        builder: (sheetContext) {
          final colorScheme = Theme.of(sheetContext).colorScheme;
          return SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const AppSheetHandle(),
                  const SizedBox(height: 16),
                  Text(
                    sheetContext.l10n.cueSplitTitle,
                    style: Theme.of(sheetContext).textTheme.titleLarge
                        ?.copyWith(fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 12),
                  Text(
                    sheetContext.l10n.cueSplitAlbum(album),
                    style: Theme.of(sheetContext).textTheme.bodyMedium
                        ?.copyWith(color: colorScheme.onSurfaceVariant),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    sheetContext.l10n.cueSplitArtist(artist),
                    style: Theme.of(sheetContext).textTheme.bodyMedium
                        ?.copyWith(color: colorScheme.onSurfaceVariant),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    sheetContext.l10n.cueSplitTrackCount(tracks.length),
                    style: Theme.of(sheetContext).textTheme.bodyMedium
                        ?.copyWith(
                          color: colorScheme.primary,
                          fontWeight: FontWeight.w600,
                        ),
                  ),
                  const SizedBox(height: 16),
                  ConstrainedBox(
                    constraints: const BoxConstraints(maxHeight: 200),
                    child: ListView.builder(
                      shrinkWrap: true,
                      itemCount: tracks.length,
                      itemBuilder: (context, index) {
                        final track = tracks[index];
                        final duration = track.endSec > 0
                            ? track.endSec - track.startSec
                            : 0.0;
                        final durationStr = duration > 0
                            ? '${(duration ~/ 60).toString().padLeft(2, '0')}:${(duration.toInt() % 60).toString().padLeft(2, '0')}'
                            : '';
                        return ListTile(
                          dense: true,
                          contentPadding: EdgeInsets.zero,
                          leading: CircleAvatar(
                            radius: 14,
                            backgroundColor: colorScheme.primaryContainer,
                            child: Text(
                              '${track.number}',
                              style: TextStyle(
                                fontSize: 11,
                                color: colorScheme.onPrimaryContainer,
                              ),
                            ),
                          ),
                          title: Text(
                            track.title,
                            style: const TextStyle(fontSize: 13),
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
                          ),
                          subtitle: track.artist.isNotEmpty
                              ? Text(
                                  track.artist,
                                  style: TextStyle(
                                    fontSize: 11,
                                    color: colorScheme.onSurfaceVariant,
                                  ),
                                  maxLines: 1,
                                  overflow: TextOverflow.ellipsis,
                                )
                              : null,
                          trailing: durationStr.isNotEmpty
                              ? Text(
                                  durationStr,
                                  style: TextStyle(
                                    fontSize: 12,
                                    color: colorScheme.onSurfaceVariant,
                                  ),
                                )
                              : null,
                        );
                      },
                    ),
                  ),
                  const SizedBox(height: 16),
                  SizedBox(
                    width: double.infinity,
                    child: FilledButton.icon(
                      onPressed: () {
                        Navigator.pop(sheetContext);
                        _confirmAndSplitCue(
                          context: this.context,
                          audioPath: audioPath,
                          album: album,
                          artist: artist,
                          genre: genre,
                          date: date,
                          tracks: tracks,
                        );
                      },
                      icon: const Icon(Icons.call_split),
                      label: Text(sheetContext.l10n.cueSplitButton),
                      style: FilledButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 16),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 8),
                ],
              ),
            ),
          );
        },
      );
    } catch (e) {
      if (!mounted) return;
      _hideCurrentSnackBar();
      _showSnackBarMessage(_l10nCueSplitFailed);
      _log.e('Failed to parse CUE sheet: $e');
    }
  }

  void _confirmAndSplitCue({
    required BuildContext context,
    required String audioPath,
    required String album,
    required String artist,
    required String genre,
    required String date,
    required List<CueSplitTrackInfo> tracks,
  }) {
    showDialog<void>(
      context: context,
      builder: (dialogContext) {
        return AlertDialog(
          title: Text(dialogContext.l10n.cueSplitConfirmTitle),
          content: Text(
            dialogContext.l10n.cueSplitConfirmMessage(album, tracks.length),
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(dialogContext),
              child: Text(dialogContext.l10n.dialogCancel),
            ),
            FilledButton(
              onPressed: () {
                Navigator.pop(dialogContext);
                _performCueSplit(
                  audioPath: audioPath,
                  album: album,
                  artist: artist,
                  genre: genre,
                  date: date,
                  tracks: tracks,
                );
              },
              child: Text(dialogContext.l10n.cueSplitButton),
            ),
          ],
        );
      },
    );
  }

  Future<Directory> _resolvePersistentCueSplitOutputDir() async {
    final settings = ref.read(settingsProvider);
    final queueState = ref.read(downloadQueueProvider);
    final configuredOutputDir = queueState.outputDir.trim();
    if (settings.storageMode != 'saf' &&
        configuredOutputDir.isNotEmpty &&
        !isContentUri(configuredOutputDir)) {
      final dir = Directory(configuredOutputDir);
      await dir.create(recursive: true);
      return dir;
    }

    if (Platform.isAndroid) {
      final externalDir = await getExternalStorageDirectory();
      if (externalDir != null) {
        final musicDir = Directory(
          '${externalDir.parent.parent.parent.parent.path}'
          '${Platform.pathSeparator}Music'
          '${Platform.pathSeparator}SpotiFLAC',
        );
        await musicDir.create(recursive: true);
        return musicDir;
      }
    }

    final docsDir = await getApplicationDocumentsDirectory();
    final fallbackDir = Directory(
      '${docsDir.path}${Platform.pathSeparator}SpotiFLAC',
    );
    await fallbackDir.create(recursive: true);
    return fallbackDir;
  }

  Future<List<String>?> _exportCueSplitOutputsToSaf({
    required List<String> outputPaths,
    required String treeUri,
    required String relativeDir,
  }) async {
    final exportedUris = <String>[];
    for (final path in outputPaths) {
      final fileName = path.split(Platform.pathSeparator).last;
      final safUri = await PlatformBridge.createSafFileFromPath(
        treeUri: treeUri,
        relativeDir: relativeDir,
        fileName: fileName,
        mimeType: audioMimeTypeForPath(path),
        srcPath: path,
      );
      if (safUri != null && safUri.isNotEmpty) {
        exportedUris.add(safUri);
      }
    }
    return exportedUris.isEmpty ? null : exportedUris;
  }

  Future<void> _performCueSplit({
    required String audioPath,
    required String album,
    required String artist,
    required String genre,
    required String date,
    required List<CueSplitTrackInfo> tracks,
  }) async {
    if (_isConverting) return;
    _setState(() => _isConverting = true);

    String? safTempAudioPath;
    Directory? tempSplitDir;
    try {
      // For SAF content:// audio paths, copy to temp for FFmpeg processing
      String workingAudioPath = audioPath;
      final isSafSource = isContentUri(audioPath);
      if (isSafSource) {
        final tempPath = await PlatformBridge.copyContentUriToTemp(audioPath);
        if (tempPath == null || tempPath.isEmpty) {
          throw Exception('Failed to copy SAF audio file to temp');
        }
        safTempAudioPath = tempPath;
        workingAudioPath = tempPath;
      }

      final String outputDir;
      final treeUri = !_isLocalItem
          ? (_downloadItem?.downloadTreeUri ?? '')
          : '';
      final relativeDir = !_isLocalItem
          ? (_downloadItem?.safRelativeDir ?? '')
          : '';
      final writeBackToSaf = isSafSource && treeUri.isNotEmpty;
      if (writeBackToSaf) {
        final tempDir = await getTemporaryDirectory();
        tempSplitDir = Directory(
          '${tempDir.path}${Platform.pathSeparator}'
          'cue_split_${DateTime.now().millisecondsSinceEpoch}',
        );
        await tempSplitDir.create(recursive: true);
        outputDir = tempSplitDir.path;
      } else if (isSafSource) {
        final persistentDir = await _resolvePersistentCueSplitOutputDir();
        outputDir = persistentDir.path;
      } else {
        outputDir = File(audioPath).parent.path;
      }

      if (!mounted) return;
      _showLongSnackBarMessage(_l10nCueSplitSplitting(1, tracks.length));

      String? coverPath;
      try {
        final tempDir = await getTemporaryDirectory();
        final coverOutput =
            '${tempDir.path}${Platform.pathSeparator}cue_cover_${DateTime.now().millisecondsSinceEpoch}.jpg';
        final coverResult = await PlatformBridge.extractCoverToFile(
          workingAudioPath,
          coverOutput,
        );
        if (coverResult['error'] == null) {
          coverPath = coverOutput;
        }
      } catch (_) {}

      final albumMetadata = <String, String>{
        'artist': artist,
        'album': album,
        'genre': genre,
        'date': date,
      };

      final outputPaths = await FFmpegService.splitCueToTracks(
        audioPath: workingAudioPath,
        outputDir: outputDir,
        tracks: tracks,
        albumMetadata: albumMetadata,
        coverPath: coverPath,
        onProgress: (current, total) {
          if (mounted) {
            _hideCurrentSnackBar();
            _showLongSnackBarMessage(_l10nCueSplitSplitting(current, total));
          }
        },
      );

      var finalOutputPaths = outputPaths;

      if (coverPath != null && finalOutputPaths != null) {
        for (final path in finalOutputPaths) {
          if (path.toLowerCase().endsWith('.flac')) {
            try {
              // Only send the cover_path field — EditFlacFields uses
              // field-presence semantics, so omitting artist/album_artist
              // means those keys won't be rewritten.  This preserves any
              // existing split artist Vorbis Comments.
              await PlatformBridge.editFileMetadata(path, {
                'cover_path': coverPath,
              });
            } catch (e) {
              _log.w('Failed to embed cover to split track: $e');
            }
          }
        }
      }

      if (writeBackToSaf && finalOutputPaths != null) {
        final exportedUris = await _exportCueSplitOutputsToSaf(
          outputPaths: finalOutputPaths,
          treeUri: treeUri,
          relativeDir: relativeDir,
        );
        finalOutputPaths = exportedUris;
      }

      if (coverPath != null) {
        try {
          await File(coverPath).delete();
        } catch (_) {}
      }

      if (mounted) {
        _hideCurrentSnackBar();
        if (finalOutputPaths != null && finalOutputPaths.isNotEmpty) {
          _showSnackBarMessage(_l10nCueSplitSuccess(finalOutputPaths.length));
        } else {
          _showSnackBarMessage(_l10nCueSplitFailed);
        }
      }
    } catch (e) {
      _log.e('CUE split failed: $e');
      if (mounted) {
        _hideCurrentSnackBar();
        _showSnackBarMessage(_l10nCueSplitFailed);
      }
    } finally {
      if (safTempAudioPath != null) {
        try {
          await File(safTempAudioPath).delete();
        } catch (_) {}
      }
      if (tempSplitDir != null) {
        try {
          await tempSplitDir.delete(recursive: true);
        } catch (_) {}
      }
      if (mounted) {
        _setState(() => _isConverting = false);
      }
    }
  }

  void _confirmAndConvert({
    required BuildContext context,
    required String sourceFormat,
    required String targetFormat,
    required String bitrate,
    LosslessConversionQuality losslessQuality =
        const LosslessConversionQuality(),
    LosslessConversionProcessing losslessProcessing =
        const LosslessConversionProcessing(),
    bool keepOriginal = false,
  }) {
    final isLossless = isLosslessConversionTarget(targetFormat);
    showDialog<void>(
      context: context,
      builder: (dialogContext) {
        return AlertDialog(
          title: Text(dialogContext.l10n.trackConvertConfirmTitle),
          content: Text(
            keepOriginal
                ? dialogContext.l10n.trackConvertConfirmKeepOriginal(
                    sourceFormat,
                    targetFormat,
                  )
                : isLossless && losslessQuality.hasCaps
                ? dialogContext.l10n.trackConvertConfirmMessageLosslessCapped(
                    sourceFormat,
                    targetFormat,
                    losslessQualityLabel(
                      losslessQuality,
                      originalLabel:
                          dialogContext.l10n.losslessConversionLabels.original,
                      originalQualityLabel: dialogContext
                          .l10n
                          .losslessConversionLabels
                          .originalQuality,
                    ),
                  )
                : isLossless
                ? dialogContext.l10n.trackConvertConfirmMessageLossless(
                    sourceFormat,
                    targetFormat,
                  )
                : dialogContext.l10n.trackConvertConfirmMessage(
                    sourceFormat,
                    targetFormat,
                    bitrate,
                  ),
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(dialogContext),
              child: Text(dialogContext.l10n.dialogCancel),
            ),
            FilledButton(
              onPressed: () {
                Navigator.pop(dialogContext);
                _performConversion(
                  targetFormat: targetFormat,
                  bitrate: bitrate,
                  losslessQuality: losslessQuality,
                  losslessProcessing: losslessProcessing,
                  keepOriginal: keepOriginal,
                );
              },
              child: Text(dialogContext.l10n.trackConvertFormat),
            ),
          ],
        );
      },
    );
  }

  Future<void> _performConversion({
    required String targetFormat,
    required String bitrate,
    LosslessConversionQuality losslessQuality =
        const LosslessConversionQuality(),
    LosslessConversionProcessing losslessProcessing =
        const LosslessConversionProcessing(),
    bool keepOriginal = false,
  }) async {
    if (_isConverting) return;
    _setState(() => _isConverting = true);
    final losslessLabels = context.l10n.losslessConversionLabels;
    String? coverPath;
    String? safTempPath;
    String? convertedSafTempPath;

    try {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(context.l10n.trackConvertConverting)),
      );

      final settings = ref.read(settingsProvider);
      final shouldEmbedLyrics =
          settings.embedLyrics && settings.lyricsMode != 'external';
      final metadata = _buildFallbackMetadata();
      try {
        final result = await PlatformBridge.readFileMetadata(cleanFilePath);
        if (result['error'] == null) {
          mergePlatformMetadataForTagEmbed(target: metadata, source: result);
        } else {
          _log.w('readFileMetadata returned error, using fallback metadata');
        }
      } catch (e) {
        _log.w('readFileMetadata threw, using fallback metadata: $e');
      }
      await ensureLyricsMetadataForConversion(
        metadata: metadata,
        sourcePath: cleanFilePath,
        shouldEmbedLyrics: shouldEmbedLyrics,
        trackName: trackName,
        artistName: artistName,
        spotifyId: _spotifyId ?? '',
        durationMs: (duration ?? 0) * 1000,
      );

      try {
        final tempDir = await getTemporaryDirectory();
        final coverOutput =
            '${tempDir.path}${Platform.pathSeparator}convert_cover_${DateTime.now().millisecondsSinceEpoch}.jpg';
        final coverResult = await PlatformBridge.extractCoverToFile(
          cleanFilePath,
          coverOutput,
        );
        if (coverResult['error'] == null) {
          coverPath = coverOutput;
        }
      } catch (_) {}

      String workingPath = cleanFilePath;
      final isSaf = _isSafFile;

      if (isSaf) {
        safTempPath = await ConversionLibraryService.copySafSourceToTemp(
          cleanFilePath,
        );
        if (safTempPath == null) {
          if (mounted) {
            _setState(() => _isConverting = false);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(context.l10n.trackConvertFailed)),
            );
          }
          return;
        }
        workingPath = safTempPath;
      }

      final newPath = await FFmpegService.convertAudioFormat(
        inputPath: workingPath,
        targetFormat: targetFormat.toLowerCase(),
        bitrate: bitrate,
        metadata: metadata,
        coverPath: coverPath,
        artistTagMode: ref.read(settingsProvider).artistTagMode,
        deleteOriginal: !isSaf && !keepOriginal,
        sourceBitDepth: bitDepth,
        losslessQuality: losslessQuality,
        losslessProcessing: losslessProcessing,
      );

      if (coverPath != null) {
        try {
          await File(coverPath).delete();
        } catch (_) {}
      }

      if (newPath == null) {
        if (safTempPath != null) {
          try {
            await File(safTempPath).delete();
          } catch (_) {}
        }
        if (mounted) {
          _setState(() => _isConverting = false);
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(context.l10n.trackConvertFailed)),
          );
        }
        return;
      }
      if (isSaf) convertedSafTempPath = newPath;

      final isLosslessOutput = isLosslessConversionTarget(targetFormat);
      int? convertedBitDepth;
      int? convertedSampleRate;
      if (isLosslessOutput) {
        try {
          final convertedMetadata = await PlatformBridge.readFileMetadata(
            newPath,
          );
          if (convertedMetadata['error'] == null) {
            convertedBitDepth = readPositiveInt(convertedMetadata['bit_depth']);
            convertedSampleRate = readPositiveInt(
              convertedMetadata['sample_rate'],
            );
          }
        } catch (_) {}
        convertedBitDepth ??= losslessQuality.effectiveBitDepth(bitDepth);
        convertedSampleRate ??= losslessQuality.effectiveSampleRate(sampleRate);
      }
      final newQuality = convertedAudioQualityLabel(
        targetFormat: targetFormat,
        bitrate: bitrate,
        labels: losslessLabels,
        losslessQuality: losslessQuality,
        actualBitDepth: convertedBitDepth,
        actualSampleRate: convertedSampleRate,
      );

      if (isSaf) {
        String? treeUri;
        String relativeDir = '';
        String oldFileName = '';
        if (_isLocalItem) {
          final uri = Uri.parse(cleanFilePath);
          final pathSegments = uri.pathSegments;
          final treeIdx = pathSegments.indexOf('tree');
          final docIdx = pathSegments.indexOf('document');
          if (treeIdx >= 0 && treeIdx + 1 < pathSegments.length) {
            final treeId = pathSegments[treeIdx + 1];
            treeUri =
                'content://${uri.authority}/tree/${Uri.encodeComponent(treeId)}';
          }
          if (docIdx >= 0 && docIdx + 1 < pathSegments.length) {
            final docPath = Uri.decodeFull(pathSegments[docIdx + 1]);
            final slashIdx = docPath.lastIndexOf('/');
            if (slashIdx >= 0) {
              oldFileName = docPath.substring(slashIdx + 1);
              final treeId = treeIdx >= 0 && treeIdx + 1 < pathSegments.length
                  ? Uri.decodeFull(pathSegments[treeIdx + 1])
                  : '';
              if (treeId.isNotEmpty && docPath.startsWith(treeId)) {
                final afterTree = docPath.substring(treeId.length);
                final trimmed = afterTree.startsWith('/')
                    ? afterTree.substring(1)
                    : afterTree;
                final lastSlash = trimmed.lastIndexOf('/');
                relativeDir = lastSlash >= 0
                    ? trimmed.substring(0, lastSlash)
                    : '';
              }
            } else {
              oldFileName = docPath;
            }
          }
        } else {
          treeUri = _downloadItem?.downloadTreeUri;
          relativeDir = _downloadItem?.safRelativeDir ?? '';
          oldFileName =
              (_downloadItem?.safFileName != null &&
                  _downloadItem!.safFileName!.isNotEmpty)
              ? _downloadItem!.safFileName!
              : _extractFileNameFromPathOrUri(cleanFilePath);
        }
        if (treeUri == null || treeUri.isEmpty) {
          try {
            await File(newPath).delete();
          } catch (_) {}
          if (safTempPath != null) {
            try {
              await File(safTempPath).delete();
            } catch (_) {}
          }
          if (mounted) {
            _setState(() => _isConverting = false);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(context.l10n.trackConvertFailed)),
            );
          }
          return;
        }

        final published = await ConversionLibraryService.publishSafConversion(
          treeUri: treeUri,
          relativeDir: relativeDir,
          originalFileName: oldFileName,
          targetFormat: targetFormat,
          sourcePath: newPath,
          keepOriginal: keepOriginal,
        );
        final safUri = published?.uri;

        if (safUri == null || safUri.isEmpty) {
          try {
            await File(newPath).delete();
          } catch (_) {}
          if (safTempPath != null) {
            try {
              await File(safTempPath).delete();
            } catch (_) {}
          }
          if (mounted) {
            _setState(() => _isConverting = false);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(context.l10n.trackConvertFailed)),
            );
          }
          return;
        }

        if (!keepOriginal && !isSameContentUri(cleanFilePath, safUri)) {
          final deletedOriginal = await PlatformBridge.safDelete(
            cleanFilePath,
          ).catchError((_) => false);
          if (deletedOriginal != true) {
            _log.w(
              'Converted SAF file created but failed deleting original URI',
            );
          }
        }

        if (!_isLocalItem) {
          await ConversionLibraryService.persistHistoryConversion(
            source: _downloadItem!,
            newFilePath: safUri,
            newQuality: newQuality,
            targetFormat: targetFormat,
            bitrate: bitrate,
            bitDepth: convertedBitDepth,
            sampleRate: convertedSampleRate,
            keepOriginal: keepOriginal,
            newSafFileName: published!.fileName,
          );
          await ref.read(downloadHistoryProvider.notifier).reloadFromStorage();
        } else {
          await LibraryDatabase.instance.replaceWithConvertedItem(
            item: _localLibraryItem!,
            newFilePath: safUri,
            targetFormat: targetFormat,
            bitrate: bitrate,
            bitDepth: convertedBitDepth,
            sampleRate: convertedSampleRate,
            keepOriginal: keepOriginal,
          );
          await ref.read(localLibraryProvider.notifier).reloadFromStorage();
        }

        try {
          await File(newPath).delete();
        } catch (_) {}
        if (safTempPath != null) {
          try {
            await File(safTempPath).delete();
          } catch (_) {}
        }
      } else {
        if (!_isLocalItem) {
          await ConversionLibraryService.persistHistoryConversion(
            source: _downloadItem!,
            newFilePath: newPath,
            newQuality: newQuality,
            targetFormat: targetFormat,
            bitrate: bitrate,
            bitDepth: convertedBitDepth,
            sampleRate: convertedSampleRate,
            keepOriginal: keepOriginal,
          );
          await ref.read(downloadHistoryProvider.notifier).reloadFromStorage();
        } else {
          await LibraryDatabase.instance.replaceWithConvertedItem(
            item: _localLibraryItem!,
            newFilePath: newPath,
            targetFormat: targetFormat,
            bitrate: bitrate,
            bitDepth: convertedBitDepth,
            sampleRate: convertedSampleRate,
            keepOriginal: keepOriginal,
          );
          await ref.read(localLibraryProvider.notifier).reloadFromStorage();
        }
      }

      if (mounted) {
        _setState(() => _isConverting = false);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(context.l10n.trackConvertSuccess(targetFormat)),
          ),
        );
        Navigator.pop(context, true);
      }
    } catch (e) {
      if (mounted) {
        _setState(() => _isConverting = false);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              context.l10n.trackSaveFailed(context.friendlyError(e)),
            ),
          ),
        );
      }
    } finally {
      for (final path in <String?>[
        coverPath,
        safTempPath,
        convertedSafTempPath,
      ]) {
        if (path == null || path.isEmpty) continue;
        try {
          final file = File(path);
          if (await file.exists()) await file.delete();
        } catch (error) {
          _log.w('Failed to clean conversion temp file $path: $error');
        }
      }
    }
  }
}
