import base64
import json
from typing import List, Optional, Tuple

from app.runtime.settings import RuntimeSettingsCompat

settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.types import MediaType
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
from app.foundation import temporal as time_tools


class YemaSpider:
    """
    YemaPT 开放 API 索引器
    """

    # YemaPT 开放 API 使用更大的分页容量时会返回空结果。
    _size = 40
    _movie_category = [4]
    _tv_category = [5, 6, 13, 14, 15, 17]
    _music_category = [8, 16]

    _labels = {
        "1": "禁转",
        "2": "首发",
        "3": "官方",
        "4": "自制",
        "5": "国语",
        "6": "中字",
        "7": "粤语",
        "8": "英字",
        "9": "HDR10",
        "10": "杜比视界",
        "11": "分集",
        "12": "完结",
    }

    def __init__(self, indexer: dict):
        """
        初始化 YemaPT 开放 API 索引器

        :param indexer: 合并站点认证信息后的索引配置
        """
        indexer = indexer or {}
        self._name = indexer.get("name") or "YemaPT"
        self._site_url = str(indexer.get("domain") or "https://www.yemapt.org/").rstrip("/")
        self._proxy = settings.PROXY if indexer.get("proxy") else None
        self._use_proxy = bool(indexer.get("proxy"))
        self._user_agent = indexer.get("ua") or settings.USER_AGENT
        self._api_key = indexer.get("apikey")
        self._timeout = indexer.get("timeout") or 15
        self._search_url = f"{self._site_url}/openApi/torrent/fetchOpenTorrentList.json"
        self._download_key_url = f"{self._site_url}/openApi/torrent/generateDownloadKey.json"

    @classmethod
    def get_search_page_size(cls, keyword: Optional[str] = None) -> Optional[int]:
        """
        获取搜索接口单页容量

        :param keyword: 搜索关键字，YemaPT 不按关键字改变分页容量
        :return: 搜索接口单页容量
        """
        return cls._size

    def _request_headers(self) -> dict:
        """
        构造开放 API 请求头

        :return: 不包含 Cookie 的 AuthKey 请求头
        """
        return {
            "Authorization": self._api_key,
            "Content-Type": "application/json",
            "Accept": "application/json, text/plain, */*",
            "User-Agent": self._user_agent,
        }

    def _build_params(
        self,
        keyword: Optional[str],
        page: Optional[int],
        category_id: Optional[int] = None,
    ) -> dict:
        """
        构造公开种子列表查询参数

        :param keyword: 搜索关键字
        :param page: MoviePilot 从 0 开始的页码
        :param category_id: 可选的站点分类 ID
        :return: YemaPT 开放 API 请求体
        """
        params = {
            "pageParam": {
                "current": int(page or 0) + 1,
                "pageSize": self._size,
            },
            "sorter": {},
        }
        if keyword:
            params["keyword"] = keyword
        if category_id is not None:
            params["categoryId"] = category_id
        return params

    def _search_category_ids(self, mtype: MediaType = None) -> List[Optional[int]]:
        """
        返回当前媒体类型需要分别查询的站点分类。

        YemaPT 分类参数不支持数组，音乐需要分别查询音频音乐和 MV/演唱会分类。
        """
        if mtype == MediaType.MOVIE:
            return list(self._movie_category)
        if mtype == MediaType.TV:
            return list(self._tv_category)
        if mtype == MediaType.MUSIC:
            return list(self._music_category)
        return [None]

    @staticmethod
    def _merge_search_results(result_groups: List[List[dict]]) -> List[dict]:
        """按种子详情链接合并多个分类查询结果并保持首次出现顺序。"""
        merged = []
        seen = set()
        for results in result_groups:
            for torrent in results:
                identity = torrent.get("page_url") or torrent.get("enclosure")
                if identity and identity in seen:
                    continue
                if identity:
                    seen.add(identity)
                merged.append(torrent)
        return merged

    def _parse_result(self, results: List[dict]) -> List[dict]:
        """
        将开放 API 种子数据转换为 MoviePilot 标准字段

        :param results: 公开种子列表接口 data 数组
        :return: MoviePilot 标准种子字典列表
        """
        torrents = []
        for result in results or []:
            if not isinstance(result, dict):
                continue
            category_value = result.get("categoryId")
            if category_value in self._tv_category:
                category = MediaType.TV.value
            elif category_value in self._movie_category:
                category = MediaType.MOVIE.value
            elif category_value in self._music_category:
                category = MediaType.MUSIC.value
            else:
                category = MediaType.UNKNOWN.value

            labels = [
                self._labels[label_id]
                for label_id in result.get("tagList") or []
                if label_id in self._labels
            ]
            torrent_id = result.get("id")
            torrents.append({
                "title": result.get("showName"),
                "description": result.get("shortDesc"),
                "enclosure": self._build_download_url(torrent_id),
                "pubdate": time_tools.normalize_datetime(result.get("listingTime")),
                "size": result.get("fileSize"),
                "seeders": result.get("seedNum"),
                "peers": result.get("leechNum"),
                "grabs": result.get("completedNum"),
                "downloadvolumefactor": self._download_factor(result.get("downloadPromotion")),
                "uploadvolumefactor": self._upload_factor(result.get("uploadPromotion")),
                "freedate": time_tools.normalize_datetime(result.get("downloadPromotionEndTime")),
                "page_url": f"{self._site_url}/#/torrent/detail/{torrent_id}/",
                "labels": labels,
                "hit_and_run": bool(result.get("hrPunishEnable")),
                "category": category,
            })
        return torrents

    def _process_search_response(self, response) -> Tuple[bool, List[dict]]:
        """
        校验开放 API 通用响应并解析搜索结果

        :param response: RequestUtils 返回的响应对象
        :return: 是否失败及标准种子列表
        """
        if response is None:
            logger.warning(f"{self._name} 搜索失败，无法连接开放 API")
            return True, []
        if response.status_code != 200:
            logger.warning(f"{self._name} 搜索失败，HTTP 错误码：{response.status_code}")
            return True, []
        try:
            payload = response.json() or {}
        except (TypeError, ValueError) as err:
            logger.warning(f"{self._name} 搜索响应不是有效 JSON：{str(err)}")
            return True, []
        if not isinstance(payload, dict):
            logger.warning(f"{self._name} 搜索响应结构无效")
            return True, []
        if not payload.get("success"):
            logger.warning(f"{self._name} 搜索失败：{payload.get('errorMessage') or '未知错误'}")
            return True, []
        results = payload.get("data")
        if not isinstance(results, list):
            logger.warning(f"{self._name} 搜索响应 data 不是数组")
            return True, []
        return False, self._parse_result(results)

    def search(
        self,
        keyword: Optional[str],
        mtype: MediaType = None,
        page: Optional[int] = 0,
    ) -> Tuple[bool, List[dict]]:
        """
        同步搜索 YemaPT 公开种子

        :param keyword: 搜索关键字
        :param mtype: MoviePilot 媒体类型，指定后按真实站点分类分别查询
        :param page: MoviePilot 从 0 开始的页码
        :return: 是否失败及标准种子列表
        """
        if not self._api_key:
            logger.warning(f"{self._name} 未配置 API AuthKey")
            return True, []
        request = RequestUtils(
            headers=self._request_headers(),
            proxies=self._proxy,
            timeout=self._timeout,
        )
        result_groups = []
        errors = []
        for category_id in self._search_category_ids(mtype):
            response = request.post_res(
                url=self._search_url,
                json=self._build_params(keyword, page, category_id),
            )
            error, results = self._process_search_response(response)
            errors.append(error)
            if not error:
                result_groups.append(results)
        return all(errors), self._merge_search_results(result_groups)

    async def async_search(
        self,
        keyword: Optional[str],
        mtype: MediaType = None,
        page: Optional[int] = 0,
    ) -> Tuple[bool, List[dict]]:
        """
        异步搜索 YemaPT 公开种子

        :param keyword: 搜索关键字
        :param mtype: MoviePilot 媒体类型，指定后按真实站点分类分别查询
        :param page: MoviePilot 从 0 开始的页码
        :return: 是否失败及标准种子列表
        """
        if not self._api_key:
            logger.warning(f"{self._name} 未配置 API AuthKey")
            return True, []
        request = AsyncRequestUtils(
            headers=self._request_headers(),
            proxies=self._proxy,
            timeout=self._timeout,
        )
        result_groups = []
        errors = []
        for category_id in self._search_category_ids(mtype):
            response = await request.post_res(
                url=self._search_url,
                json=self._build_params(keyword, page, category_id),
            )
            error, results = self._process_search_response(response)
            errors.append(error)
            if not error:
                result_groups.append(results)
        return all(errors), self._merge_search_results(result_groups)

    @staticmethod
    def _download_factor(promotion: str) -> float:
        """
        转换下载促销类型

        :param promotion: 开放 API 下载促销枚举
        :return: MoviePilot 下载系数
        """
        return {
            "free": 0,
            "half": 0.5,
            "none": 1,
        }.get(promotion, 1)

    @staticmethod
    def _upload_factor(promotion: str) -> float:
        """
        转换上传促销类型

        :param promotion: 开放 API 上传促销枚举
        :return: MoviePilot 上传系数
        """
        return {
            "none": 1,
            "one_half": 1.5,
            "double_upload": 2,
        }.get(promotion, 1)

    def _build_download_url(self, torrent_id: int) -> str:
        """
        构造先生成下载凭证再获取种子文件的两段式链接

        :param torrent_id: YemaPT 种子 ID
        :return: Base64 请求配置与下载凭证接口 URL
        """
        request_config = {
            "method": "post",
            "cookie": False,
            "header": {
                "Authorization": self._api_key,
                "Accept": "application/json",
            },
            "params": {"id": torrent_id},
            "proxy": self._use_proxy,
            "success": "success",
            "result": "data",
            "result_base_url": self._site_url,
            "result_path": "api/torrent/download1",
            "result_query_param": "token",
        }
        encoded_config = base64.b64encode(
            json.dumps(request_config).encode("utf-8")
        ).decode("ascii")
        return f"[{encoded_config}]{self._download_key_url}"
