from typing import List, Optional, Tuple, Union

from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.runtime.settings import RuntimeSettingsCompat

settings = RuntimeSettingsCompat()
from app.domain.context import MediaInfo
from app.domain.meta.metabase import MetaBase
from app.domain.scraper import MediaScraperHelper
from app.runtime.log import logger
from app.modules import _ModuleBase
from app.modules.anilist.anilist import AniListApi
from app.schemas.types import (
    MediaRecognizeType,
    MediaSource,
    MediaSourceSelection,
    MediaType,
    ModuleType,
)
from app.domain.media import is_media_source_enabled


class AniListModule(_ModuleBase):
    """
    AniList 动画媒体识别与刮削模块
    """

    CONFIG_WATCH = {"PROXY_HOST"}

    anilist_api: AniListApi = None
    scraper: MediaScraperHelper = None

    def init_module(self) -> None:
        """初始化 AniList 客户端与通用刮削器"""
        self.anilist_api = AniListApi()
        self.scraper = MediaScraperHelper()

    def init_setting(self) -> Tuple[str, Union[str, bool]]:
        """AniList 模块无需独立开关"""
        return None

    def stop(self) -> None:
        """关闭 AniList 模块"""
        return None

    def test(self) -> Tuple[bool, str]:
        """测试 AniList GraphQL API 连通性"""
        result = self.anilist_api.search("Cowboy Bebop", count=1)
        return (True, "") if result else (False, "AniList网络连接失败")

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

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

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

    @staticmethod
    def get_priority() -> int:
        """获取模块优先级"""
        return 4

    @staticmethod
    def _source_enabled(media_source: Optional[MediaSource]) -> bool:
        """
        判断本次识别是否指定 AniList。

        :param media_source: 请求级识别数据源
        :return: 是否启用 AniList 识别
        """
        return (media_source or settings.RECOGNIZE_SOURCE) == MediaSource.AniList

    @staticmethod
    def _media_type(info: dict) -> MediaType:
        """
        将 AniList 发布格式转换为系统媒体类型。

        :param info: AniList 媒体信息
        :return: 系统媒体类型
        """
        return MediaType.MOVIE if info.get("format") == "MOVIE" else MediaType.TV

    @classmethod
    def _matches_meta(cls, meta: MetaBase, info: dict) -> bool:
        """
        判断 AniList 候选项是否符合标题解析出的类型与年份。

        :param meta: 标题解析元数据
        :param info: AniList 候选项
        :return: 是否符合筛选条件
        """
        if meta.type in {MediaType.MOVIE, MediaType.TV} and cls._media_type(info) != meta.type:
            return False
        year = info.get("startDate", {}).get("year") or info.get("seasonYear")
        return not meta.year or not year or str(year) == str(meta.year)

    @staticmethod
    def _enrich_people(info: dict) -> dict:
        """
        将 AniList 人物连接转换为统一媒体信息所需的演职员结构。

        :param info: AniList 媒体详情
        :return: 补充演员和导演后的媒体详情
        """
        enriched = dict(info)
        actors = []
        for edge in info.get("characters", {}).get("edges") or []:
            character = edge.get("node") or {}
            voice_actors = edge.get("voiceActors") or []
            actor = voice_actors[0] if voice_actors else {}
            actor_name = actor.get("name", {}).get("full")
            if not actor_name:
                continue
            actors.append(
                {
                    "id": actor.get("id"),
                    "name": actor_name,
                    "character": character.get("name", {}).get("full")
                    or character.get("name", {}).get("native"),
                    "avatar": {"large": actor.get("image", {}).get("large")},
                    "url": actor.get("siteUrl"),
                }
            )
        enriched["actors"] = actors

        directors = []
        for edge in info.get("staff", {}).get("edges") or []:
            role = edge.get("role") or ""
            if "Director" not in role:
                continue
            staff = edge.get("node") or {}
            directors.append(
                {
                    "id": staff.get("id"),
                    "name": staff.get("name", {}).get("full"),
                    "job": role,
                    "avatar": {"large": staff.get("image", {}).get("large")},
                    "url": staff.get("siteUrl"),
                }
            )
        enriched["directors"] = directors
        return enriched

    @staticmethod
    def _person_name(name_info: dict) -> Optional[str]:
        """
        按原语言、通用名顺序选择 AniList 人物姓名。

        :param name_info: AniList 人物姓名字段
        :return: 可展示姓名
        """
        return name_info.get("native") or name_info.get("full")

    @staticmethod
    def _person_date(date_info: dict) -> Optional[str]:
        """
        将 AniList 人物模糊日期转换为标准日期文本。

        :param date_info: AniList FuzzyDate 字段
        :return: 日期文本
        """
        return MediaInfo._anilist_date(date_info)

    @classmethod
    def _build_credit_person(cls, edge: dict) -> Optional[_SchemaMediaPerson]:
        """
        将 AniList 角色配音关系转换为统一人物信息。

        :param edge: AniList 角色关系边
        :return: 媒体人物信息
        """
        actor = next(iter(edge.get("voiceActors") or []), None)
        if not actor:
            return None
        name_info = actor.get("name") or {}
        character_name = (edge.get("node") or {}).get("name") or {}
        images = actor.get("image") or {}
        return _SchemaMediaPerson(
            source="anilist",
            id=actor.get("id"),
            name=cls._person_name(name_info),
            original_name=name_info.get("full"),
            also_known_as=name_info.get("alternative") or [],
            character=character_name.get("native") or character_name.get("full"),
            images=images,
            avatar=images,
            url=actor.get("siteUrl"),
        )

    @classmethod
    def _build_person_detail(cls, info: dict) -> _SchemaMediaPerson:
        """
        将 AniList 人物详情转换为统一人物信息。

        :param info: AniList 人物详情
        :return: 媒体人物信息
        """
        name_info = info.get("name") or {}
        images = info.get("image") or {}
        return _SchemaMediaPerson(
            source="anilist",
            id=info.get("id"),
            name=cls._person_name(name_info),
            original_name=name_info.get("full"),
            also_known_as=name_info.get("alternative") or [],
            images=images,
            avatar=images,
            biography=info.get("description"),
            birthday=cls._person_date(info.get("dateOfBirth") or {}),
            deathday=cls._person_date(info.get("dateOfDeath") or {}),
            gender=info.get("gender"),
            place_of_birth=info.get("homeTown"),
            career=info.get("primaryOccupations") or [],
            url=info.get("siteUrl"),
        )

    def recognize_media(
        self,
        meta: MetaBase = None,
        media_source: Optional[MediaSource] = None,
        media_id: Optional[str] = None,
        **kwargs,
    ) -> Optional[MediaInfo]:
        """
        按 AniList ID 或标题识别动画媒体信息。

        :param meta: 标题解析元数据
        :param media_source: 请求级识别数据源
        :param media_id: 数据源原生ID
        :return: 统一媒体信息
        """
        # AniList 只处理动画影视，不能在音乐模块未响应时接管音乐请求。
        if (
                kwargs.get("mtype") == MediaType.MUSIC
                or getattr(meta, "type", None) == MediaType.MUSIC
        ):
            return None
        if media_source and media_source != MediaSource.AniList:
            return None
        if media_id is not None and (
                media_source != MediaSource.AniList or not str(media_id).isdigit()
        ):
            return None
        anilistid = int(media_id) if media_id is not None else None
        if not anilistid and (not meta or not self._source_enabled(media_source)):
            return None
        info = self.anilist_api.detail(anilistid) if anilistid else self._match_by_meta(meta)
        if not info:
            return None
        mediainfo = MediaInfo(anilist_info=self._enrich_people(info))
        if meta and meta.begin_season is not None:
            mediainfo.season = meta.begin_season
        logger.info(
            f"{anilistid or meta.name} AniList识别结果：{mediainfo.type.value} "
            f"{mediainfo.title_year}"
        )
        return mediainfo

    async def async_recognize_media(
        self,
        meta: MetaBase = None,
        media_source: Optional[MediaSource] = None,
        media_id: Optional[str] = None,
        **kwargs,
    ) -> Optional[MediaInfo]:
        """
        异步按 AniList ID 或标题识别动画媒体信息。

        :param meta: 标题解析元数据
        :param media_source: 请求级识别数据源
        :param media_id: 数据源原生ID
        :return: 统一媒体信息
        """
        # 与同步入口保持同一类型边界，音乐请求不得进入 AniList。
        if (
                kwargs.get("mtype") == MediaType.MUSIC
                or getattr(meta, "type", None) == MediaType.MUSIC
        ):
            return None
        if media_source and media_source != MediaSource.AniList:
            return None
        if media_id is not None and (
                media_source != MediaSource.AniList or not str(media_id).isdigit()
        ):
            return None
        anilistid = int(media_id) if media_id is not None else None
        if not anilistid and (not meta or not self._source_enabled(media_source)):
            return None
        info = (
            await self.anilist_api.async_detail(anilistid)
            if anilistid
            else await self._async_match_by_meta(meta)
        )
        if not info:
            return None
        mediainfo = MediaInfo(anilist_info=self._enrich_people(info))
        if meta and meta.begin_season is not None:
            mediainfo.season = meta.begin_season
        logger.info(
            f"{anilistid or meta.name} AniList识别结果：{mediainfo.type.value} "
            f"{mediainfo.title_year}"
        )
        return mediainfo

    def _match_by_meta(self, meta: MetaBase) -> Optional[dict]:
        """
        同步搜索并筛选最符合标题解析结果的 AniList 条目。

        :param meta: 标题解析元数据
        :return: AniList 媒体详情
        """
        for info in self.anilist_api.search(meta.name):
            if self._matches_meta(meta, info):
                return info
        return None

    async def _async_match_by_meta(self, meta: MetaBase) -> Optional[dict]:
        """
        异步搜索并筛选最符合标题解析结果的 AniList 条目。

        :param meta: 标题解析元数据
        :return: AniList 媒体详情
        """
        for info in await self.anilist_api.async_search(meta.name):
            if self._matches_meta(meta, info):
                return info
        return None

    def search_medias(
        self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
    ) -> Optional[List[MediaInfo]]:
        """
        搜索 AniList 动画媒体信息。

        :param meta: 标题解析元数据
        :param media_source: 请求级搜索数据源
        :return: 统一媒体信息列表
        """
        if not is_media_source_enabled(media_source, MediaSource.AniList):
            return None
        if not meta or not meta.name:
            return []
        return [
            MediaInfo(anilist_info=self._enrich_people(info))
            for info in self.anilist_api.search(meta.name)
            if self._matches_meta(meta, info)
        ]

    async def async_search_medias(
        self, meta: MetaBase, media_source: Optional[MediaSourceSelection] = None
    ) -> Optional[List[MediaInfo]]:
        """
        异步搜索 AniList 动画媒体信息。

        :param meta: 标题解析元数据
        :param media_source: 请求级搜索数据源
        :return: 统一媒体信息列表
        """
        if not is_media_source_enabled(media_source, MediaSource.AniList):
            return None
        if not meta or not meta.name:
            return []
        return [
            MediaInfo(anilist_info=self._enrich_people(info))
            for info in await self.anilist_api.async_search(meta.name)
            if self._matches_meta(meta, info)
        ]

    def anilist_info(self, anilist_id: int) -> Optional[dict]:
        """
        获取 AniList 动画详情。

        :param anilist_id: AniList 媒体 ID
        :return: AniList 媒体详情
        """
        return self.anilist_api.detail(anilist_id) if anilist_id else None

    async def async_anilist_info(self, anilist_id: int) -> Optional[dict]:
        """
        异步获取 AniList 动画详情。

        :param anilist_id: AniList 媒体 ID
        :return: AniList 媒体详情
        """
        return await self.anilist_api.async_detail(anilist_id) if anilist_id else None

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

        :return: 统一媒体信息列表
        """
        return [
            MediaInfo(anilist_info=info)
            for info in self.anilist_api.trending(page=page, count=count)
        ]

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

        :return: 统一媒体信息列表
        """
        return [
            MediaInfo(anilist_info=info)
            for info in await self.anilist_api.async_trending(page=page, count=count)
        ]

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

        :return: 统一媒体信息列表
        """
        return [
            MediaInfo(anilist_info=info)
            for info in self.anilist_api.popular_this_season(page=page, count=count)
        ]

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

        :return: 统一媒体信息列表
        """
        infos = await self.anilist_api.async_popular_this_season(page=page, count=count)
        return [MediaInfo(anilist_info=info) for info in infos]

    def anilist_discover(self, **kwargs) -> List[MediaInfo]:
        """
        按组合条件探索 AniList 动画。

        :return: 统一媒体信息列表
        """
        return [
            MediaInfo(anilist_info=info)
            for info in self.anilist_api.discover(**kwargs)
        ]

    async def async_anilist_discover(self, **kwargs) -> List[MediaInfo]:
        """
        异步按组合条件探索 AniList 动画。

        :return: 统一媒体信息列表
        """
        return [
            MediaInfo(anilist_info=info)
            for info in await self.anilist_api.async_discover(**kwargs)
        ]

    def anilist_credits(
        self, anilist_id: int, page: int = 1, count: int = 20
    ) -> List[_SchemaMediaPerson]:
        """
        获取 AniList 动画配音演员。

        :return: 媒体人物列表
        """
        persons = (
            self._build_credit_person(edge)
            for edge in self.anilist_api.credits(anilist_id, page=page, count=count)
        )
        return [person for person in persons if person]

    async def async_anilist_credits(
        self, anilist_id: int, page: int = 1, count: int = 20
    ) -> List[_SchemaMediaPerson]:
        """
        异步获取 AniList 动画配音演员。

        :return: 媒体人物列表
        """
        edges = await self.anilist_api.async_credits(anilist_id, page=page, count=count)
        persons = (self._build_credit_person(edge) for edge in edges)
        return [person for person in persons if person]

    def anilist_recommendations(
        self, anilist_id: int, page: int = 1, count: int = 20
    ) -> List[MediaInfo]:
        """
        获取 AniList 动画相关推荐。

        :return: 统一媒体信息列表
        """
        infos = self.anilist_api.recommendations(anilist_id, page=page, count=count)
        return [MediaInfo(anilist_info=info) for info in infos]

    async def async_anilist_recommendations(
        self, anilist_id: int, page: int = 1, count: int = 20
    ) -> List[MediaInfo]:
        """
        异步获取 AniList 动画相关推荐。

        :return: 统一媒体信息列表
        """
        infos = await self.anilist_api.async_recommendations(
            anilist_id, page=page, count=count
        )
        return [MediaInfo(anilist_info=info) for info in infos]

    def anilist_person_detail(self, person_id: int) -> Optional[_SchemaMediaPerson]:
        """
        获取 AniList 人物详情。

        :param person_id: AniList 人物 ID
        :return: 媒体人物信息
        """
        info = self.anilist_api.person_detail(person_id)
        return self._build_person_detail(info) if info else None

    async def async_anilist_person_detail(
        self, person_id: int
    ) -> Optional[_SchemaMediaPerson]:
        """
        异步获取 AniList 人物详情。

        :param person_id: AniList 人物 ID
        :return: 媒体人物信息
        """
        info = await self.anilist_api.async_person_detail(person_id)
        return self._build_person_detail(info) if info else None

    def anilist_person_credits(
        self, person_id: int, page: int = 1, count: int = 20
    ) -> List[MediaInfo]:
        """
        获取 AniList 人物参与的动画作品。

        :return: 统一媒体信息列表
        """
        infos = self.anilist_api.person_credits(person_id, page=page, count=count)
        return [MediaInfo(anilist_info=info) for info in infos]

    async def async_anilist_person_credits(
        self, person_id: int, page: int = 1, count: int = 20
    ) -> List[MediaInfo]:
        """
        异步获取 AniList 人物参与的动画作品。

        :return: 统一媒体信息列表
        """
        infos = await self.anilist_api.async_person_credits(
            person_id, page=page, count=count
        )
        return [MediaInfo(anilist_info=info) for info in infos]

    def metadata_nfo(
        self,
        mediainfo: MediaInfo,
        season: Optional[int] = None,
        episode: Optional[int] = None,
        **kwargs,
    ) -> Optional[str]:
        """
        生成 AniList 来源的 NFO 内容。

        :param mediainfo: 统一媒体信息
        :param season: 季号
        :param episode: 集号
        :return: NFO XML 文本
        """
        scrape_source = mediainfo.scrape_source or settings.SCRAP_SOURCE
        if scrape_source != "anilist":
            return None
        return self.scraper.get_metadata_nfo(mediainfo, season=season, episode=episode)

    def metadata_img(
        self,
        mediainfo: MediaInfo,
        season: Optional[int] = None,
        episode: Optional[int] = None,
    ) -> Optional[dict]:
        """
        获取 AniList 来源的刮削图片清单。

        :param mediainfo: 统一媒体信息
        :param season: 季号
        :param episode: 集号
        :return: 图片文件名与下载地址映射
        """
        scrape_source = mediainfo.scrape_source or settings.SCRAP_SOURCE
        if scrape_source != "anilist":
            return None
        return self.scraper.get_metadata_img(mediainfo, season=season, episode=episode)

    def clear_cache(self) -> None:
        """清理 AniList 接口缓存"""
        self.anilist_api.clear_cache()
