"""IMDb 原生媒体数据源 Module。"""

import asyncio
import re
from dataclasses import dataclass
from typing import Any, Optional, Tuple, Union

from app.domain.context import MediaInfo
from app.domain.media import is_media_source_enabled
from app.domain.meta.metabase import MetaBase
from app.domain.scraper import MediaScraperHelper
from app.foundation.text import convert as zhconv_convert
from app.modules import _ModuleBase
from app.runtime.settings import RuntimeSettingsCompat

settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.context import MediaCredit, MediaImageSet
from app.schemas.media import normalize_media_source
from app.schemas.types import (
    MediaRecognizeType,
    MediaSource,
    MediaSourceSelection,
    MediaType,
    ModuleType,
)

from .api import (
    ImdbAka,
    ImdbApi,
    ImdbCredit,
    ImdbEpisode,
    ImdbImage,
    ImdbSeason,
    ImdbTitle,
)


@dataclass(frozen=True, slots=True)
class ImdbConfigSnapshot:
    """IMDb Module 一次配置 generation 使用的网络快照。"""

    proxy: Any


class ImdbModule(_ModuleBase):
    """提供 IMDb 搜索、识别、详情补全与刮削能力。"""

    CONFIG_WATCH = {"PROXY_HOST"}
    _IMDB_ID_PATTERN = re.compile(r"^tt\d+$", re.IGNORECASE)
    _MOVIE_TYPES = frozenset({"movie", "tvMovie"})
    _TV_TYPES = frozenset({"tvSeries", "tvMiniSeries", "tvShort", "tvSpecial"})

    imdb_api: Optional[ImdbApi] = None
    scraper: Optional[MediaScraperHelper] = None
    _config = ImdbConfigSnapshot(proxy=None)

    def init_module(self) -> None:
        """按当前代理配置初始化 IMDb 客户端和通用刮削器。"""
        self._config = ImdbConfigSnapshot(proxy=settings.PROXY)
        self.imdb_api = ImdbApi(proxies=self._config.proxy)
        self.scraper = MediaScraperHelper()

    def init_setting(self) -> Optional[Tuple[str, Union[str, bool]]]:
        """IMDb 随宿主启动，无需独立模块开关。"""
        return None

    @staticmethod
    def get_name() -> str:
        """返回模块展示名称。"""
        return "IMDb"

    @staticmethod
    def get_type() -> ModuleType:
        """返回模块所属的媒体识别类型。"""
        return ModuleType.MediaRecognize

    @staticmethod
    def get_subtype() -> MediaRecognizeType:
        """返回 IMDb 媒体识别子类型。"""
        return MediaRecognizeType.IMDb

    @staticmethod
    def get_priority() -> int:
        """返回模块调度优先级。"""
        return 4

    def stop(self) -> None:
        """释放当前 generation 持有的客户端引用。"""
        if self.imdb_api:
            self.imdb_api.close()
        self.imdb_api = None
        self.scraper = None

    def test(self) -> Tuple[bool, str]:
        """通过最小标题搜索测试 IMDb 数据服务连通性。"""
        if not self.imdb_api:
            return False, "IMDb 模块未初始化"
        results = self.imdb_api.search_titles("The Shawshank Redemption", limit=1)
        return (True, "") if results else (False, "IMDb 数据服务未返回搜索结果")

    @classmethod
    def _media_type(cls, title_type: str) -> MediaType:
        """把 IMDb 标题类型转换为 MoviePilot 媒体类型。"""
        if title_type in cls._MOVIE_TYPES:
            return MediaType.MOVIE
        if title_type in cls._TV_TYPES:
            return MediaType.TV
        return MediaType.UNKNOWN

    @staticmethod
    def _normalize_name(value: Optional[str]) -> str:
        """移除标题标点与空白，生成用于精确比较的稳定文本。"""
        return re.sub(r"[\W_]+", "", value or "", flags=re.UNICODE).casefold()

    @classmethod
    def _name_matches(cls, query: str, names: list[Optional[str]]) -> bool:
        """判断查询标题是否与任一 IMDb 标题或别名精确匹配。"""
        normalized_query = cls._normalize_name(query)
        return bool(
            normalized_query
            and any(
                cls._normalize_name(candidate) == normalized_query
                for candidate in names
                if candidate
            )
        )

    @classmethod
    def _normalize_imdb_id(cls, media_id: object) -> Optional[str]:
        """校验并规范化 IMDb 的 ``tt`` 数字 ID。"""
        value = str(media_id or "").strip().lower()
        return value if cls._IMDB_ID_PATTERN.fullmatch(value) else None

    @classmethod
    def _supports_request_type(
        cls, meta: Optional[MetaBase], mtype: Optional[MediaType]
    ) -> bool:
        """限制 IMDb Module 只处理电影和电视剧请求。"""
        requested_type = mtype or getattr(meta, "type", None)
        return requested_type not in {MediaType.MUSIC, MediaType.MUSIC.value, "music"}

    @staticmethod
    def _recognize_names(meta: MetaBase) -> list[str]:
        """按中文、简体中文、英文和解析主标题顺序生成识别词。"""
        simplified_name = (
            zhconv_convert(meta.cn_name, "zh-hans") if meta.cn_name else None
        )
        names = [meta.cn_name, simplified_name, meta.en_name, meta.name]
        return list(dict.fromkeys(name for name in names if isinstance(name, str) and name))

    @classmethod
    def _candidate_titles(
        cls,
        titles: list[ImdbTitle],
        mtype: Optional[MediaType],
        year: Optional[str],
    ) -> list[ImdbTitle]:
        """按支持类型、请求类型和年份给 IMDb 候选项排序。"""
        candidates = [
            title
            for title in titles
            if cls._media_type(title.type) in {MediaType.MOVIE, MediaType.TV}
        ]
        if mtype in {MediaType.MOVIE, MediaType.TV}:
            candidates = [
                title for title in candidates if cls._media_type(title.type) == mtype
            ]
        if year and str(year).isdigit():
            requested_year = int(year)
            candidates.sort(
                key=lambda title: (
                    abs((title.start_year or requested_year + 99) - requested_year),
                    -(title.start_year or 0),
                )
            )
        else:
            candidates.sort(key=lambda title: title.start_year or 0, reverse=True)
        return candidates

    def _pick_title(
        self,
        query: str,
        titles: list[ImdbTitle],
        mtype: Optional[MediaType],
        year: Optional[str],
    ) -> Optional[ImdbTitle]:
        """同步选取标题、原名或别名精确命中的 IMDb 候选项。"""
        candidates = self._candidate_titles(titles, mtype, year)
        for title in candidates:
            if self._name_matches(query, [title.primary_title, title.original_title]):
                return title
        if not self.imdb_api:
            return None
        for title in candidates[:10]:
            akas = self.imdb_api.list_akas(title.id)
            if self._name_matches(query, [aka.text for aka in akas]):
                return title
        return None

    async def _async_pick_title(
        self,
        query: str,
        titles: list[ImdbTitle],
        mtype: Optional[MediaType],
        year: Optional[str],
    ) -> Optional[ImdbTitle]:
        """异步选取标题、原名或别名精确命中的 IMDb 候选项。"""
        candidates = self._candidate_titles(titles, mtype, year)
        for title in candidates:
            if self._name_matches(query, [title.primary_title, title.original_title]):
                return title
        if not self.imdb_api:
            return None
        for title in candidates[:10]:
            akas = await self.imdb_api.async_list_akas(title.id)
            if self._name_matches(query, [aka.text for aka in akas]):
                return title
        return None

    def _match_by_meta(
        self, meta: MetaBase, mtype: Optional[MediaType]
    ) -> Optional[ImdbTitle]:
        """同步按解析元数据搜索并匹配一个 IMDb 条目。"""
        if not self.imdb_api:
            return None
        requested_type = mtype or meta.type
        for name in self._recognize_names(meta):
            logger.info("正在使用 IMDb 识别：%s ...", name)
            titles = self.imdb_api.search_titles(name)
            title = self._pick_title(name, titles, requested_type, meta.year)
            if title:
                return title
        return None

    async def _async_match_by_meta(
        self, meta: MetaBase, mtype: Optional[MediaType]
    ) -> Optional[ImdbTitle]:
        """异步按解析元数据搜索并匹配一个 IMDb 条目。"""
        if not self.imdb_api:
            return None
        requested_type = mtype or meta.type
        for name in self._recognize_names(meta):
            logger.info("正在使用 IMDb 识别：%s ...", name)
            titles = await self.imdb_api.async_search_titles(name)
            title = await self._async_pick_title(
                name, titles, requested_type, meta.year
            )
            if title:
                return title
        return None

    @staticmethod
    def _person_credit(
        credit: ImdbCredit, *, director: bool = False
    ) -> Optional[dict]:
        """把 IMDb 演职员条目转换为统一演职员摘要。"""
        if not credit.name or not credit.name.display_name:
            return None
        image_url = credit.name.primary_image.url if credit.name.primary_image else None
        media_credit = MediaCredit(
            id=credit.name.id,
            name=credit.name.display_name,
            character=credit.characters[0] if credit.characters else None,
            job="Director" if director else None,
            profile_path=image_url,
            url=(
                f"https://www.imdb.com/name/{credit.name.id}/"
                if credit.name.id
                else None
            ),
            avatar=image_url,
            images=MediaImageSet(large=image_url) if image_url else None,
        )
        return media_credit.model_dump(exclude_none=True)

    @staticmethod
    def _backdrop_url(images: list[ImdbImage]) -> Optional[str]:
        """按剧照、幕后照顺序选择一张 IMDb 背景图。"""
        for image_type in ("still_frame", "behind_the_scenes"):
            if image := next(
                (item for item in images if item.type == image_type and item.url),
                None,
            ):
                return image.url
        return next((item.url for item in images if item.url), None)

    @classmethod
    def _category(cls, title: ImdbTitle, mtype: MediaType) -> str:
        """按 IMDb 类型、风格、国家和语言生成 MoviePilot 二级分类。"""
        genres = {genre.casefold() for genre in title.genres}
        countries = {item.code for item in title.origin_countries if item.code}
        languages = {item.code for item in title.spoken_languages if item.code}
        if mtype == MediaType.MOVIE:
            if "animation" in genres:
                return "动画电影"
            if languages.intersection({"zho", "cmn", "yue", "nan"}):
                return "华语电影"
            return "外语电影"
        if "animation" in genres and countries.intersection({"CN", "TW", "HK"}):
            return "国漫"
        if "animation" in genres and "JP" in countries:
            return "日番"
        if "documentary" in genres:
            return "纪录片"
        if genres.intersection({"reality-tv", "game-show"}):
            return "综艺"
        if countries.intersection({"CN", "TW", "HK"}):
            return "国产剧"
        if countries.intersection({"JP", "KP", "KR", "TH", "IN", "SG"}):
            return "日韩剧"
        if countries.intersection({"US", "FR", "GB", "DE", "ES", "IT", "NL", "PT", "RU", "UK"}):
            return "欧美剧"
        return "未分类"

    @classmethod
    def _to_media_info(
        cls,
        title: ImdbTitle,
        akas: Optional[list[ImdbAka]] = None,
        credits: Optional[list[ImdbCredit]] = None,
        episodes: Optional[list[ImdbEpisode]] = None,
        seasons: Optional[list[ImdbSeason]] = None,
        images: Optional[list[ImdbImage]] = None,
    ) -> MediaInfo:
        """把 IMDb 详情及关联资源转换为统一媒体信息。"""
        akas = akas or []
        credits = credits or []
        episodes = episodes or []
        seasons = seasons or []
        images = images or []
        media_type = cls._media_type(title.type)
        names = list(
            dict.fromkeys(
                name
                for name in (
                    title.primary_title,
                    title.original_title,
                    *(aka.text for aka in akas),
                )
                if name
            )
        )
        media_info = MediaInfo()
        media_info.media_source = MediaSource.IMDb
        media_info.media_id = title.id
        media_info.imdb_id = title.id
        media_info.type = media_type
        media_info.title = title.primary_title or title.original_title or ""
        media_info.en_title = title.primary_title
        media_info.original_title = title.original_title
        media_info.original_name = title.original_title
        media_info.names = names
        media_info.year = str(title.start_year) if title.start_year else ""
        media_info.overview = title.plot or ""
        media_info.adult = bool(title.is_adult)
        media_info.poster_path = title.primary_image.url if title.primary_image else None
        media_info.backdrop_path = cls._backdrop_url(images)
        media_info.genres = [
            {"id": genre, "name": genre} for genre in title.genres
        ]
        media_info.origin_country = [
            item.code for item in title.origin_countries if item.code
        ]
        media_info.production_countries = [
            {"name": item.name or item.code}
            for item in title.origin_countries
            if item.name or item.code
        ]
        media_info.spoken_languages = [
            {"iso_639_1": item.code, "name": item.name or item.code}
            for item in title.spoken_languages
            if item.code
        ]
        if title.spoken_languages:
            media_info.original_language = title.spoken_languages[0].code
        if title.rating:
            media_info.vote_average = title.rating.aggregate_rating
            media_info.vote_count = title.rating.vote_count
        if title.runtime_seconds:
            media_info.runtime = max(1, round(title.runtime_seconds / 60))
            media_info.episode_run_time = [media_info.runtime]

        directors: list[dict] = []
        actors: list[dict] = []
        for credit in credits:
            category = (credit.category or "").upper()
            if category == "DIRECTOR":
                if item := cls._person_credit(credit, director=True):
                    directors.append(item)
            elif category in {"CAST", "ACTOR", "ACTRESS"}:
                if item := cls._person_credit(credit):
                    actors.append(item)
        if not directors:
            directors = [
                item
                for person in title.directors
                if (item := cls._person_credit(ImdbCredit(name=person), director=True))
            ]
        if not actors:
            actors = [
                item
                for person in title.stars
                if (item := cls._person_credit(ImdbCredit(name=person)))
            ]
        media_info.directors = directors[:3]
        media_info.actors = actors[:12]

        season_info: dict[int, dict] = {}
        for season in seasons:
            if not season.season or not season.season.isdigit():
                continue
            season_number = int(season.season)
            season_info[season_number] = {
                "season_number": season_number,
                "episode_count": season.episode_count,
                "name": season.season,
            }
        for episode in episodes:
            if not episode.season or not episode.season.isdigit():
                continue
            season_number = int(episode.season)
            media_info.seasons.setdefault(season_number, []).append(
                episode.episode_number or 0
            )
            if (
                season_number not in media_info.season_years
                and episode.release_date
                and episode.release_date.year
            ):
                media_info.season_years[season_number] = str(episode.release_date.year)
            season_info.setdefault(
                season_number,
                {
                    "season_number": season_number,
                    "episode_count": None,
                    "name": str(season_number),
                },
            )
        media_info.season_info = list(season_info.values())
        if media_type == MediaType.TV:
            media_info.number_of_seasons = len(season_info)
            media_info.number_of_episodes = len(episodes)
        media_info.set_category(cls._category(title, media_type))
        return media_info

    def _load_media_info(self, title: ImdbTitle) -> MediaInfo:
        """同步补齐一个 IMDb 条目的别名、演职员、剧集和图片。"""
        if not self.imdb_api:
            return self._to_media_info(title)
        details = self.imdb_api.get_title(title.id) or title
        akas = self.imdb_api.list_akas(title.id)
        credits = self.imdb_api.list_credits(title.id)
        images = self.imdb_api.list_images(title.id)
        if self._media_type(details.type) == MediaType.TV:
            episodes = self.imdb_api.list_episodes(title.id)
            seasons = self.imdb_api.list_seasons(title.id)
        else:
            episodes = []
            seasons = []
        return self._to_media_info(
            details,
            akas=akas,
            credits=credits,
            episodes=episodes,
            seasons=seasons,
            images=images,
        )

    async def _async_load_media_info(self, title: ImdbTitle) -> MediaInfo:
        """并发补齐一个 IMDb 条目的别名、演职员、剧集和图片。"""
        if not self.imdb_api:
            return self._to_media_info(title)
        is_tv = self._media_type(title.type) == MediaType.TV
        results = await asyncio.gather(
            self.imdb_api.async_get_title(title.id),
            self.imdb_api.async_list_akas(title.id),
            self.imdb_api.async_list_credits(title.id),
            self.imdb_api.async_list_images(title.id),
            self.imdb_api.async_list_episodes(title.id) if is_tv else asyncio.sleep(0, result=[]),
            self.imdb_api.async_list_seasons(title.id) if is_tv else asyncio.sleep(0, result=[]),
            return_exceptions=True,
        )
        details = results[0] if isinstance(results[0], ImdbTitle) else title
        return self._to_media_info(
            details,
            akas=results[1] if isinstance(results[1], list) else [],
            credits=results[2] if isinstance(results[2], list) else [],
            images=results[3] if isinstance(results[3], list) else [],
            episodes=results[4] if isinstance(results[4], list) else [],
            seasons=results[5] if isinstance(results[5], list) else [],
        )

    def recognize_media(
        self,
        meta: Optional[MetaBase] = None,
        mtype: Optional[MediaType] = None,
        media_source: Optional[MediaSource] = None,
        media_id: Optional[str] = None,
        episode_group: Optional[str] = None,
        cache: bool = True,
        **kwargs,
    ) -> Optional[MediaInfo]:
        """按 IMDb 显式身份或标题元数据同步识别影视信息。"""
        del episode_group, cache, kwargs
        if not self._supports_request_type(meta, mtype) or not self.imdb_api:
            return None
        requested_source = normalize_media_source(media_source)
        if media_id is not None:
            imdb_id = self._normalize_imdb_id(media_id)
            if requested_source != MediaSource.IMDb or not imdb_id:
                return None
            title = self.imdb_api.get_title(imdb_id)
        else:
            if requested_source not in {None, MediaSource.IMDb}:
                return None
            selected_source = requested_source or normalize_media_source(
                settings.RECOGNIZE_SOURCE
            )
            if selected_source != MediaSource.IMDb or not meta or not meta.name:
                return None
            title = self._match_by_meta(meta, mtype)
        if not title:
            return None
        media_info = self._load_media_info(title)
        if meta and meta.begin_season is not None:
            media_info.season = meta.begin_season
        logger.info(
            "IMDb 识别结果：%s %s %s:%s",
            media_info.type.value,
            media_info.title_year,
            media_info.media_source,
            media_info.media_id,
        )
        return media_info

    async def async_recognize_media(
        self,
        meta: Optional[MetaBase] = None,
        mtype: Optional[MediaType] = None,
        media_source: Optional[MediaSource] = None,
        media_id: Optional[str] = None,
        episode_group: Optional[str] = None,
        cache: bool = True,
        **kwargs,
    ) -> Optional[MediaInfo]:
        """按 IMDb 显式身份或标题元数据异步识别影视信息。"""
        del episode_group, cache, kwargs
        if not self._supports_request_type(meta, mtype) or not self.imdb_api:
            return None
        requested_source = normalize_media_source(media_source)
        if media_id is not None:
            imdb_id = self._normalize_imdb_id(media_id)
            if requested_source != MediaSource.IMDb or not imdb_id:
                return None
            title = await self.imdb_api.async_get_title(imdb_id)
        else:
            if requested_source not in {None, MediaSource.IMDb}:
                return None
            selected_source = requested_source or normalize_media_source(
                settings.RECOGNIZE_SOURCE
            )
            if selected_source != MediaSource.IMDb or not meta or not meta.name:
                return None
            title = await self._async_match_by_meta(meta, mtype)
        if not title:
            return None
        media_info = await self._async_load_media_info(title)
        if meta and meta.begin_season is not None:
            media_info.season = meta.begin_season
        logger.info(
            "IMDb 异步识别结果：%s %s %s:%s",
            media_info.type.value,
            media_info.title_year,
            media_info.media_source,
            media_info.media_id,
        )
        return media_info

    def search_medias(
        self,
        meta: MetaBase,
        media_source: Optional[MediaSourceSelection] = None,
    ) -> Optional[list[MediaInfo]]:
        """按请求级来源选择同步搜索 IMDb 影视条目。"""
        if not is_media_source_enabled(media_source, MediaSource.IMDb):
            return None
        if not self.imdb_api or not meta.name:
            return []
        return [
            self._to_media_info(title)
            for title in self._candidate_titles(
                self.imdb_api.search_titles(meta.name), meta.type, meta.year
            )
        ]

    async def async_search_medias(
        self,
        meta: MetaBase,
        media_source: Optional[MediaSourceSelection] = None,
    ) -> Optional[list[MediaInfo]]:
        """按请求级来源选择异步搜索 IMDb 影视条目。"""
        if not is_media_source_enabled(media_source, MediaSource.IMDb):
            return None
        if not self.imdb_api or not meta.name:
            return []
        titles = await self.imdb_api.async_search_titles(meta.name)
        return [
            self._to_media_info(title)
            for title in self._candidate_titles(titles, meta.type, meta.year)
        ]

    def clear_cache(self) -> None:
        """清理 IMDb 请求缓存并记录统一模块日志。"""
        if not self.imdb_api:
            return
        logger.info("开始清除 IMDb 缓存 ...")
        self.imdb_api.clear_cache()
        logger.info("IMDb 缓存清除完成")

    def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
        """为 IMDb 媒体信息同步补充缺失的背景图片。"""
        if mediainfo.media_source != MediaSource.IMDb or not self.imdb_api:
            return None
        if mediainfo.backdrop_path:
            return mediainfo
        imdb_id = self._normalize_imdb_id(mediainfo.media_id)
        if not imdb_id:
            return None
        mediainfo.backdrop_path = self._backdrop_url(
            self.imdb_api.list_images(imdb_id)
        )
        return mediainfo

    async def async_obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
        """为 IMDb 媒体信息异步补充缺失的背景图片。"""
        if mediainfo.media_source != MediaSource.IMDb or not self.imdb_api:
            return None
        if mediainfo.backdrop_path:
            return mediainfo
        imdb_id = self._normalize_imdb_id(mediainfo.media_id)
        if not imdb_id:
            return None
        mediainfo.backdrop_path = self._backdrop_url(
            await self.imdb_api.async_list_images(imdb_id)
        )
        return mediainfo

    def metadata_nfo(
        self,
        mediainfo: MediaInfo,
        season: Optional[int] = None,
        episode: Optional[int] = None,
        **kwargs,
    ) -> Optional[str]:
        """生成 IMDb 来源的 NFO 元数据文本。"""
        del kwargs
        if (mediainfo.scrape_source or settings.SCRAP_SOURCE) != MediaSource.IMDb.value:
            return None
        if not self.scraper:
            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]:
        """生成 IMDb 来源的图片文件名与下载地址映射。"""
        if (mediainfo.scrape_source or settings.SCRAP_SOURCE) != MediaSource.IMDb.value:
            return None
        if not self.scraper:
            return None
        return self.scraper.get_metadata_img(
            mediainfo, season=season, episode=episode
        )
