from pathlib import Path
from typing import Set, Tuple, Optional, Union, List, Dict

from qbittorrentapi import TorrentFilesList
from torrentool.torrent import Torrent

from app import schemas
from app.core.cache import FileCache
from app.core.config import settings
from app.core.metainfo import MetaInfo
from app.log import logger
from app.modules import _ModuleBase, _DownloaderBase
from app.modules.qbittorrent.qbittorrent import Qbittorrent
from app.schemas import DownloaderTorrent
from app.schemas.types import (
    DownloadTaskState,
    DownloaderType,
    ModuleType,
    TorrentQueryStatus,
    TorrentStatus,
)
from app.utils.string import StringUtils

_QBITTORRENT_DOWNLOADING_STATES = {
    "allocating",
    "checkingdl",
    "downloading",
    "forceddl",
    "metadl",
    "queueddl",
    "stalleddl",
}
_QBITTORRENT_PAUSED_STATES = {
    "paused",
    "pauseddl",
    "pausedup",
    "stoppeddl",
    "stoppedup",
}
_TORRENT_FILES_RETRY_TIMES = 5
_TORRENT_FILES_RETRY_INTERVAL = 1


class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
    """
    qBittorrent 下载器模块，负责下载任务添加、文件选择和任务管理。
    """

    def init_module(self) -> None:
        """
        初始化模块
        """
        super().init_service(service_name=Qbittorrent.__name__.lower(),
                             service_type=Qbittorrent)

    @staticmethod
    def get_name() -> str:
        """
        获取模块名称
        """
        return "Qbittorrent"

    @staticmethod
    def get_type() -> ModuleType:
        """
        获取模块类型
        """
        return ModuleType.Downloader

    @staticmethod
    def get_subtype() -> DownloaderType:
        """
        获取模块子类型
        """
        return DownloaderType.Qbittorrent

    @staticmethod
    def get_priority() -> int:
        """
        获取模块优先级，数字越小优先级越高，只有同一接口下优先级才生效
        """
        return 1

    def stop(self) -> None:
        """
        停止模块
        """
        pass

    def test(self) -> Optional[Tuple[bool, str]]:
        """
        测试模块连接性
        """
        if not self.get_instances():
            return None
        for name, server in self.get_instances().items():
            if server.is_inactive():
                server.reconnect()
            if not server.transfer_info():
                return False, f"无法连接Qbittorrent下载器：{name}"
        return True, ""

    def init_setting(self) -> Tuple[str, Union[str, bool]]:
        """
        返回控制模块启用状态的配置项
        """
        pass

    def scheduler_job(self) -> None:
        """
        定时任务，每10分钟调用一次
        """
        for name, server in self.get_instances().items():
            if server.is_inactive():
                logger.info(f"Qbittorrent下载器 {name} 连接断开，尝试重连 ...")
                server.reconnect()

    def download(self, content: Union[Path, str, bytes], download_dir: Path, cookie: str,
                 episodes: Set[int] = None, category: Optional[str] = None, label: Optional[str] = None,
                 downloader: Optional[str] = None) -> Optional[Tuple[Optional[str], Optional[str], Optional[str], str]]:
        """
        根据种子文件，选择并添加下载任务
        :param content:  种子文件地址或者磁力链接或者种子内容
        :param download_dir:  下载目录
        :param cookie:  cookie
        :param episodes:  需要下载的集数
        :param category:  分类
        :param label:  标签
        :param downloader:  下载器
        :return: 下载器名称、种子Hash、种子文件布局、错误原因
        """

        def __get_torrent_info() -> Tuple[Optional[Torrent], Optional[bytes]]:
            """
            获取种子名称
            """
            torrent_info, torrent_content = None, None
            try:
                if isinstance(content, Path):
                    if content.exists():
                        torrent_content = content.read_bytes()
                    else:
                        # 读取缓存的种子文件
                        torrent_content = FileCache().get(content.as_posix(), region="torrents")
                else:
                    torrent_content = content

                if torrent_content:
                    # 检查是否为磁力链接
                    if StringUtils.is_magnet_link(torrent_content):
                        return None, torrent_content
                    else:
                        torrent_info = Torrent.from_string(torrent_content)

                return torrent_info, torrent_content
            except Exception as e:
                logger.error(f"获取种子名称失败：{e}")
                return None, None

        if not content:
            return None, None, None, "下载内容为空"

        # 读取种子的名称
        torrent_from_file, content = __get_torrent_info()
        # 检查是否为磁力链接
        is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content,
                                                                                             bytes) and content.startswith(
            b"magnet:")
        if not torrent_from_file and not is_magnet:
            return None, None, None, f"添加种子任务失败：无法读取种子文件"

        # 获取下载器
        server: Qbittorrent = self.get_instance(downloader)
        if not server:
            return None

        # 生成随机Tag
        tag = StringUtils.generate_random_str(10)
        if label:
            tags = label.split(',') + [tag]
        elif settings.TORRENT_TAG:
            tags = [tag, settings.TORRENT_TAG]
        else:
            tags = [tag]
        # 如果要选择文件则先暂停
        is_paused = True if episodes else False
        # 添加任务
        state, added_torrent_ids = server.add_torrent(
            content=content,
            download_dir=self.normalize_path(download_dir, downloader),
            is_paused=is_paused,
            tag=tags,
            cookie=cookie,
            category=category,
            ignore_category_check=False
        )

        # 获取种子内容布局: `Original: 原始, Subfolder: 创建子文件夹, NoSubfolder: 不创建子文件夹`
        torrent_layout = server.get_content_layout()

        if not state:
            # 查询所有下载器的种子
            torrents, error = server.get_torrents()
            if error:
                return None, None, None, "无法连接qbittorrent下载器"
            if torrents:
                try:
                    for torrent in torrents:
                        # 名称与大小相等则认为是同一个种子
                        if torrent.get("name") == getattr(torrent_from_file, 'name', '') \
                                and torrent.get("total_size") == getattr(torrent_from_file, 'total_size', 0):
                            torrent_hash = torrent.get("hash")
                            torrent_tags = [str(tag).strip() for tag in torrent.get("tags").split(',')]
                            logger.warn(f"下载器中已存在该种子任务：{torrent_hash} - {torrent.get('name')}")
                            # 给种子打上标签
                            if "已整理" in torrent_tags:
                                server.remove_torrents_tag(ids=torrent_hash, tag=['已整理'])
                            if settings.TORRENT_TAG and settings.TORRENT_TAG not in torrent_tags:
                                logger.info(f"给种子 {torrent_hash} 打上标签：{settings.TORRENT_TAG}")
                                server.set_torrents_tag(ids=torrent_hash, tags=[settings.TORRENT_TAG])
                            return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, f"下载任务已存在"
                finally:
                    torrents.clear()
                    del torrents
            return None, None, None, f"添加种子任务失败：{content}"
        else:
            # 获取种子Hash
            torrent_hash = next(iter(added_torrent_ids), None)
            if torrent_hash:
                server.delete_torrents_tag(torrent_hash, tag)
            else:
                torrent_hash = server.get_torrent_id_by_tag(tags=tag)
            if not torrent_hash:
                return None, None, None, f"下载任务添加成功，但获取Qbittorrent任务信息失败：{content}"
            else:
                if is_paused:
                    # 种子文件
                    torrent_files = server.get_files(
                        torrent_hash,
                        retry=_TORRENT_FILES_RETRY_TIMES,
                        interval=_TORRENT_FILES_RETRY_INTERVAL,
                    )
                    if not torrent_files:
                        return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, "获取种子文件失败，下载任务可能在暂停状态"

                    # 不需要的文件ID
                    file_ids = []
                    # 需要的集清单
                    sucess_epidised = []
                    try:
                        for torrent_file in torrent_files:
                            file_id = torrent_file.get("id")
                            file_name = torrent_file.get("name")
                            meta_info = MetaInfo(file_name)
                            if not meta_info.episode_list \
                                    or not set(meta_info.episode_list).issubset(episodes):
                                file_ids.append(file_id)
                            else:
                                sucess_epidised = list(set(sucess_epidised).union(set(meta_info.episode_list)))
                    finally:
                        torrent_files.clear()
                        del torrent_files
                    if sucess_epidised and file_ids:
                        # 选择文件
                        server.set_files(torrent_hash=torrent_hash, file_ids=file_ids, priority=0)
                    # 开始任务
                    if server.is_force_resume():
                        # 强制继续
                        server.torrents_set_force_start(torrent_hash)
                    else:
                        server.start_torrents(torrent_hash)
                    return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, f"添加下载成功，已选择集数：{sucess_epidised}"
                else:
                    if server.is_force_resume():
                        server.torrents_set_force_start(torrent_hash)
                    return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, "添加下载成功"

    def list_torrents(self, status: TorrentStatus = None,
                      hashs: Union[list, str] = None,
                      downloader: Optional[str] = None,
                      include_all_tags: bool = False,
                      ) -> Optional[List[DownloaderTorrent]]:
        """
        获取下载器种子列表
        :param status:  种子状态
        :param hashs:  种子Hash
        :param downloader:  下载器
        :param include_all_tags:  是否包含未打内置标签的下载任务
        :return: 下载器中符合状态的种子列表
        """
        # 获取下载器
        if downloader:
            server: Qbittorrent = self.get_instance(downloader)
            if not server:
                return None
            servers = {downloader: server}
        else:
            servers: Dict[str, Qbittorrent] = self.get_instances()
        ret_torrents = []
        query_status = self.__normalize_query_status(status)
        query_tags = None if include_all_tags else settings.TORRENT_TAG

        def __get_torrent_path(torrent_data: dict) -> Path:
            """
            获取种子内容路径。
            """
            content_path = torrent_data.get("content_path")
            if content_path:
                return Path(content_path)
            return Path(torrent_data.get('save_path')) / torrent_data.get('name')

        def __build_torrent(downloader_name: str, torrent_data: dict) -> DownloaderTorrent:
            """
            构造统一下载器任务对象。
            """
            meta = MetaInfo(torrent_data.get('name'))
            torrent_path = __get_torrent_path(torrent_data)
            dlspeed = torrent_data.get('dlspeed') or 0
            total_size = torrent_data.get('total_size') or 0
            completed_size = torrent_data.get('completed') or 0
            return DownloaderTorrent(
                downloader=downloader_name,
                title=torrent_data.get('name'),
                name=meta.name,
                year=meta.year,
                season_episode=meta.season_episode,
                path=Path(self.normalize_return_path(torrent_path, downloader_name)),
                save_path=self.normalize_return_path(
                    Path(torrent_data.get('save_path') or ""), downloader_name
                ) if torrent_data.get('save_path') else None,
                content_path=self.normalize_return_path(torrent_path, downloader_name),
                hash=torrent_data.get('hash'),
                size=total_size,
                tags=torrent_data.get('tags'),
                category=torrent_data.get('category'),
                download_limit=(torrent_data.get('dl_limit') or 0) / 1024,
                upload_limit=(torrent_data.get('up_limit') or 0) / 1024,
                ratio_limit=torrent_data.get('ratio_limit'),
                seeding_time_limit=torrent_data.get('seeding_time_limit'),
                progress=(torrent_data.get('progress') or 0) * 100,
                state=self.__normalize_torrent_state(torrent_data.get('state')),
                dlspeed=StringUtils.str_filesize(dlspeed),
                upspeed=StringUtils.str_filesize(torrent_data.get('upspeed')),
                left_time=StringUtils.str_secends(
                    (total_size - completed_size) / dlspeed
                ) if dlspeed > 0 else '',
            )

        if hashs:
            # 按Hash获取
            for name, server in servers.items():
                torrents, _ = server.get_torrents(ids=hashs, tags=query_tags) or []
                try:
                    for torrent_info in torrents:
                        ret_torrents.append(__build_torrent(name, torrent_info))
                finally:
                    torrents.clear()
                    del torrents
        elif query_status == TorrentQueryStatus.TRANSFER:
            # 获取已完成且未整理的
            for name, server in servers.items():
                torrents = server.get_completed_torrents(tags=query_tags) or []
                try:
                    for torrent_info in torrents:
                        tags = torrent_info.get("tags") or []
                        if "已整理" in tags:
                            continue
                        ret_torrents.append(__build_torrent(name, torrent_info))
                finally:
                    torrents.clear()
                    del torrents
        elif query_status == TorrentQueryStatus.DOWNLOADING:
            # 获取正在下载的任务
            for name, server in servers.items():
                torrents = server.get_downloading_torrents(tags=query_tags) or []
                try:
                    for torrent_info in torrents:
                        ret_torrents.append(__build_torrent(name, torrent_info))
                finally:
                    torrents.clear()
                    del torrents
        elif query_status in (
                TorrentQueryStatus.ALL,
                TorrentQueryStatus.COMPLETED,
                TorrentQueryStatus.PAUSED,
        ):
            # 获取完整任务列表，由 MoviePilot 统一归一实际下载器状态。
            for name, server in servers.items():
                torrents, _ = server.get_torrents(tags=query_tags) or []
                try:
                    for torrent_info in torrents:
                        torrent_state = self.__normalize_torrent_state(torrent_info.get('state'))
                        if (
                                query_status != TorrentQueryStatus.ALL
                                and torrent_state != query_status.value
                        ):
                            continue
                        ret_torrents.append(__build_torrent(name, torrent_info))
                finally:
                    torrents.clear()
                    del torrents
        else:
            return None
        return ret_torrents  # noqa

    @staticmethod
    def __normalize_query_status(
            status: Optional[Union[TorrentStatus, TorrentQueryStatus, str]]
    ) -> TorrentQueryStatus:
        """
        归一任务查询状态。
        """
        status_value = getattr(status, "value", status)
        status_text = str(status_value or "").strip().lower()
        if not status_text or status_text in {"all", "全部"}:
            return TorrentQueryStatus.ALL
        if status_text in {
            TorrentStatus.TRANSFER.value,
            TorrentQueryStatus.TRANSFER.value,
            "transfer",
        }:
            return TorrentQueryStatus.TRANSFER
        if status_text in {
            TorrentStatus.DOWNLOADING.value,
            TorrentQueryStatus.DOWNLOADING.value,
            "downloading",
        }:
            return TorrentQueryStatus.DOWNLOADING
        if status_text in {
            TorrentQueryStatus.COMPLETED.value,
            "complete",
            "seeding",
            "完成",
            "已完成",
        }:
            return TorrentQueryStatus.COMPLETED
        if status_text in {TorrentQueryStatus.PAUSED.value, "pause", "暂停", "已暂停"}:
            return TorrentQueryStatus.PAUSED
        return TorrentQueryStatus.ALL

    @staticmethod
    def __normalize_torrent_state(state: Optional[Union[str, int]]) -> str:
        """
        归一 qBittorrent 原始任务状态。
        """
        state_text = str(state or "").strip().lower()
        if state_text in _QBITTORRENT_PAUSED_STATES:
            return DownloadTaskState.PAUSED.value
        if state_text in _QBITTORRENT_DOWNLOADING_STATES:
            return DownloadTaskState.DOWNLOADING.value
        return DownloadTaskState.COMPLETED.value

    def transfer_completed(self, hashs: str, downloader: Optional[str] = None) -> None:
        """
        转移完成后的处理
        :param hashs:  种子Hash
        :param downloader:  下载器
        """
        server: Qbittorrent = self.get_instance(downloader)
        if not server:
            return None
        server.set_torrents_tag(ids=hashs, tags=['已整理'])
        return None

    def remove_torrents(self, hashs: Union[str, list], delete_file: Optional[bool] = True,
                        downloader: Optional[str] = None) -> Optional[bool]:
        """
        删除下载器种子
        :param hashs:  种子Hash
        :param delete_file:  是否删除文件
        :param downloader:  下载器
        :return: bool
        """
        server: Qbittorrent = self.get_instance(downloader)
        if not server:
            return None
        return server.delete_torrents(delete_file=delete_file, ids=hashs)

    def set_torrents_tag(self, hashs: Union[str, list], tags: list,
                        downloader: Optional[str] = None) -> Optional[bool]:
        """
        设置种子标签
        :param hashs:  种子Hash
        :param tags:  标签列表
        :param downloader:  下载器
        :return: bool
        """
        server: Qbittorrent = self.get_instance(downloader)
        if not server:
            return None
        server.set_torrents_tag(ids=hashs, tags=tags)
        return True

    def update_torrent(
            self,
            hash_string: str,
            downloader: Optional[str] = None,
            download_limit: Optional[float] = None,
            upload_limit: Optional[float] = None,
            tracker_list: Optional[list] = None,
            save_path: Optional[str] = None,
            category: Optional[str] = None,
            ratio_limit: Optional[float] = None,
            seeding_time_limit: Optional[int] = None,
    ) -> Optional[Dict[str, bool]]:
        """
        修改下载任务属性。
        :param hash_string: 种子Hash
        :param downloader: 下载器
        :param download_limit: 下载限速，单位 KB/s
        :param upload_limit: 上传限速，单位 KB/s
        :param tracker_list: Tracker URL列表
        :param save_path: 保存目录
        :param category: 分类
        :param ratio_limit: 分享率限制
        :param seeding_time_limit: 做种时间限制，单位分钟
        :return: 各项修改结果
        """
        server: Qbittorrent = self.get_instance(downloader)
        if not server:
            return None
        results = {}
        if any(
                value is not None
                for value in (download_limit, upload_limit, ratio_limit, seeding_time_limit)
        ):
            results["limits"] = server.change_torrent(
                hash_string=hash_string,
                download_limit=download_limit,
                upload_limit=upload_limit,
                ratio_limit=ratio_limit,
                seeding_time_limit=seeding_time_limit,
            )
        if tracker_list is not None:
            results["trackers"] = server.update_tracker(
                hash_string=hash_string, tracker_list=tracker_list
            )
        if save_path is not None:
            results["save_path"] = server.set_torrent_location(
                hash_string=hash_string,
                location=self.normalize_path(Path(save_path), downloader),
            )
        if category is not None:
            results["category"] = server.set_torrent_category(
                hash_string=hash_string, category=category
            )
        return results

    def get_torrent_trackers(
            self,
            hash_string: str,
            downloader: Optional[str] = None,
    ) -> Optional[Dict[str, List[str]]]:
        """
        查询下载任务Tracker列表。
        :param hash_string: 种子Hash
        :param downloader: 下载器
        :return: 下载器名称到Tracker列表的映射
        """
        if downloader:
            server: Qbittorrent = self.get_instance(downloader)
            if not server:
                return None
            servers = {downloader: server}
        else:
            servers: Dict[str, Qbittorrent] = self.get_instances()
        ret_trackers = {}
        for name, server in servers.items():
            trackers = server.get_trackers(hash_string)
            if trackers is not None:
                ret_trackers[name] = trackers
        return ret_trackers

    def start_torrents(self, hashs: Union[list, str],
                       downloader: Optional[str] = None) -> Optional[bool]:
        """
        开始下载
        :param hashs:  种子Hash
        :param downloader:  下载器
        :return: bool
        """
        server: Qbittorrent = self.get_instance(downloader)
        if not server:
            return None
        return server.start_torrents(ids=hashs)

    def stop_torrents(self, hashs: Union[list, str], downloader: Optional[str] = None) -> Optional[bool]:
        """
        停止下载
        :param hashs:  种子Hash
        :param downloader:  下载器
        :return: bool
        """
        server: Qbittorrent = self.get_instance(downloader)
        if not server:
            return None
        return server.stop_torrents(ids=hashs)

    def torrent_files(self, tid: str, downloader: Optional[str] = None) -> Optional[TorrentFilesList]:
        """
        获取种子文件列表
        """
        server: Qbittorrent = self.get_instance(downloader)
        if not server:
            return None
        return server.get_files(tid=tid)

    def downloader_info(self, downloader: Optional[str] = None) -> Optional[List[schemas.DownloaderInfo]]:
        """
        下载器信息
        """
        if downloader:
            server: Qbittorrent = self.get_instance(downloader)
            if not server:
                return None
            servers = [server]
        else:
            servers = self.get_instances().values()
        # 调用Qbittorrent API查询实时信息
        ret_info = []
        for server in servers:
            info = server.transfer_info()
            if not info:
                continue
            ret_info.append(schemas.DownloaderInfo(
                download_speed=info.get("dl_info_speed"),
                upload_speed=info.get("up_info_speed"),
                download_size=info.get("dl_info_data"),
                upload_size=info.get("up_info_data")
            ))
        return ret_info
