from datetime import date
from typing import Optional

from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat

settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.adapters.network.http import AsyncRequestUtils, RequestUtils


class AniListApi:
    """
    AniList 中文 GraphQL API 客户端
    """

    _base_url = "https://trace.moe/anilist/"
    _official_url = "https://graphql.anilist.co"
    _translations_url = (
        "https://raw.githubusercontent.com/soruly/anilist-chinese/"
        "master/anilist-chinese.json"
    )
    _media_summary_fields = """
        id
        idMal
        title { romaji english native }
        format
        status
        description(asHtml: false)
        startDate { year month day }
        endDate { year month day }
        season
        seasonYear
        episodes
        duration
        countryOfOrigin
        coverImage { extraLarge large }
        bannerImage
        genres
        synonyms
        averageScore
        popularity
        isAdult
        siteUrl
        studios(isMain: true) { nodes { name } }
    """
    _media_fields = f"""
        {_media_summary_fields}
        staff(perPage: 25, sort: [RELEVANCE]) {{
          edges {{ role node {{ id name {{ full native }} image {{ large }} siteUrl }} }}
        }}
        characters(perPage: 25, sort: [ROLE, RELEVANCE]) {{
          edges {{
            role
            node {{ id name {{ full native }} image {{ large }} siteUrl }}
            voiceActors(language: JAPANESE, sort: [RELEVANCE]) {{
              id
              name {{ full native alternative }}
              image {{ large medium }}
              siteUrl
            }}
          }}
        }}
        externalLinks {{ site url type }}
    """
    _page_query = f"""
        query (
          $page: Int!,
          $count: Int!,
          $search: String,
          $genre: String,
          $format: MediaFormat,
          $season: MediaSeason,
          $seasonYear: Int,
          $status: MediaStatus,
          $country: CountryCode,
          $sort: [MediaSort]
        ) {{
          Page(page: $page, perPage: $count) {{
            media(
              search: $search,
              type: ANIME,
              genre: $genre,
              format: $format,
              season: $season,
              seasonYear: $seasonYear,
              status: $status,
              countryOfOrigin: $country,
              isAdult: false,
              sort: $sort
            ) {{ {_media_summary_fields} }}
          }}
        }}
    """
    _media_by_ids_query = f"""
        query ($ids: [Int!]!, $count: Int!) {{
          Page(page: 1, perPage: $count) {{
            media(id_in: $ids, type: ANIME) {{ {_media_summary_fields} }}
          }}
        }}
    """

    def __init__(self) -> None:
        """初始化同步与异步请求客户端"""
        headers = {
            "User-Agent": settings.NORMAL_USER_AGENT,
            "Accept": "application/json",
            "Content-Type": "application/json",
        }
        self._request = RequestUtils(
            proxies=settings.PROXY,
            headers=headers,
        )
        self._async_request = AsyncRequestUtils(
            proxies=settings.PROXY,
            headers=headers,
        )
        self._proxy_available = True
        self._translations: Optional[dict[int, dict]] = None

    @staticmethod
    def _extract_response(response) -> Optional[dict]:
        """
        提取 GraphQL 响应数据并统一处理上游错误。

        :param response: HTTP 响应对象
        :return: GraphQL data 字段
        """
        if response is None or response.status_code != 200:
            return None
        try:
            result = response.json()
        except Exception as err:
            logger.error(f"解析 AniList 响应失败：{str(err)}")
            return None
        if result.get("errors"):
            logger.warning(f"AniList 接口返回错误：{result.get('errors')}")
            return None
        return result.get("data")

    def _invoke(self, query: str, variables: dict) -> Optional[dict]:
        """
        执行同步 GraphQL 请求。

        :param query: GraphQL 查询
        :param variables: 查询变量
        :return: GraphQL data 字段
        """
        payload = {"query": query, "variables": variables}
        if self._proxy_available:
            response = self._request.post_res(self._base_url, json=payload)
            result = self._extract_response(response)
            if result is not None:
                return self._inject_chinese(result, self._translation_map())
            self._disable_proxy(response)
        response = self._request.post_res(self._official_url, json=payload)
        result = self._extract_response(response)
        return self._inject_chinese(result, self._translation_map()) if result else result

    async def _async_invoke(self, query: str, variables: dict) -> Optional[dict]:
        """
        执行异步 GraphQL 请求。

        :param query: GraphQL 查询
        :param variables: 查询变量
        :return: GraphQL data 字段
        """
        payload = {"query": query, "variables": variables}
        if self._proxy_available:
            response = await self._async_request.post_res(self._base_url, json=payload)
            result = self._extract_response(response)
            if result is not None:
                translations = await self._async_translation_map()
                return self._inject_chinese(result, translations)
            self._disable_proxy(response)
        response = await self._async_request.post_res(self._official_url, json=payload)
        result = self._extract_response(response)
        if not result:
            return result
        translations = await self._async_translation_map()
        return self._inject_chinese(result, translations)

    def _disable_proxy(self, response) -> None:
        """
        标记中文代理不可用，避免当前进程持续请求已失效的上游。

        :param response: 中文代理响应对象
        """
        self._proxy_available = False
        status_code = getattr(response, "status_code", None)
        logger.warning(
            f"anilist-chinese 代理不可用（HTTP {status_code}），"
            "改用 AniList 官方接口并合并中文数据集"
        )

    @staticmethod
    def _build_translation_map(items) -> dict[int, dict]:
        """
        将 anilist-chinese 数据集转换为按 AniList ID 索引的字典。

        :param items: anilist-chinese JSON 数据
        :return: 中文标题数据索引
        """
        if not isinstance(items, list):
            return {}
        return {
            item.get("id"): item
            for item in items
            if isinstance(item, dict) and item.get("id")
        }

    def _translation_map(self) -> dict[int, dict]:
        """
        同步加载并复用 anilist-chinese 中文标题数据。

        :return: 中文标题数据索引
        """
        if self._translations is None:
            items = self._request.get_json(self._translations_url)
            self._translations = self._build_translation_map(items)
            if not self._translations:
                logger.warning("加载 anilist-chinese 中文数据集失败")
        return self._translations

    async def _async_translation_map(self) -> dict[int, dict]:
        """
        异步加载并复用 anilist-chinese 中文标题数据。

        :return: 中文标题数据索引
        """
        if self._translations is None:
            items = await self._async_request.get_json(self._translations_url)
            self._translations = self._build_translation_map(items)
            if not self._translations:
                logger.warning("加载 anilist-chinese 中文数据集失败")
        return self._translations

    @classmethod
    def _inject_chinese(cls, value, translations: dict[int, dict]):
        """
        递归合并 anilist-chinese 标题，覆盖代理不会处理的嵌套媒体。

        :param value: AniList GraphQL data 字段或其子节点
        :param translations: 中文标题数据索引
        :return: 合并中文标题后的原数据结构
        """
        if isinstance(value, list):
            for item in value:
                cls._inject_chinese(item, translations)
            return value
        if not isinstance(value, dict):
            return value

        translation = translations.get(value.get("id"))
        title = value.get("title")
        if translation and isinstance(title, dict):
            title["chinese"] = translation.get("title")
        synonyms = value.get("synonyms")
        if translation and isinstance(synonyms, list):
            value["synonyms"] = list(
                dict.fromkeys([*synonyms, *(translation.get("synonyms") or [])])
            )
        for child in value.values():
            cls._inject_chinese(child, translations)
        return value

    @staticmethod
    def _page_variables(
        page: int,
        count: int,
        search: Optional[str] = None,
        genre: Optional[str] = None,
        media_format: Optional[str] = None,
        season: Optional[str] = None,
        season_year: Optional[int] = None,
        status: Optional[str] = None,
        country: Optional[str] = None,
        sort: Optional[str] = None,
    ) -> dict:
        """
        构造 AniList 分页媒体查询变量。

        :return: 去除空值后的 GraphQL 变量
        """
        variables = {
            "page": page,
            "count": count,
            "search": search,
            "genre": genre,
            "format": media_format,
            "season": season,
            "seasonYear": season_year,
            "status": status,
            "country": country,
            "sort": [sort] if sort else ["POPULARITY_DESC"],
        }
        return {key: value for key, value in variables.items() if value is not None}

    @staticmethod
    def _page_medias(result: Optional[dict]) -> list[dict]:
        """
        从分页响应中提取媒体列表。

        :param result: GraphQL data 字段
        :return: AniList 媒体列表
        """
        return result.get("Page", {}).get("media") or [] if result else []

    @staticmethod
    def _ordered_medias(media_ids: list[int], medias: list[dict]) -> list[dict]:
        """
        按上游关系顺序重排批量查询返回的媒体。

        :param media_ids: 关系查询返回的 AniList 媒体 ID
        :param medias: Page.media 批量查询结果
        :return: 保持原关系顺序的媒体列表
        """
        media_map = {media.get("id"): media for media in medias if media.get("id")}
        return [media_map[media_id] for media_id in media_ids if media_id in media_map]

    def _medias_by_ids(self, media_ids: list[int]) -> list[dict]:
        """
        通过根级 Page.media 批量查询媒体，使中文代理能够注入标题。

        :param media_ids: AniList 媒体 ID 列表
        :return: 按输入顺序排列的媒体列表
        """
        unique_ids = list(dict.fromkeys(media_id for media_id in media_ids if media_id))
        if not unique_ids:
            return []
        result = self._invoke(
            self._media_by_ids_query,
            {"ids": unique_ids, "count": len(unique_ids)},
        )
        return self._ordered_medias(media_ids, self._page_medias(result))

    async def _async_medias_by_ids(self, media_ids: list[int]) -> list[dict]:
        """
        异步通过根级 Page.media 批量查询媒体，使中文代理能够注入标题。

        :param media_ids: AniList 媒体 ID 列表
        :return: 按输入顺序排列的媒体列表
        """
        unique_ids = list(dict.fromkeys(media_id for media_id in media_ids if media_id))
        if not unique_ids:
            return []
        result = await self._async_invoke(
            self._media_by_ids_query,
            {"ids": unique_ids, "count": len(unique_ids)},
        )
        return self._ordered_medias(media_ids, self._page_medias(result))

    @staticmethod
    def _current_season(today: Optional[date] = None) -> tuple[str, int]:
        """
        根据当前日期计算 AniList 季度与年份。

        :param today: 用于测试或指定季度的日期
        :return: AniList 季度枚举和年份
        """
        current = today or date.today()
        seasons = ("WINTER", "SPRING", "SUMMER", "FALL")
        return seasons[(current.month - 1) // 3], current.year

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="detail",
    )
    def detail(self, anilist_id: int) -> Optional[dict]:
        """
        根据 AniList ID 获取动画详情。

        :param anilist_id: AniList 媒体 ID
        :return: AniList 媒体详情
        """
        query = f"query ($id: Int!) {{ Media(id: $id, type: ANIME) {{ {self._media_fields} }} }}"
        result = self._invoke(query, {"id": anilist_id})
        return result.get("Media") if result else None

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="detail",
    )
    async def async_detail(self, anilist_id: int) -> Optional[dict]:
        """
        异步根据 AniList ID 获取动画详情。

        :param anilist_id: AniList 媒体 ID
        :return: AniList 媒体详情
        """
        query = f"query ($id: Int!) {{ Media(id: $id, type: ANIME) {{ {self._media_fields} }} }}"
        result = await self._async_invoke(query, {"id": anilist_id})
        return result.get("Media") if result else None

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="search",
    )
    def search(self, name: str, count: int = 20) -> list[dict]:
        """
        按标题搜索 AniList 动画。

        :param name: 动画标题
        :param count: 返回条数
        :return: AniList 媒体列表
        """
        query = f"""
            query ($search: String!, $count: Int!) {{
              Page(page: 1, perPage: $count) {{
                media(search: $search, type: ANIME, sort: SEARCH_MATCH) {{ {self._media_fields} }}
              }}
            }}
        """
        result = self._invoke(query, {"search": name, "count": count})
        return self._page_medias(result)

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="search",
    )
    async def async_search(self, name: str, count: int = 20) -> list[dict]:
        """
        异步按标题搜索 AniList 动画。

        :param name: 动画标题
        :param count: 返回条数
        :return: AniList 媒体列表
        """
        query = f"""
            query ($search: String!, $count: Int!) {{
              Page(page: 1, perPage: $count) {{
                media(search: $search, type: ANIME, sort: SEARCH_MATCH) {{ {self._media_fields} }}
              }}
            }}
        """
        result = await self._async_invoke(query, {"search": name, "count": count})
        return self._page_medias(result)

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="discover",
    )
    def discover(
        self,
        page: int = 1,
        count: int = 20,
        search: Optional[str] = None,
        genre: Optional[str] = None,
        media_format: Optional[str] = None,
        season: Optional[str] = None,
        season_year: Optional[int] = None,
        status: Optional[str] = None,
        country: Optional[str] = None,
        sort: Optional[str] = None,
    ) -> list[dict]:
        """
        按组合条件探索 AniList 动画。

        :return: AniList 媒体列表
        """
        variables = self._page_variables(
            page=page,
            count=count,
            search=search,
            genre=genre,
            media_format=media_format,
            season=season,
            season_year=season_year,
            status=status,
            country=country,
            sort=sort,
        )
        return self._page_medias(self._invoke(self._page_query, variables))

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="discover",
    )
    async def async_discover(
        self,
        page: int = 1,
        count: int = 20,
        search: Optional[str] = None,
        genre: Optional[str] = None,
        media_format: Optional[str] = None,
        season: Optional[str] = None,
        season_year: Optional[int] = None,
        status: Optional[str] = None,
        country: Optional[str] = None,
        sort: Optional[str] = None,
    ) -> list[dict]:
        """
        异步按组合条件探索 AniList 动画。

        :return: AniList 媒体列表
        """
        variables = self._page_variables(
            page=page,
            count=count,
            search=search,
            genre=genre,
            media_format=media_format,
            season=season,
            season_year=season_year,
            status=status,
            country=country,
            sort=sort,
        )
        result = await self._async_invoke(self._page_query, variables)
        return self._page_medias(result)

    def trending(self, page: int = 1, count: int = 20) -> list[dict]:
        """
        获取 AniList 当前趋势榜。

        :param page: 页码
        :param count: 每页条数
        :return: AniList 媒体列表
        """
        return self.discover(page=page, count=count, sort="TRENDING_DESC")

    async def async_trending(self, page: int = 1, count: int = 20) -> list[dict]:
        """
        异步获取 AniList 当前趋势榜。

        :param page: 页码
        :param count: 每页条数
        :return: AniList 媒体列表
        """
        return await self.async_discover(page=page, count=count, sort="TRENDING_DESC")

    def popular_this_season(self, page: int = 1, count: int = 20) -> list[dict]:
        """
        获取 AniList 本季热门榜。

        :param page: 页码
        :param count: 每页条数
        :return: AniList 媒体列表
        """
        season, season_year = self._current_season()
        return self.discover(
            page=page,
            count=count,
            season=season,
            season_year=season_year,
            sort="POPULARITY_DESC",
        )

    async def async_popular_this_season(self, page: int = 1, count: int = 20) -> list[dict]:
        """
        异步获取 AniList 本季热门榜。

        :param page: 页码
        :param count: 每页条数
        :return: AniList 媒体列表
        """
        season, season_year = self._current_season()
        return await self.async_discover(
            page=page,
            count=count,
            season=season,
            season_year=season_year,
            sort="POPULARITY_DESC",
        )

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="credits",
    )
    def credits(self, anilist_id: int, page: int = 1, count: int = 20) -> list[dict]:
        """
        获取 AniList 动画的日语配音演员。

        :return: AniList 人物边列表
        """
        query = """
            query ($id: Int!, $page: Int!, $count: Int!) {
              Media(id: $id, type: ANIME) {
                characters(page: $page, perPage: $count, sort: [ROLE, RELEVANCE]) {
                  edges {
                    role
                    node { id name { full native } }
                    voiceActors(language: JAPANESE, sort: [RELEVANCE]) {
                      id name { full native alternative } image { large medium } siteUrl
                    }
                  }
                }
              }
            }
        """
        result = self._invoke(query, {"id": anilist_id, "page": page, "count": count})
        return result.get("Media", {}).get("characters", {}).get("edges") or [] if result else []

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="credits",
    )
    async def async_credits(self, anilist_id: int, page: int = 1, count: int = 20) -> list[dict]:
        """
        异步获取 AniList 动画的日语配音演员。

        :return: AniList 人物边列表
        """
        query = """
            query ($id: Int!, $page: Int!, $count: Int!) {
              Media(id: $id, type: ANIME) {
                characters(page: $page, perPage: $count, sort: [ROLE, RELEVANCE]) {
                  edges {
                    role
                    node { id name { full native } }
                    voiceActors(language: JAPANESE, sort: [RELEVANCE]) {
                      id name { full native alternative } image { large medium } siteUrl
                    }
                  }
                }
              }
            }
        """
        result = await self._async_invoke(query, {"id": anilist_id, "page": page, "count": count})
        return result.get("Media", {}).get("characters", {}).get("edges") or [] if result else []

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="recommendations",
    )
    def recommendations(self, anilist_id: int, page: int = 1, count: int = 20) -> list[dict]:
        """
        获取 AniList 动画相关推荐。

        :return: AniList 媒体列表
        """
        query = """
            query ($id: Int!, $page: Int!, $count: Int!) {
              Media(id: $id, type: ANIME) {
                recommendations(page: $page, perPage: $count, sort: [RATING_DESC, ID]) {
                  nodes { mediaRecommendation { id } }
                }
              }
            }
        """
        result = self._invoke(query, {"id": anilist_id, "page": page, "count": count})
        nodes = result.get("Media", {}).get("recommendations", {}).get("nodes") or [] if result else []
        media_ids = [node.get("mediaRecommendation", {}).get("id") for node in nodes]
        return self._medias_by_ids(media_ids)

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="recommendations",
    )
    async def async_recommendations(self, anilist_id: int, page: int = 1, count: int = 20) -> list[dict]:
        """
        异步获取 AniList 动画相关推荐。

        :return: AniList 媒体列表
        """
        query = """
            query ($id: Int!, $page: Int!, $count: Int!) {
              Media(id: $id, type: ANIME) {
                recommendations(page: $page, perPage: $count, sort: [RATING_DESC, ID]) {
                  nodes { mediaRecommendation { id } }
                }
              }
            }
        """
        result = await self._async_invoke(query, {"id": anilist_id, "page": page, "count": count})
        nodes = result.get("Media", {}).get("recommendations", {}).get("nodes") or [] if result else []
        media_ids = [node.get("mediaRecommendation", {}).get("id") for node in nodes]
        return await self._async_medias_by_ids(media_ids)

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="person_detail",
    )
    def person_detail(self, person_id: int) -> Optional[dict]:
        """
        获取 AniList 演员详情。

        :param person_id: AniList 人物 ID
        :return: AniList 人物详情
        """
        query = """
            query ($id: Int!) {
              Staff(id: $id) {
                id name { full native alternative } image { large medium }
                description(asHtml: false) dateOfBirth { year month day }
                dateOfDeath { year month day } gender homeTown primaryOccupations siteUrl
              }
            }
        """
        result = self._invoke(query, {"id": person_id})
        return result.get("Staff") if result else None

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="person_detail",
    )
    async def async_person_detail(self, person_id: int) -> Optional[dict]:
        """
        异步获取 AniList 演员详情。

        :param person_id: AniList 人物 ID
        :return: AniList 人物详情
        """
        query = """
            query ($id: Int!) {
              Staff(id: $id) {
                id name { full native alternative } image { large medium }
                description(asHtml: false) dateOfBirth { year month day }
                dateOfDeath { year month day } gender homeTown primaryOccupations siteUrl
              }
            }
        """
        result = await self._async_invoke(query, {"id": person_id})
        return result.get("Staff") if result else None

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="person_credits",
    )
    def person_credits(self, person_id: int, page: int = 1, count: int = 20) -> list[dict]:
        """
        获取 AniList 演员参与的动画作品。

        :return: AniList 媒体列表
        """
        query = """
            query ($id: Int!, $page: Int!, $count: Int!) {
              Staff(id: $id) {
                characterMedia(page: $page, perPage: $count, sort: [POPULARITY_DESC]) {
                  nodes { id }
                }
              }
            }
        """
        result = self._invoke(query, {"id": person_id, "page": page, "count": count})
        nodes = result.get("Staff", {}).get("characterMedia", {}).get("nodes") or [] if result else []
        return self._medias_by_ids([node.get("id") for node in nodes])

    @cached(
        maxsize=settings.CONF.anilist,
        ttl=settings.CONF.meta,
        skip_empty=True,
        shared_key="person_credits",
    )
    async def async_person_credits(self, person_id: int, page: int = 1, count: int = 20) -> list[dict]:
        """
        异步获取 AniList 演员参与的动画作品。

        :return: AniList 媒体列表
        """
        query = """
            query ($id: Int!, $page: Int!, $count: Int!) {
              Staff(id: $id) {
                characterMedia(page: $page, perPage: $count, sort: [POPULARITY_DESC]) {
                  nodes { id }
                }
              }
            }
        """
        result = await self._async_invoke(query, {"id": person_id, "page": page, "count": count})
        nodes = result.get("Staff", {}).get("characterMedia", {}).get("nodes") or [] if result else []
        return await self._async_medias_by_ids([node.get("id") for node in nodes])

    def clear_cache(self) -> None:
        """清理 AniList 接口缓存"""
        for method in (
            self.detail,
            self.search,
            self.discover,
            self.credits,
            self.recommendations,
            self.person_detail,
            self.person_credits,
        ):
            method.cache_clear()
