content: import 'dart:async';
  import 'dart:convert';
  import 'dart:math';
  import 'package:http_interceptor/http_interceptor.dart';
  import 'package:shonenx/core_new/models/video.dart';
  import 'package:shonenx/core_new/services/http/m_client.dart';
  
  enum CloudDriveType { quark, uc }
  
  class QuarkUcExtractor {
    late CloudDriveType cloudDriveType;
    String apiUrl = "";
    // String cookie = "";
    String refererUrl = "";
    String ua = "";
    String host = "";
    Map<String, dynamic> shareTokenCache = {};
    String pr = "";
    final List<String> subtitleExts = ['.srt', '.ass', '.scc', '.stl', '.ttml'];
    Map<String, String> saveFileIdCaches = {};
    String? saveDirId;
    final String saveDirName = 'TV';
    String _lastCookie = "";
  
    Future<void> initCloudDrive(
      String cookie,
      CloudDriveType cloudDriveType,
    ) async {
      this.cloudDriveType = cloudDriveType;
      if (cloudDriveType == CloudDriveType.quark) {
        apiUrl = "https://drive-pc.quark.cn/1/clouddrive/";
        pr = "pr=ucpro&fr=pc";
        ua =
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch";
        refererUrl = "https://pan.quark.cn/";
        host = "https://quark.cn";
        _lastCookie = "https://quarkcookie.last";
      } else {
        apiUrl = "https://pc-api.uc.cn/1/clouddrive/";
        pr = "pr=UCBrowser&fr=pc";
        ua =
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) uc-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch";
        refererUrl = "https://drive.uc.cn/";
        host = "https://uc.cn";
        _lastCookie = "https://uccookie.last";
      }
      if (cookie.isNotEmpty && getLastCookie() != cookie) {
        MClient.setCookie(host, ua, null, cookie: cookie);
        MClient.setCookie(_lastCookie, ua, null, cookie: cookie);
      }
    }
  
    String getLastCookie() {
      var cookie = MClient.getCookiesPref(_lastCookie);
      return cookie.isNotEmpty ? cookie.values.first : "";
    }
  
    String getCurrentCookie() {
      var cookie = MClient.getCookiesPref(host);
      return cookie.isNotEmpty ? cookie.values.first : "";
    }
  
    Map<String, String> getHeaders() {
      return {
        'User-Agent': ua,
        'Referer': refererUrl,
        "Content-Type": "application/json",
        "Cookie": getCurrentCookie(),
      };
    }
  
    Future<Map<String, dynamic>> api(
      String url,
      dynamic data,
      String method,
    ) async {
      InterceptedClient client = MClient.init(
        reqcopyWith: {'useDartHttpClient': true},
      );
      late Response resp;
      if (method != "get") {
        resp = await client.post(
          Uri.parse(apiUrl + url),
          body: jsonEncode(data),
          headers: getHeaders(),
        );
      } else {
        resp = await client.get(Uri.parse(apiUrl + url), headers: getHeaders());
      }
      // if (resp.headers['set-cookie'] != null) {
      //   print('headers: ${resp.headers}');
      //   final puus = resp.headers['set-cookie']!
      //       .split(';;;')
      //       .join()
      //       .split(';')
      //       .firstWhere((element) => element.startsWith('__puus='),
      //           orElse: () => '');
      //   if (puus.isNotEmpty) {
      //     final newPuus = puus.split('=')[1];
      //     var cookie = getCurrentCookie();
      //     if (cookie != null && cookie.contains('__puus=')) {
      //       cookie =
      //           cookie.replaceFirst(RegExp(r'__puus=[^;]+'), '__puus=$newPuus');
      //     }
      //     MClient.setCookie(host, ua, cookie: cookie);
      //   }
      // }
      // 处理 set-cookie
      if (resp.headers['set-cookie'] != null) {
        final cookies = resp.headers['set-cookie']!.split(';;;');
        for (var cookie in cookies) {
          if (cookie.contains('__puus=')) {
            final newPuus = cookie.split(';')[0]; // 获取新的 __puus
            var currentCookie = getCurrentCookie();
            if (currentCookie.isNotEmpty) {
              // 更新 __puus
              if (currentCookie.contains('__puus=')) {
                currentCookie = currentCookie.replaceFirst(
                  RegExp(r'__puus=[^;]+'),
                  newPuus,
                );
              } else {
                currentCookie = '$currentCookie; $newPuus';
              }
              MClient.setCookie(host, ua, null, cookie: currentCookie);
            }
            break;
          }
        }
      }
      return jsonDecode(resp.body);
    }
  
    Map<String, String>? getShareData(String url) {
      RegExp regex;
      if (cloudDriveType == CloudDriveType.quark) {
        regex = RegExp(r'https://pan\.quark\.cn/s/([^\\|#/]+)');
      } else {
        regex = RegExp(r'https://drive\.uc\.cn/s/([^?]+)');
      }
      final matches = regex.firstMatch(url);
      if (matches != null) {
        return {'shareId': matches.group(1)!, 'folderId': '0'};
      }
      return null;
    }
  
    Future<void> getShareToken(Map<String, String> shareData) async {
      if (!shareTokenCache.containsKey(shareData['shareId'])) {
        shareTokenCache.remove(shareData['shareId']);
        final shareToken = await api('share/sharepage/token?$pr', {
          'pwd_id': shareData['shareId'],
          'passcode': shareData['sharePwd'] ?? '',
        }, 'post');
        if (shareToken['data'] != null && shareToken['data']['stoken'] != null) {
          shareTokenCache[shareData['shareId']!] = shareToken['data'];
        }
      }
    }
  
    Future<List<dynamic>> listFile(
      int shareIndex,
      Map<String, String> shareData,
      List<dynamic> videos,
      List<dynamic> subtitles,
      String shareId,
      String folderId, {
      int page = 1,
    }) async {
      const int prePage = 200;
      final listData = await api(
        'share/sharepage/detail?$pr&pwd_id=$shareId&stoken=${Uri.encodeComponent(shareTokenCache[shareId]['stoken'])}&pdir_fid=$folderId&force=0&_page=$page&_size=$prePage&_sort=file_type:asc,file_name:desc',
        null,
        'get',
      );
      if (listData['data'] == null) return [];
      final items = listData['data']['list'];
      if (items == null) return [];
      final subDir = [];
      for (final item in items) {
        if (item['dir'] == true) {
          subDir.add(item);
        } else if (item['file'] == true && item['obj_category'] == 'video') {
          if (item['size'] < 1024 * 1024 * 5) continue;
          item['stoken'] = shareTokenCache[shareData['shareId']]['stoken'];
          videos.add(
            Item.objectFrom(
              item,
              shareData['shareId']!,
              shareIndex,
              cloudDriveType,
            ),
          );
        } else if (item['type'] == 'file' &&
            subtitleExts.any((x) => item['file_name'].endsWith(x))) {
          subtitles.add(
            Item.objectFrom(
              item,
              shareData['shareId']!,
              shareIndex,
              cloudDriveType,
            ),
          );
        }
      }
      if (page < (listData['metadata']['_total'] / prePage).ceil()) {
        final nextItems = await listFile(
          shareIndex,
          shareData,
          videos,
          subtitles,
          shareId,
          folderId,
          page: page + 1,
        );
        items.addAll(nextItems);
      }
      for (final dir in subDir) {
        final subItems = await listFile(
          shareIndex,
          shareData,
          videos,
          subtitles,
          shareId,
          dir['fid'],
        );
        items.addAll(subItems);
      }
      return items;
    }
  
    Map<String, dynamic> findBestLCS(Item mainItem, List<Item> targetItems) {
      final results = [];
      var bestMatchIndex = 0;
      for (var i = 0; i < targetItems.length; i++) {
        final currentLCS = lcs(mainItem.name, targetItems[i].name);
        results.add({'target': targetItems[i], 'lcs': currentLCS});
        if (currentLCS['length'] > results[bestMatchIndex]['lcs']['length']) {
          bestMatchIndex = i;
        }
      }
      final bestMatch = results[bestMatchIndex];
      return {
        'allLCS': results,
        'bestMatch': bestMatch,
        'bestMatchIndex': bestMatchIndex,
      };
    }
  
    Future<void> getFilesByShareUrl(
      int shareIndex,
      dynamic shareInfo,
      List<dynamic> videos,
      List<dynamic> subtitles,
    ) async {
      final shareData = shareInfo is String ? getShareData(shareInfo) : shareInfo;
      if (shareData == null) return;
      await getShareToken(shareData);
      if (!shareTokenCache.containsKey(shareData['shareId'])) return;
      await listFile(
        shareIndex,
        shareData,
        videos,
        subtitles,
        shareData['shareId']!,
        shareData['folderId']!,
      );
      if (subtitles.isNotEmpty) {
        for (var item in videos) {
          var matchSubtitle = findBestLCS(item, subtitles as List<Item>);
          if (matchSubtitle['bestMatch'] != null) {
            item.subtitle = matchSubtitle['bestMatch']['target'];
          }
        }
      }
    }
  
    void clean() {
      saveFileIdCaches.clear();
    }
  
    Future<void> clearSaveDir() async {
      final listData = await api(
        'file/sort?$pr&pdir_fid=$saveDirId&_page=1&_size=200&_sort=file_type:asc,updated_at:desc',
        {},
        'get',
      );
      if (listData['data'] != null &&
          listData['data']['list'] != null &&
          listData['data']['list'].isNotEmpty) {
        await api('file/delete?$pr', {
          'action_type': 2,
          'filelist': listData['data']['list'].map((v) => v['fid']).toList(),
          'exclude_fids': [],
        }, 'post');
      }
    }
  
    Future<void> createSaveDir(bool clean) async {
      if (saveDirId != null) {
        if (clean) await clearSaveDir();
        return;
      }
      final listData = await api(
        'file/sort?$pr&pdir_fid=0&_page=1&_size=200&_sort=file_type:asc,updated_at:desc',
        {},
        'get',
      );
      if (listData['data'] != null && listData['data']['list'] != null) {
        for (final item in listData['data']['list']) {
          if (item['file_name'] == saveDirName) {
            saveDirId = item['fid'];
            await clearSaveDir();
            break;
          }
        }
      }
      if (saveDirId == null) {
        final create = await api('file?$pr', {
          'pdir_fid': '0',
          'file_name': saveDirName,
          'dir_path': '',
          'dir_init_lock': false,
        }, 'post');
        if (create['data'] != null && create['data']['fid'] != null) {
          saveDirId = create['data']['fid'];
        }
      }
    }
  
    Future<String?> save(
      String shareId,
      String stoken,
      String fileId,
      String fileToken,
      bool clean,
    ) async {
      await createSaveDir(clean);
      if (clean) {
        this.clean();
      }
      if (saveDirId == null) return null;
      if (stoken.isEmpty) {
        await getShareToken({'shareId': shareId});
        if (!shareTokenCache.containsKey(shareId)) return null;
      }
      final saveResult = await api('share/sharepage/save?$pr', {
        'fid_list': [fileId],
        'fid_token_list': [fileToken],
        'to_pdir_fid': saveDirId,
        'pwd_id': shareId,
        'stoken': stoken.isNotEmpty ? stoken : shareTokenCache[shareId]['stoken'],
        'pdir_fid': '0',
        'scene': 'link',
      }, 'post');
      if (saveResult['data'] != null && saveResult['data']['task_id'] != null) {
        var retry = 0;
        while (true) {
          final taskResult = await api(
            'task?$pr&task_id=${saveResult['data']['task_id']}&retry_index=$retry',
            {},
            'get',
          );
          if (taskResult['data'] != null &&
              taskResult['data']['save_as'] != null &&
              taskResult['data']['save_as']['save_as_top_fids'] != null &&
              taskResult['data']['save_as']['save_as_top_fids'].isNotEmpty) {
            return taskResult['data']['save_as']['save_as_top_fids'][0];
          }
          retry++;
          if (retry > 2) break;
          await Future.delayed(const Duration(seconds: 1));
        }
      }
      return null;
    }
  
    Future<List<Map<String, String>>?> getLiveTranscoding(
      String shareId,
      String stoken,
      String fileId,
      String fileToken,
    ) async {
      if (!saveFileIdCaches.containsKey(fileId)) {
        final saveFileId = await save(shareId, stoken, fileId, fileToken, true);
        if (saveFileId == null) return null;
        saveFileIdCaches[fileId] = saveFileId;
      }
      final transcoding = await api('file/v2/play?$pr', {
        'fid': saveFileIdCaches[fileId],
        'resolutions': 'normal,low,high,super,2k,4k',
        'supports': 'fmp4',
      }, 'post');
      if (transcoding['data'] != null &&
          transcoding['data']['video_list'] != null) {
        List<Map<String, String>> qualityOptions = [];
        for (final video in transcoding['data']['video_list']) {
          qualityOptions.add({
            'url': video['video_info']['url'],
            'quality': video['resolution'],
          });
        }
        return qualityOptions;
      }
      return null;
    }
  
    Future<Map<String, dynamic>?> getDownload(
      String shareId,
      String stoken,
      String fileId,
      String fileToken,
      bool clean,
    ) async {
      if (!saveFileIdCaches.containsKey(fileId)) {
        final saveFileId = await save(shareId, stoken, fileId, fileToken, clean);
        if (saveFileId == null) return null;
        saveFileIdCaches[fileId] = saveFileId;
      }
      final down = await api('file/download?$pr&uc_param_str=', {
        'fids': [saveFileIdCaches[fileId]],
      }, 'post');
      if (down['data'] != null) {
        return down['data'][0];
      }
      return null;
    }
  
    Future<List<Map<String, String>>> videoFilesFromUrl(
      List<String> shareUrlList, {
      String typeName = "电影",
    }) async {
      List<dynamic> videoItems = [];
      List<dynamic> subItems = [];
  
      for (int i = 0; i < shareUrlList.length; i++) {
        String shareUrl = shareUrlList[i];
        await getFilesByShareUrl(i + 1, shareUrl, videoItems, subItems);
      }
  
      return await getVodFile(videoItems, subItems, typeName);
    }
  
    Future<List<Map<String, String>>> getVodFile(
      List<dynamic> videoItemList,
      List<dynamic> subItemList,
      String typeName,
    ) async {
      if (videoItemList.isEmpty) {
        return [];
      }
      List<Map<String, String>> vodItems = [];
      for (var videoItem in videoItemList) {
        String episodeUrl = videoItem.getEpisodeUrl(typeName);
        String subtitles = findSubs(videoItem.getName(), subItemList);
        String fullUrl = episodeUrl + subtitles;
        List<String> parts = fullUrl.split('\$');
        String name = parts[0].trim();
        String url = parts[1];
        vodItems.add({"name": name, "url": url});
      }
      // print(vodItems);
      return vodItems;
    }
  
    Future<List<Video>> videosFromUrl(String url) async {
      List<String> parts = url.split('++');
      String fileId = parts[1];
      String fileToken = parts[2];
      String shareId = parts[3];
      String stoken = parts[4];
      // String type = parts[0];
      List<String> subtitleParts = parts.length > 5 ? parts[5].split('+') : [];
      // 获取可用的质量列表
      //List<String> qualities = getPlayFormtList();
      List<Video> videos = [];
  
      String? originalUrl;
      List<Map<String, String>>? qualityOptions = await getLiveTranscoding(
        shareId,
        stoken,
        fileId,
        fileToken,
      );
      originalUrl = qualityOptions?[0]['url'];
      var headers = getHeaders();
      headers.remove('Content-Type');
      if (qualityOptions != null) {
        for (Map<String, String> qualityOption in qualityOptions) {
          videos.add(
            Video(
              qualityOption['url'] ?? '',
              qualityOption['quality'] ?? '',
              originalUrl ?? '',
              headers: headers,
            ),
          );
        }
      }
  
      // 处理字幕
      List<Track> subtitles = [];
      for (String subtitleInfo in subtitleParts) {
        if (subtitleInfo.isNotEmpty) {
          List<String> subParts = subtitleInfo.split('@@@');
          if (subParts.length == 3) {
            String subName = subParts[0];
            String subFileId = subParts[2];
            var subDownload = await getDownload(
              shareId,
              stoken,
              subFileId,
              '',
              false,
            );
            String? subUrl = subDownload?['download_url'];
            if (subUrl != null) {
              subtitles.add(Track(file: subUrl, label: subName));
            }
          }
        }
      }
  
      // 为所有视频添加字幕
      for (var video in videos) {
        video.subtitles = subtitles;
      }
      return videos;
    }
  
    String findSubs(String name, List<dynamic> itemList) {
      List<dynamic> subItemList = [];
      pair(removeExt(name).toLowerCase(), itemList, subItemList);
      if (subItemList.isEmpty) {
        subItemList.addAll(itemList);
      }
      String subStr = "";
      for (var item in subItemList) {
        subStr +=
            "+${removeExt(item.getName())}@@@${item.getFileExtension()}@@@${item.getFileId()}";
      }
      return subStr;
    }
  
    void pair(String name, List<dynamic> itemList, List<dynamic> subItemList) {
      for (var item in itemList) {
        final subName = removeExt(item.getName()).toLowerCase();
        if (name.contains(subName) || subName.contains(name)) {
          subItemList.add(item);
        }
      }
    }
  
    String removeExt(String text) {
      return text.contains('.') ? text.substring(0, text.lastIndexOf(".")) : text;
    }
  
    Map<String, dynamic> lcs(String str1, String str2) {
      if (str1.isEmpty || str2.isEmpty) {
        return {'length': 0, 'sequence': '', 'offset': 0};
      }
      var sequence = '';
      var str1Length = str1.length;
      var str2Length = str2.length;
      var num = List.generate(str1Length, (_) => List<int>.filled(str2Length, 0));
      var maxlen = 0;
      var lastSubsBegin = 0;
      var thisSubsBegin = 0;
      for (var i = 0; i < str1Length; i++) {
        for (var j = 0; j < str2Length; j++) {
          if (str1[i] != str2[j]) {
            num[i][j] = 0;
          } else {
            if (i == 0 || j == 0) {
              num[i][j] = 1;
            } else {
              num[i][j] = 1 + num[i - 1][j - 1];
            }
            if (num[i][j] > maxlen) {
              maxlen = num[i][j];
              thisSubsBegin = i - num[i][j] + 1;
              if (lastSubsBegin == thisSubsBegin) {
                sequence += str1[i];
              } else {
                lastSubsBegin = thisSubsBegin;
                sequence = str1.substring(lastSubsBegin, i + 1);
              }
            }
          }
        }
      }
      return {'length': maxlen, 'sequence': sequence, 'offset': thisSubsBegin};
    }
  }
  
  class Item {
    String fileId = "";
    String shareId = "";
    String shareToken = "";
    String shareFileToken = "";
    String seriesId = "";
    String name = "";
    String type = "";
    String formatType = "";
    String size = "";
    String parent = "";
    dynamic shareData;
    int shareIndex = 0;
    int lastUpdateAt = 0;
    dynamic subtitle;
    late CloudDriveType cloudDriveType;
  
    static Item objectFrom(
      Map<String, dynamic> itemJson,
      String shareId,
      int shareIndex,
      CloudDriveType cloudDriveType,
    ) {
      Item item = Item();
      item.fileId = itemJson['fid'] ?? "";
      item.shareId = shareId;
      item.shareToken = itemJson['stoken'] ?? "";
      item.shareFileToken = itemJson['share_fid_token'] ?? "";
      item.seriesId = itemJson['series_id'] ?? "";
      item.name = itemJson['file_name'] ?? "";
      item.type = itemJson['obj_category'] ?? "";
      item.formatType = itemJson['format_type'] ?? "";
      item.size = (itemJson['size'] ?? 0).toString();
      item.parent = itemJson['pdir_fid'] ?? "";
      item.lastUpdateAt = itemJson['last_update_at'] ?? 0;
      item.shareIndex = shareIndex;
      item.cloudDriveType = cloudDriveType;
      return item;
    }
  
    String getFileExtension() {
      return name.split(".").last;
    }
  
    String getFileId() {
      return fileId;
    }
  
    String getName() {
      return name;
    }
  
    String getParent() {
      return "[$parent]";
    }
  
    String getSize() {
      return size == "0" ? "" : "[${getHumanReadableSize(int.parse(size))}]";
    }
  
    int getShareIndex() {
      return shareIndex;
    }
  
    String getDisplayName(String typeName) {
      String drivePrefix = cloudDriveType == CloudDriveType.quark
          ? '[quark]'
          : '[uc]';
      String displayName = getName();
      if (typeName == "电视剧") {
        List<String> replaceNameList = ["4k", "4K"];
        displayName = displayName.replaceAll(".$getFileExtension()", "");
        displayName = displayName.replaceAll(" ", "").replaceAll(" ", "");
        for (String replaceName in replaceNameList) {
          displayName = displayName.replaceAll(replaceName, "");
        }
        displayName =
            RegExp(r'\.S01E(.*?)\.').firstMatch(displayName)?.group(1) ??
            displayName;
        final numbers = RegExp(
          r'\d+',
        ).allMatches(displayName).map((m) => m.group(0)).toList();
        if (numbers.isNotEmpty) {
          displayName = numbers[0]!;
        }
      }
      return "$drivePrefix $displayName ${getSize()}";
    }
  
    String getEpisodeUrl(String typeName) {
      return "${getDisplayName(typeName)}\$${cloudDriveType == CloudDriveType.quark ? "quark" : "uc"}++${getFileId()}++$shareFileToken++$shareId++$shareToken";
    }
  
    String getHumanReadableSize(int bytes) {
      if (bytes <= 0) return "";
      final units = ['B', 'KB', 'MB', 'GB', 'TB'];
      int digitGroups = (log(bytes) / log(1024)).floor();
      return '${(bytes / pow(1024, digitGroups)).toStringAsFixed(2)} ${units[digitGroups]}';
    }
  }
  