import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';

import 'package:namico_db_wrapper/namico_db_wrapper.dart';

import 'package:namida/class/file_parts.dart';
import 'package:namida/class/track.dart';
import 'package:namida/class/video.dart';
import 'package:namida/controller/history_controller.dart';
import 'package:namida/controller/indexer_controller.dart';
import 'package:namida/controller/navigator_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/controller/time_ago_controller.dart';
import 'package:namida/controller/video_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/enums.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/icon_fonts/broken_icons.dart';
import 'package:namida/core/translations/language.dart';
import 'package:namida/core/utils.dart';
import 'package:namida/ui/widgets/custom_widgets.dart';
import 'package:namida/youtube/controller/youtube_history_controller.dart';
import 'package:namida/youtube/controller/youtube_info_controller.dart';
import 'package:namida/youtube/widgets/yt_thumbnail.dart';

enum _CacheSorting { recommended, size, listenCount, accessTime }

class StorageCacheManager {
  const StorageCacheManager();

  Future<void> trimExtraFiles() async {
    final priorityDbFileInfo = VideoController.inst.videosPriorityManager.cacheVideosPriorityDB.fileInfo;
    await Future.wait([
      _VideoTrimmer()._trimExcessVideoCache(priorityDbFileInfo),
      _ImageTrimmer()._trimExcessImageCache(priorityDbFileInfo),
      _ImageTrimmer()._trimExcessImageCacheTemp(priorityDbFileInfo),
      _AudioTrimmer()._trimExcessAudioCache(priorityDbFileInfo),
    ]);
  }

  Future<int> getTempVideosSize() async {
    return _AudioVideoTrimmer._getTempFilesSizeIsolate.thready({'temp': AppDirs.VIDEOS_CACHE_TEMP, 'normal': AppDirs.VIDEOS_CACHE});
  }

  Future<void> deleteTempVideos() async {
    return _AudioVideoTrimmer._deleteTempFilesIsolate.thready({'temp': AppDirs.VIDEOS_CACHE_TEMP, 'normal': AppDirs.VIDEOS_CACHE});
  }

  Future<void> deleteAllVideos() async {
    await Directory(AppDirs.VIDEOS_CACHE).delete(recursive: true).catchError((_) => Directory(''));
    await Directory(AppDirs.VIDEOS_CACHE_TEMP).delete(recursive: true).catchError((_) => Directory(''));

    await Directory(AppDirs.VIDEOS_CACHE).create(recursive: true);
    await Directory(AppDirs.VIDEOS_CACHE_TEMP).create(recursive: true);
  }

  Future<int> getTempAudiosSize() async {
    return _AudioVideoTrimmer._getTempFilesSizeIsolate.thready({'normal': AppDirs.AUDIOS_CACHE});
  }

  Future<void> deleteTempAudios() async {
    return _AudioVideoTrimmer._deleteTempFilesIsolate.thready({'normal': AppDirs.AUDIOS_CACHE});
  }

  Future<void> deleteMediaFilesThatAlreadyExistsInLocalLibrary(Map<String, List<Track>> idsMap, {required bool forVideos}) async {
    final dirPath = forVideos ? AppDirs.VIDEOS_CACHE : AppDirs.AUDIOS_CACHE;
    return _AudioVideoTrimmer._deleteAudioFilesThatAlreadyExistsIsolate.thready({
      'normal': dirPath,
      'idsMap': idsMap,
      'forVideos': forVideos,
    });
  }

  Future<(List<File>, int)> countMediaFilesThatAlreadyExistsInLocalLibrary(Map<String, List<Track>> idsMap, {required bool forVideos}) async {
    final dirPath = forVideos ? AppDirs.VIDEOS_CACHE : AppDirs.AUDIOS_CACHE;
    return _AudioVideoTrimmer._countAudioFilesThatAlreadyExistsIsolate.thready({
      'normal': dirPath,
      'idsMap': idsMap,
      'forVideos': forVideos,
    });
  }

  Future<void> deleteAllAudios() async {
    await Directory(AppDirs.AUDIOS_CACHE).delete(recursive: true).catchError((_) => Directory(''));
    await Directory(AppDirs.AUDIOS_CACHE).create(recursive: true);
  }

  Future<Map<File, int>> getTempVideosForID(String videoId) async {
    return _VideoTrimmer._getTempVideosForID.thready({'id': videoId, 'temp': AppDirs.VIDEOS_CACHE_TEMP, 'normal': AppDirs.VIDEOS_CACHE});
  }

  Future<Map<File, int>> getTempAudiosForID(String videoId) async {
    return _AudioTrimmer._getTempAudiosForID.thready({'id': videoId, 'dirPath': AppDirs.AUDIOS_CACHE});
  }

  String getDeleteSizeSubtitleText(int length, int totalSize) {
    return lang.deleteFileCacheSubtitle(count: length, totalSizeText: totalSize.fileSizeFormatted);
  }

  void promptCacheDeleteDialog<T>({
    required List<T> allItems,
    required String Function(List<T> items) deleteStatsNote,
    required String chooseNote,
    required void Function() onChoosePrompt,
    required Future<void> Function() onDeleteEVERYTHING,
  }) {
    /// First Dialog
    NamidaNavigator.inst.navigateDialog(
      dialog: CustomBlurryDialog(
        isWarning: true,
        normalTitleStyle: true,
        bodyText: "${deleteStatsNote(allItems)}\n$chooseNote",
        actions: [
          /// Pressing Choose
          NamidaButton(
            text: lang.choose,
            onTap: () {
              NamidaNavigator.inst.closeDialog();
              onChoosePrompt();
            },
          ),
          const CancelButton(),
          NamidaButton(
            colorScheme: Colors.red,
            text: lang.delete.toUpperCase(),
            onTap: () async {
              NamidaNavigator.inst.closeDialog();
              await onDeleteEVERYTHING();
            },
          ),
        ],
      ),
    );
  }

  void showChooseToDeleteDialog<T>({
    required List<T> allItems,
    required String Function(T item) itemToPath,
    required String? Function(T item) itemToYtId,
    required String Function(T item, int itemSize) itemToSubtitle,
    required String Function(int length, int totalSize) confirmDialogText,
    required Future<void> Function(Iterable<T> itemsToDelete) onDeleteFiles,
    required Future<int> Function() tempFilesSize,
    required Future<void> Function() onDeleteTempFiles,
    bool includeLocalTracksListens = true,
    required bool forVideos,
  }) async {
    final itemsToDelete = <T>{}.obs;
    final itemsToDeleteSize = 0.obs;
    final allFiles = allItems.obs;
    final allItemsByPaths = {for (final e in allFiles.value) itemToPath(e): e};

    final deleteTempFiles = false.obs;
    final tempFilesSizeFinal = 0.obs;
    final deleteAlreadyInLocalLibraryFiles = false.obs;
    final alreadyInLocalLibraryFilesSizeFinal = (<File>[], 0).obs;
    tempFilesSize().then((value) => tempFilesSizeFinal.value = value);

    countMediaFilesThatAlreadyExistsInLocalLibrary(
      Indexer.inst.allTracksMappedByYTID,
      forVideos: forVideos,
    ).then(
      (countAndSize) {
        alreadyInLocalLibraryFilesSizeFinal.value = countAndSize;
      },
    );

    final currentSort = _CacheSorting.recommended.obs;

    final localIdTrackMap = includeLocalTracksListens ? Indexer.inst.allTracksMappedByYTID : <String, List<Track>>{};

    int getTotalListensForIDLength(String id) {
      final correspondingTracks = localIdTrackMap[id];
      int localCount = 0;
      int ytCount = 0;
      if (correspondingTracks != null && correspondingTracks.isNotEmpty) {
        for (final t in correspondingTracks) {
          localCount += HistoryController.inst.topTracksMapListens.value[t]?.length ?? 0;
        }
      }
      ytCount = YoutubeHistoryController.inst.topTracksMapListens.value[id]?.length ?? 0;
      return localCount + ytCount;
    }

    final listensMap = <String?, int>{};
    final sizesMap = <String, int>{};
    final accessTimeMap = <String, int>{};
    int maxListenCount = 0;

    final int length = allFiles.value.length;
    for (int i = 0; i < length; i++) {
      var e = allFiles.value[i];

      final path = itemToPath(e);
      final stats = await File(path).stat();
      final accessed = stats.accessed.millisecondsSinceEpoch;
      final modified = stats.modified.millisecondsSinceEpoch;
      final finalMS = modified > accessed ? modified : accessed;
      sizesMap[path] = stats.size;
      accessTimeMap[path] = finalMS;

      final videoId = itemToYtId(e);
      if (videoId != null) {
        final listensCount = getTotalListensForIDLength(videoId);
        listensMap[videoId] = listensCount;
        if (maxListenCount < listensCount) maxListenCount = listensCount;
      }
    }

    void sortBy(_CacheSorting type) {
      currentSort.value = type;
      switch (type) {
        case _CacheSorting.recommended:
          final maxLastAccess = DateTime.now().millisecondsSinceEpoch;
          allFiles.sortBy((e) {
            final accessTime = accessTimeMap[itemToPath(e)] ?? 0;
            final listenCount = listensMap[itemToYtId(e)] ?? 0;
            final normalizedAccessTime = (accessTime / maxLastAccess);
            final normalizedListenCount = (listenCount / maxListenCount);
            return normalizedAccessTime * 0.3 + normalizedListenCount * 0.7;
          });
        case _CacheSorting.size:
          allFiles.sortByReverse((e) => sizesMap[itemToPath(e)] ?? 0);
        case _CacheSorting.accessTime:
          allFiles.sortBy((e) => accessTimeMap[itemToPath(e)] ?? 0);
        case _CacheSorting.listenCount:
          allFiles.sortBy((e) => listensMap[itemToYtId(e)] ?? 0);
      }
    }

    Widget getChipButton({
      required _CacheSorting sort,
      required String title,
      required IconData icon,
      required bool Function(_CacheSorting sort) enabled,
    }) {
      return NamidaInkWell(
        animationDurationMS: 100,
        borderRadius: 8.0,
        bgColor: namida.theme.cardTheme.color,
        padding: const EdgeInsets.all(8.0),
        decoration: BoxDecoration(
          border: enabled(sort) ? Border.all(color: namida.theme.colorScheme.primary) : null,
          borderRadius: BorderRadius.circular(8.0.multipliedRadius),
        ),
        onTap: () => sortBy(sort),
        child: Row(
          children: [
            Icon(icon, size: 18.0),
            const SizedBox(width: 4.0),
            Text(
              title,
              style: namida.textTheme.displayMedium,
            ),
            const SizedBox(width: 4.0),
            const Icon(Broken.arrow_down_2, size: 14.0),
          ],
        ),
      );
    }

    sortBy(currentSort.value);

    NamidaNavigator.inst.navigateDialog(
      onDisposing: () {
        itemsToDelete.close();
        deleteTempFiles.close();
        tempFilesSizeFinal.close();
        deleteAlreadyInLocalLibraryFiles.close();
        alreadyInLocalLibraryFilesSizeFinal.close();
        allFiles.close();
        currentSort.close();
      },
      dialog: CustomBlurryDialog(
        horizontalInset: 24.0,
        contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
        isWarning: true,
        normalTitleStyle: true,
        title: lang.choose,
        actions: [
          const CancelButton(),

          /// Clear after choosing
          Obx(
            (context) => NamidaButton(
              colorScheme: Colors.red,
              enabled: itemsToDeleteSize.valueR > 0 || itemsToDelete.valueR.isNotEmpty,
              text: "${lang.delete.toUpperCase()} (${itemsToDeleteSize.valueR.fileSizeFormatted})",
              onTap: () async {
                final hasTemp = deleteTempFiles.value && tempFilesSizeFinal.value > 0;
                final finalItemsToDeleteOnlySize = itemsToDeleteSize.value - (hasTemp ? tempFilesSizeFinal.value : 0);
                final firstLine = itemsToDelete.value.isNotEmpty || finalItemsToDeleteOnlySize > 0 ? confirmDialogText(itemsToDelete.value.length, finalItemsToDeleteOnlySize) : '';
                final tempFilesLine = hasTemp ? "${lang.deleteTempFiles} (${tempFilesSizeFinal.value.fileSizeFormatted})?" : '';
                NamidaNavigator.inst.navigateDialog(
                  dialog: CustomBlurryDialog(
                    isWarning: true,
                    normalTitleStyle: true,
                    actions: [
                      const CancelButton(),

                      /// final clear confirm
                      NamidaButton(
                        colorScheme: Colors.red,
                        text: lang.delete.toUpperCase(),
                        onTap: () async {
                          NamidaNavigator.inst.closeDialog(2);
                          onDeleteFiles(itemsToDelete.value);
                          if (hasTemp) onDeleteTempFiles();
                        },
                      ),
                    ],
                    bodyText: [
                      firstLine,
                      tempFilesLine,
                    ].joinText(separator: '\n'),
                  ),
                );
              },
            ),
          ),
        ],
        child: SizedBox(
          width: namida.width,
          height: namida.height * 0.65,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const SizedBox(height: 12.0),
              SmoothSingleChildScrollView(
                scrollDirection: Axis.horizontal,
                child: ObxO(
                  rx: currentSort,
                  builder: (context, currentSort) => Row(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: [
                      const SizedBox(width: 24.0),
                      getChipButton(
                        sort: _CacheSorting.recommended,
                        title: lang.auto,
                        icon: Broken.magic_star,
                        enabled: (sort) => sort == currentSort,
                      ),
                      const SizedBox(width: 12.0),
                      getChipButton(
                        sort: _CacheSorting.size,
                        title: lang.size,
                        icon: Broken.size,
                        enabled: (sort) => sort == currentSort,
                      ),
                      const SizedBox(width: 12.0),
                      getChipButton(
                        sort: _CacheSorting.accessTime,
                        title: lang.oldestWatch,
                        icon: Broken.sort,
                        enabled: (sort) => sort == currentSort,
                      ),
                      const SizedBox(width: 12.0),
                      getChipButton(
                        sort: _CacheSorting.listenCount,
                        title: lang.totalListens,
                        icon: Broken.math,
                        enabled: (sort) => sort == currentSort,
                      ),
                      const SizedBox(width: 24.0),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 6.0),
              Expanded(
                child: NamidaScrollbarWithController(
                  child: (sc) => ObxO(
                    rx: allFiles,
                    builder: (context, allFiles) => ObxO(
                      rx: itemsToDelete,
                      builder: (context, toDelete) {
                        final theme = context.theme;
                        final textTheme = theme.textTheme;
                        return SuperSmoothListView.builder(
                          controller: sc,
                          padding: EdgeInsets.zero,
                          itemCount: allFiles.length,
                          itemBuilder: (context, index) {
                            final item = allFiles[index];
                            final id = itemToYtId(item);
                            final listens = id == null ? null : listensMap[id];
                            final itemSize = sizesMap[itemToPath(item)] ?? 0;
                            String? lastPlayedTimeText;
                            if (currentSort.value == _CacheSorting.accessTime || currentSort.value == _CacheSorting.recommended) {
                              final accessTime = accessTimeMap[itemToPath(item)];
                              if (accessTime != null) lastPlayedTimeText = TimeAgoController.dateMSSEFromNow(accessTime);
                            }
                            final isSelected = toDelete.contains(item);
                            return NamidaInkWell(
                              animationDurationMS: 200,
                              margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
                              padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
                              decoration: BoxDecoration(
                                border: isSelected
                                    ? Border.all(
                                        color: theme.colorScheme.secondary.withOpacityExt(0.5),
                                        width: 2.0,
                                      )
                                    : null,
                              ),
                              onTap: () {
                                final didRemove = itemsToDelete.value.remove(item);
                                if (didRemove) {
                                  itemsToDeleteSize.value -= itemSize;
                                } else {
                                  itemsToDelete.value.add(item);
                                  itemsToDeleteSize.value += itemSize;
                                }
                                itemsToDelete.refresh();
                              },
                              child: Row(
                                children: [
                                  YoutubeThumbnail(
                                    key: Key(id ?? ''),
                                    type: ThumbnailType.video,
                                    videoId: id,
                                    borderRadius: 8.0,
                                    iconSize: 24.0,
                                    width: 92.0,
                                    height: 92 * 9 / 16,
                                    forceSquared: true,
                                    isImportantInCache: false,
                                  ),
                                  const SizedBox(width: 8.0),
                                  Expanded(
                                    child: Column(
                                      crossAxisAlignment: CrossAxisAlignment.start,
                                      children: [
                                        _VideoIdToTitleWidget(
                                          id: id ?? '',
                                          style: textTheme.displayMedium,
                                        ),
                                        Text(
                                          itemToSubtitle(item, itemSize),
                                          style: textTheme.displaySmall,
                                        ),
                                        if (lastPlayedTimeText != null)
                                          Text(
                                            lastPlayedTimeText,
                                            style: textTheme.displaySmall,
                                          ),
                                      ],
                                    ),
                                  ),
                                  const SizedBox(width: 8.0),
                                  if (listens != null && listens > 0) ...[
                                    Text(
                                      listens.toString(),
                                      style: textTheme.displaySmall,
                                    ),
                                    const SizedBox(width: 8.0),
                                  ],
                                  IgnorePointer(
                                    child: NamidaCheckMark(
                                      size: 16.0,
                                      active: isSelected,
                                    ),
                                  ),
                                ],
                              ),
                            );
                          },
                        );
                      },
                    ),
                  ),
                ),
              ),
              ObxO(
                rx: tempFilesSizeFinal,
                builder: (context, tempfs) => tempfs > 0
                    ? Padding(
                        padding: const EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0),
                        child: ObxO(
                          rx: deleteTempFiles,
                          builder: (context, deleteTempf) => AnimatedOpacity(
                            duration: const Duration(milliseconds: 200),
                            opacity: deleteTempf ? 1.0 : 0.6,
                            child: NamidaInkWell(
                              borderRadius: 6.0,
                              bgColor: namida.theme.cardColor,
                              padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
                              onTap: () {
                                deleteTempFiles.toggle();
                                if (deleteTempFiles.value) {
                                  itemsToDeleteSize.value += tempFilesSizeFinal.value;
                                } else {
                                  itemsToDeleteSize.value -= tempFilesSizeFinal.value;
                                }
                              },
                              child: Row(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  NamidaCheckMark(
                                    size: 12.0,
                                    active: deleteTempf,
                                  ),
                                  const SizedBox(width: 8.0),
                                  Text(
                                    '${lang.deleteTempFiles} (${tempfs.fileSizeFormatted})',
                                    style: namida.textTheme.displaySmall,
                                  ),
                                ],
                              ),
                            ),
                          ),
                        ),
                      )
                    : const SizedBox(),
              ),
              ObxO(
                rx: alreadyInLocalLibraryFilesSizeFinal,
                builder: (context, alrInLocalLib) => alrInLocalLib.$1.isNotEmpty
                    ? Padding(
                        padding: const EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0),
                        child: ObxO(
                          rx: deleteAlreadyInLocalLibraryFiles,
                          builder: (context, deleteAlrInLocalLib) => AnimatedOpacity(
                            duration: const Duration(milliseconds: 200),
                            opacity: deleteAlrInLocalLib ? 1.0 : 0.6,
                            child: NamidaInkWell(
                              borderRadius: 6.0,
                              bgColor: namida.theme.cardColor,
                              padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
                              onTap: () {
                                deleteAlreadyInLocalLibraryFiles.toggle();

                                int effectiveSize = 0;

                                if (deleteAlreadyInLocalLibraryFiles.value) {
                                  for (final f in alreadyInLocalLibraryFilesSizeFinal.value.$1) {
                                    final item = allItemsByPaths[f.path];
                                    if (item != null) {
                                      final alreadySelected = itemsToDelete.value.contains(item);
                                      if (!alreadySelected) {
                                        effectiveSize += sizesMap[f.path] ?? 0;
                                        itemsToDelete.value.add(item);
                                      }
                                    }
                                  }
                                } else {
                                  for (final f in alreadyInLocalLibraryFilesSizeFinal.value.$1) {
                                    final item = allItemsByPaths[f.path];
                                    if (item != null) {
                                      final alreadySelected = itemsToDelete.value.contains(item);
                                      if (alreadySelected) {
                                        effectiveSize += sizesMap[f.path] ?? 0;
                                        itemsToDelete.value.remove(item);
                                      }
                                    }
                                  }
                                }

                                if (deleteAlreadyInLocalLibraryFiles.value) {
                                  itemsToDeleteSize.value += effectiveSize;
                                } else {
                                  itemsToDeleteSize.value -= effectiveSize;
                                }

                                itemsToDelete.refresh();
                              },
                              child: Row(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  NamidaCheckMark(
                                    size: 12.0,
                                    active: deleteAlrInLocalLib,
                                  ),
                                  const SizedBox(width: 8.0),
                                  Text(
                                    '${lang.selectFilesAlreadyInLocalLibrary} [${alrInLocalLib.$1.length}] (${alrInLocalLib.$2.fileSizeFormatted})',
                                    style: namida.textTheme.displaySmall,
                                  ),
                                ],
                              ),
                            ),
                          ),
                        ),
                      )
                    : const SizedBox(),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _VideoIdToTitleWidget extends StatefulWidget {
  final String? id;
  final TextStyle? style;

  const _VideoIdToTitleWidget({
    required this.id,
    required this.style,
  });

  @override
  State<_VideoIdToTitleWidget> createState() => _VideoIdToTitleWidgetState();
}

class _VideoIdToTitleWidgetState extends State<_VideoIdToTitleWidget> {
  String? _title;

  @override
  void initState() {
    super.initState();
    initValues();
  }

  void initValues() async {
    final id = widget.id;
    if (id == null) return;
    final newTitle = await YoutubeInfoController.utils.getVideoName(id);
    if (mounted) {
      if (newTitle != _title) {
        setState(() => _title = newTitle);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      _title ?? widget.id ?? '',
      style: widget.style,
    );
  }
}

class _VideoTrimmer {
  int get _videosMaxCacheInMB => settings.videosMaxCacheInMB.value;

  Future<int> _trimExcessVideoCache(DbWrapperFileInfo priorityDbInfo) async {
    final maxMB = _videosMaxCacheInMB;
    if (maxMB < 0) return 0;
    final totalMaxBytes = maxMB * 1024 * 1024;
    final paramters = _TrimDirParam(
      maxBytes: totalMaxBytes,
      dirPath: AppDirs.VIDEOS_CACHE,
      extraDirPath: AppDirs.VIDEOS_CACHE_TEMP,
      priorityDbInfo: priorityDbInfo,
    );
    return await _trimExcessVideoCacheIsolate.thready(paramters);
  }

  static Future<int> _trimExcessVideoCacheIsolate(_TrimDirParam params) async {
    final maxBytes = params.maxBytes;
    final dirPath = params.dirPath;
    final dirPathTemp = params.extraDirPath!;
    final priorityMap = await VideosPriorityManager.loadEverythingSync(params.priorityDbInfo);

    final videos = Directory(dirPath).listSyncSafe();
    final videosTemp = Directory(dirPathTemp).listSyncSafe();
    final videosFinal = [...videosTemp, ...videos];
    _Trimmer._sortFiles(videosFinal, priorityMap);
    return _Trimmer._trimExcessCache(videosFinal, maxBytes);
  }

  static Map<File, int> _getTempVideosForID(Map params) {
    final id = params['id'] as String;
    final tempDir = params['temp'] as String;
    final normalDir = params['normal'] as String;

    final sep = Platform.pathSeparator;

    final filesMap = <File, int>{};
    void checkFileAndAdd(FileSystemEntity e) {
      if (e is File) {
        final filename = e.path.splitLast(sep);
        if (filename.startsWith(id)) {
          filesMap[e] = e.fileSizeSync() ?? 0;
        }
      }
    }

    for (final e in Directory(tempDir).listSyncSafe()) {
      checkFileAndAdd(e);
    }
    for (final e in Directory(normalDir).listSyncSafe()) {
      if (e.path.endsWith('.part')) {
        checkFileAndAdd(e);
      }
    }
    return filesMap;
  }
}

class _AudioTrimmer {
  int get _audiosMaxCacheInMB => settings.audiosMaxCacheInMB.value;

  /// Returns total deleted bytes.
  Future<int> _trimExcessAudioCache(DbWrapperFileInfo priorityDbInfo) async {
    final maxMB = _audiosMaxCacheInMB;
    if (maxMB < 0) return 0;
    final totalMaxBytes = maxMB * 1024 * 1024;
    final paramters = _TrimDirParam(
      maxBytes: totalMaxBytes,
      dirPath: AppDirs.AUDIOS_CACHE,
      priorityDbInfo: priorityDbInfo,
    );
    return await _trimExcessAudioCacheIsolate.thready(paramters);
  }

  static Future<int> _trimExcessAudioCacheIsolate(_TrimDirParam params) async {
    final maxBytes = params.maxBytes;
    final dirPath = params.dirPath;
    final priorityMap = await VideosPriorityManager.loadEverythingSync(params.priorityDbInfo);

    final audios = Directory(dirPath).listSyncSafe();
    _Trimmer._sortFiles(audios, priorityMap);
    return _Trimmer._trimExcessCache(audios, maxBytes);
  }

  static Map<File, int> _getTempAudiosForID(Map params) {
    final id = params['id'] as String;
    final dirPath = params['dirPath'] as String;

    final sep = Platform.pathSeparator;

    final filesMap = <File, int>{};

    final files = Directory(dirPath).listSyncSafe();
    for (var e in files) {
      if (e.path.endsWith('.part')) {
        if (e is File) {
          final filename = e.path.splitLast(sep);
          if (filename.startsWith(id)) {
            filesMap[e] = e.fileSizeSync() ?? 0;
          }
        }
      }
    }
    return filesMap;
  }
}

class _ImageTrimmer {
  int get _imagesMaxCacheInMB => settings.imagesMaxCacheInMB.value;

  /// Returns total deleted bytes.
  Future<int> _trimExcessImageCache(DbWrapperFileInfo priorityDbInfo) async {
    final maxMB = _imagesMaxCacheInMB;
    if (maxMB < 0) return 0;
    final totalMaxBytes = maxMB * 1024 * 1024;
    final paramters = _TrimDirParam(
      maxBytes: totalMaxBytes,
      dirPath: AppDirs.YT_THUMBNAILS,
      extraDirPath: AppDirs.YT_THUMBNAILS_CHANNELS,
      priorityDbInfo: priorityDbInfo,
    );
    return await _trimExcessImageCacheIsolate.thready(paramters);
  }

  static Future<int> _trimExcessImageCacheIsolate(_TrimDirParam params) async {
    final maxBytes = params.maxBytes;
    final dirPath = params.dirPath;
    final dirPathChannel = params.extraDirPath!;
    final priorityMap = await VideosPriorityManager.loadEverythingSync(params.priorityDbInfo);

    final imagesVideos = Directory(dirPath).listSyncSafe();
    final imagesChannels = Directory(dirPathChannel).listSyncSafe();

    _Trimmer._sortFiles(imagesVideos, priorityMap);
    _Trimmer._sortFiles(imagesChannels, null); // file names dont start with videoId
    final maxBytesImages = (maxBytes * 0.9).round();
    final maxBytesImagesChannels = maxBytes - maxBytesImages;
    int total = 0;
    total += _Trimmer._trimExcessCache(imagesVideos, maxBytesImages);
    total += _Trimmer._trimExcessCache(imagesChannels, maxBytesImagesChannels);
    return total;
  }

  Future<void> _trimExcessImageCacheTemp(DbWrapperFileInfo priorityDbInfo) async {
    final dirPath = FileParts.joinPath(AppDirs.YT_THUMBNAILS, 'temp');
    if (!await Directory(dirPath).exists()) return;
    final params = _TrimDirParam(
      dirPath: dirPath,
      maxBytes: 0, // not by bytes
      priorityDbInfo: priorityDbInfo,
    );
    return await _trimExcessImageCacheTempIsolate.thready(params);
  }

  static Future<void> _trimExcessImageCacheTempIsolate(_TrimDirParam params) async {
    final dirPath = params.dirPath;
    final priorityMap = await VideosPriorityManager.loadEverythingSync(params.priorityDbInfo);

    final imagesPre = Directory(dirPath).listSyncSafe();
    int excess = imagesPre.length - 2000; // keeping it at max 2000 good files.
    if (excess <= 0) return;

    final images = <File>[];

    for (final e in imagesPre) {
      if (e.path.endsWith('.temp')) {
        try {
          e.deleteSync();
          excess--;
        } catch (_) {}
      } else {
        if (e is File) images.add(e);
      }
    }

    if (excess <= 0) return;

    _Trimmer._sortFiles(images, priorityMap);

    for (final element in images.take(excess)) {
      try {
        element.deleteSync();
      } catch (_) {}
    }
  }
}

class _Trimmer {
  /// cached files are guranteed to have a name starting with [priorityMap].key
  static void _sortFiles(List<FileSystemEntity> files, Map<String, CacheVideoPriority>? priorityMap) {
    int compareAccessTime(FileSystemEntity a, FileSystemEntity b) {
      try {
        final aTime = a.statSync().accessed;
        final bTime = b.statSync().accessed;
        return aTime.compareTo(bTime);
      } catch (_) {
        return 0;
      }
    }

    if (priorityMap != null && priorityMap.isNotEmpty) {
      final videoIdsLookup = <String, String?>{};
      final finalFiles = <FileSystemEntity>[];
      for (final f in files) {
        try {
          final videoId = videoIdsLookup[f.path] = f.path.getFilename.substring(0, 11);
          if (priorityMap[videoId] != CacheVideoPriority.VIP) {
            finalFiles.add(f);
          }
        } catch (_) {}
      }
      finalFiles.sort((a, b) {
        final aVideoId = videoIdsLookup[a.path];
        final bVideoId = videoIdsLookup[b.path];
        if (aVideoId != null && bVideoId != null) {
          final aPriority = priorityMap[aVideoId] ?? CacheVideoPriority.normal;
          final bPriority = priorityMap[bVideoId] ?? CacheVideoPriority.normal;
          final priorityCompare = bPriority.index.compareTo(aPriority.index); // the lower the index, the more important, thats why the reverse.
          if (priorityCompare != 0) return priorityCompare;
        }
        return compareAccessTime(a, b);
      });
    } else {
      files.sort(compareAccessTime);
    }
  }

  static int _trimExcessCache(List<FileSystemEntity> files, int maxBytes) {
    int totalDeletedBytes = 0;
    int totalBytes = 0;
    final sizesMap = <String, int>{};
    for (var f in files) {
      if (f is File) {
        final size = f.fileSizeSync() ?? 0;
        sizesMap[f.path] = size;
        totalBytes += size;
      }
    }
    for (final file in files) {
      if (totalBytes <= maxBytes) break; // better than checking with each loop
      if (file is File) {
        final deletedSize = sizesMap[file.path] ?? file.fileSizeSync() ?? 0;
        try {
          file.deleteSync();
          totalBytes -= deletedSize;
          totalDeletedBytes += deletedSize;
        } catch (_) {}
      }
    }

    return totalDeletedBytes;
  }
}

class _AudioVideoTrimmer {
  static int _getTempFilesSizeIsolate(Map dirsPath) {
    int size = 0;
    final tempDir = dirsPath['temp'] as String?;
    final normalDir = dirsPath['normal'] as String;
    if (tempDir != null) {
      final files = Directory(tempDir).listSyncSafe();
      for (var e in files) {
        if (e is File) {
          size += e.fileSizeSync() ?? 0;
        }
      }
    }
    final files = Directory(normalDir).listSyncSafe();
    for (var e in files) {
      if (e is File && e.path.endsWith('.part')) {
        size += e.fileSizeSync() ?? 0;
      }
    }
    return size;
  }

  static void _deleteTempFilesIsolate(Map dirsPath) {
    final tempDir = dirsPath['temp'] as String?;
    final normalDir = dirsPath['normal'] as String;
    if (tempDir != null) {
      final files = Directory(tempDir).listSyncSafe();
      for (var e in files) {
        if (e is File) {
          try {
            e.deleteSync();
          } catch (_) {}
        }
      }
    }
    final files = Directory(normalDir).listSyncSafe();
    for (var e in files) {
      if (e.path.endsWith('.part')) {
        if (e is File) {
          try {
            e.deleteSync();
          } catch (_) {}
        }
      }
    }
  }

  static (List<File>, int) _countAudioFilesThatAlreadyExistsIsolate(Map params) {
    int totalSize = 0;
    final files = <File>[];
    _loopAudioCacheExistingIdsMap(params, (file, isMetadata) {
      if (!isMetadata) {
        // -- we only get non-metadata files, but for deletion we delete all
        totalSize += file.fileSizeSync() ?? 0;
        files.add(file);
      }
    });
    return (files, totalSize);
  }

  static void _deleteAudioFilesThatAlreadyExistsIsolate(Map params) {
    return _loopAudioCacheExistingIdsMap(params, (file, isMetadata) => file.deleteSync());
  }

  static void _loopAudioCacheExistingIdsMap(Map params, void Function(File file, bool isMetadata) callback) {
    final normalDir = params['normal'] as String;
    final alreadyExistingInLibrary = params['idsMap'] as Map<String, List<Track>>;
    final forVideos = params['forVideos'] as bool;
    // ignore: prefer_function_declarations_over_variables
    final forVideosChecker = forVideos ? (Track tr) => tr is Video : (Track tr) => true;
    final files = Directory(normalDir).listSyncSafe();
    for (var file in files) {
      if (file is File) {
        try {
          final filename = file.path.getFilename;
          final id = filename.substring(0, 11); // 'Wd_gr91dgDa_23393.m4a' -> 'Wd_gr91dgDa'
          final valInMap = alreadyExistingInLibrary[id];
          final alreadyExists = valInMap != null && valInMap.isNotEmpty && valInMap.any((element) => forVideosChecker(element) && File(element.path).existsSync());
          if (alreadyExists) {
            final isMetadata = filename.endsWith('.metadata') || filename.endsWith('.part') || filename.endsWith('.mime');
            callback(file, isMetadata);
          }
        } catch (_) {}
      }
    }
  }
}

class _TrimDirParam {
  final String dirPath;
  final String? extraDirPath;
  final int maxBytes;
  final DbWrapperFileInfo priorityDbInfo;

  const _TrimDirParam({
    required this.dirPath,
    this.extraDirPath,
    required this.maxBytes,
    required this.priorityDbInfo,
  });
}
