"""媒体识别管线 mixin。

从 ChainBase 拆出的识别域：原生模块识别路由、识别缓存回填、共享识别、
插件补充识别。方法经 MRO 解析，依赖 ChainBase 实例的 run_module/eventmanager
等协作对象。
"""
import copy
from typing import Optional

from fastapi.concurrency import run_in_threadpool

from app.adapters.external.server import MoviePilotServerHelper
from app.application.configuration import get_configured_system_config
from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.runtime.cache import fresh, async_fresh
from app.runtime.events import Event
from app.runtime.log import logger
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.schemas.types import ChainEventType, MediaSource, MediaType, SystemConfigKey


class RecognitionMixin:

    def _can_use_media_recognize_share(
            self,
            meta: Optional[MetaBase],
            media_source: Optional[MediaSource],
            media_id: Optional[str],
    ) -> bool:
        """
        仅在名称识别场景下使用共享识别，显式ID识别不再重复回查
        """
        return bool(
            self.runtime_config.media_recognize_share
            and meta
            and not media_source
            and not media_id
        )

    @staticmethod
    def _snapshot_recognize_cache_meta(meta: Optional[MetaBase]) -> Optional[MetaBase]:
        """
        保存共享识别前的本地缓存关键元数据，用于共享成功后回填正缓存覆盖负缓存。
        """
        if not meta:
            return None
        return copy.deepcopy(meta)

    def _update_local_recognize_cache(
            self,
            meta: Optional[MetaBase],
            mediainfo: Optional[MediaInfo],
    ) -> None:
        """
        共享识别成功后回填本地识别缓存，避免名称负缓存导致后续重复回查共享。
        """
        if not meta or not mediainfo:
            return
        self.run_module(
            "update_recognize_cache",
            meta=meta,
            mediainfo=mediainfo,
        )

    async def _async_update_local_recognize_cache(
            self,
            meta: Optional[MetaBase],
            mediainfo: Optional[MediaInfo],
    ) -> None:
        """
        异步回填本地识别缓存。
        """
        if not meta or not mediainfo:
            return
        await self.async_run_module(
            "async_update_recognize_cache",
            meta=meta,
            mediainfo=mediainfo,
        )

    @staticmethod
    def _record_media_recognize_share_hit() -> None:
        """记录一次共享媒体识别成功命中，统计失败不影响识别结果。"""
        try:
            get_configured_system_config().increment(SystemConfigKey.MediaRecognizeShareCount)
        except Exception as err:
            logger.error(f"记录共享媒体识别命中次数失败：{str(err)}")

    def _run_native_media_recognize(
            self,
            module_kwargs: dict,
            cache: bool,
    ) -> Optional[MediaInfo]:
        """执行同步原生媒体模块识别，具体媒体领域可覆写该路由钩子。"""
        with fresh(not cache):
            return self.run_module("recognize_media", **module_kwargs)

    async def _async_run_native_media_recognize(
            self,
            module_kwargs: dict,
            cache: bool,
    ) -> Optional[MediaInfo]:
        """执行异步原生媒体模块识别，具体媒体领域可覆写该路由钩子。"""
        async with async_fresh(not cache):
            return await self.async_run_module(
                "async_recognize_media", **module_kwargs
            )

    def recognize_media(
            self,
            meta: MetaBase = None,
            mtype: Optional[MediaType] = None,
            media_source: Optional[MediaSource] = None,
            media_id: Optional[str] = None,
            episode_group: Optional[str] = None,
            cache: bool = True,
            share_meta: MetaBase = None,
            music_type: Optional[str] = None,
    ) -> Optional[MediaInfo]:
        """
        识别媒体信息，不含Fanart图片
        :param meta:     识别的元数据
        :param share_meta: 共享识别查询/上报使用的原始元数据
        :param mtype:    识别的媒体类型
        :param media_source: 请求级识别数据源
        :param media_id: 数据源原生ID，必须与media_source成对提供
        :param episode_group: 剧集组
        :param cache:    是否使用缓存
        :param music_type: 音乐实体类型，显式音乐 ID 必须据此区分单曲与专辑
        :return: 识别的媒体信息，包括剧集信息
        """
        # 仅传数据源是请求级识别源约束（按名称识别限定数据源），显式 media_id 才要求来源成对
        explicit_identity = media_id is not None
        requested_source = normalize_media_source(media_source) or media_source
        media_source, media_id = resolve_media_identity(
            media=meta,
            media_source=media_source,
            media_id=media_id,
        )
        if explicit_identity and (not media_source or not media_id):
            logger.warning("媒体识别需要同时提供有效的 media_source 和 media_id")
            return None
        if not media_id and requested_source is not None:
            media_source = requested_source
            # meta 自带同源身份（如 {tmdbid=} 标题）时直接按身份识别，避免退化为名称搜索
            meta_source, meta_id = resolve_media_identity(media=meta)
            if meta_id and meta_source == requested_source:
                media_source, media_id = meta_source, meta_id
        if not episode_group and hasattr(meta, "episode_group"):
            episode_group = meta.episode_group
        if not mtype and not (media_source and media_id) and meta and meta.type in [
            MediaType.TV, MediaType.MOVIE, MediaType.MUSIC
        ]:
            mtype = meta.type
        share_query_meta = share_meta or meta
        module_kwargs = {
            "meta": meta,
            "mtype": mtype,
            "media_source": media_source,
            "media_id": media_id,
            "episode_group": episode_group,
            "cache": cache,
        }
        if music_type is not None:
            module_kwargs["music_type"] = music_type
        mediainfo = self._run_native_media_recognize(module_kwargs, cache)
        # 原生识别未取得远端身份时，允许插件按已知要素补充匹配媒体信息（影视与音乐统一）
        mediainfo = self._supplement_media_recognize(
            meta=meta, mtype=mtype, media_source=media_source,
            media_id=media_id, mediainfo=mediainfo,
            music_type=music_type,
        )
        fallback_mediainfo = (
            mediainfo
            if mediainfo and not self._media_info_has_identity(mediainfo)
            else None
        )
        if mediainfo and self._media_info_has_identity(mediainfo):
            # 电影、电视剧、音乐统一上报；音乐的 tmdb 等字段恒为 None，身份取数据源原生 ID
            if not getattr(mediainfo, "recognize_cache_hit", False):
                MoviePilotServerHelper.report_recognize_share(
                    meta=meta,
                    mediainfo=mediainfo,
                    keyword_meta=share_query_meta,
                )
            return mediainfo

        if self._can_use_media_recognize_share(
                share_query_meta, media_source, media_id
        ):
            shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
            share_query_kwargs = {
                "meta": meta,
                "mtype": mtype,
                "keyword_meta": share_query_meta,
            }
            if music_type is not None:
                share_query_kwargs["music_type"] = music_type
            shared_item = MoviePilotServerHelper.query_recognize_share(
                **share_query_kwargs,
            )
            shared_params = MoviePilotServerHelper.to_recognize_params(shared_item)
            if shared_params:
                shared_module_kwargs = {
                        "meta": meta,
                        "mtype": shared_params.get("mtype") or mtype,
                        "media_source": shared_params.get("media_source"),
                        "media_id": shared_params.get("media_id"),
                        "episode_group": episode_group,
                        "cache": cache,
                }
                shared_music_type = shared_params.get("music_type") or music_type
                if shared_music_type is not None:
                    shared_module_kwargs["music_type"] = shared_music_type
                mediainfo = self._run_native_media_recognize(
                    shared_module_kwargs,
                    cache,
                )
                if mediainfo and self._media_info_has_identity(mediainfo):
                    self._update_local_recognize_cache(shared_cache_meta, mediainfo)
                    self._record_media_recognize_share_hit()
                    return mediainfo
                if mediainfo and not fallback_mediainfo:
                    fallback_mediainfo = mediainfo
        return fallback_mediainfo

    async def async_recognize_media(
            self,
            meta: MetaBase = None,
            mtype: Optional[MediaType] = None,
            media_source: Optional[MediaSource] = None,
            media_id: Optional[str] = None,
            episode_group: Optional[str] = None,
            cache: bool = True,
            share_meta: MetaBase = None,
            music_type: Optional[str] = None,
    ) -> Optional[MediaInfo]:
        """
        识别媒体信息，不含Fanart图片（异步版本）
        :param meta:     识别的元数据
        :param share_meta: 共享识别查询/上报使用的原始元数据
        :param mtype:    识别的媒体类型
        :param media_source: 请求级识别数据源
        :param media_id: 数据源原生ID，必须与media_source成对提供
        :param episode_group: 剧集组
        :param cache:    是否使用缓存
        :param music_type: 音乐实体类型，显式音乐 ID 必须据此区分单曲与专辑
        :return: 识别的媒体信息，包括剧集信息
        """
        # 仅传数据源是请求级识别源约束（按名称识别限定数据源），显式 media_id 才要求来源成对
        explicit_identity = media_id is not None
        requested_source = normalize_media_source(media_source) or media_source
        media_source, media_id = resolve_media_identity(
            media=meta,
            media_source=media_source,
            media_id=media_id,
        )
        if explicit_identity and (not media_source or not media_id):
            logger.warning("媒体识别需要同时提供有效的 media_source 和 media_id")
            return None
        if not media_id and requested_source is not None:
            media_source = requested_source
            # meta 自带同源身份（如 {tmdbid=} 标题）时直接按身份识别，避免退化为名称搜索
            meta_source, meta_id = resolve_media_identity(media=meta)
            if meta_id and meta_source == requested_source:
                media_source, media_id = meta_source, meta_id
        if not episode_group and hasattr(meta, "episode_group"):
            episode_group = meta.episode_group
        if not mtype and not (media_source and media_id) and meta and meta.type in [
            MediaType.TV, MediaType.MOVIE, MediaType.MUSIC
        ]:
            mtype = meta.type
        share_query_meta = share_meta or meta
        module_kwargs = {
            "meta": meta,
            "mtype": mtype,
            "media_source": media_source,
            "media_id": media_id,
            "episode_group": episode_group,
            "cache": cache,
        }
        if music_type is not None:
            module_kwargs["music_type"] = music_type
        mediainfo = await self._async_run_native_media_recognize(module_kwargs, cache)
        # 原生识别未取得远端身份时，允许插件按已知要素补充匹配媒体信息（影视与音乐统一）
        mediainfo = await self._async_supplement_media_recognize(
            meta=meta, mtype=mtype, media_source=media_source,
            media_id=media_id, mediainfo=mediainfo,
            music_type=music_type,
        )
        fallback_mediainfo = (
            mediainfo
            if mediainfo and not self._media_info_has_identity(mediainfo)
            else None
        )
        if mediainfo and self._media_info_has_identity(mediainfo):
            # 电影、电视剧、音乐统一上报；音乐的 tmdb 等字段恒为 None，身份取数据源原生 ID
            if not getattr(mediainfo, "recognize_cache_hit", False):
                await MoviePilotServerHelper.async_report_recognize_share(
                    meta=meta,
                    mediainfo=mediainfo,
                    keyword_meta=share_query_meta,
                )
            return mediainfo

        if self._can_use_media_recognize_share(
                share_query_meta, media_source, media_id
        ):
            shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
            share_query_kwargs = {
                "meta": meta,
                "mtype": mtype,
                "keyword_meta": share_query_meta,
            }
            if music_type is not None:
                share_query_kwargs["music_type"] = music_type
            shared_item = await MoviePilotServerHelper.async_query_recognize_share(
                **share_query_kwargs,
            )
            shared_params = MoviePilotServerHelper.to_recognize_params(shared_item)
            if shared_params:
                shared_module_kwargs = {
                        "meta": meta,
                        "mtype": shared_params.get("mtype") or mtype,
                        "media_source": shared_params.get("media_source"),
                        "media_id": shared_params.get("media_id"),
                        "episode_group": episode_group,
                        "cache": cache,
                }
                shared_music_type = shared_params.get("music_type") or music_type
                if shared_music_type is not None:
                    shared_module_kwargs["music_type"] = shared_music_type
                mediainfo = await self._async_run_native_media_recognize(
                    shared_module_kwargs,
                    cache,
                )
                if mediainfo and self._media_info_has_identity(mediainfo):
                    await self._async_update_local_recognize_cache(shared_cache_meta, mediainfo)
                    await run_in_threadpool(self._record_media_recognize_share_hit)
                    return mediainfo
                if mediainfo and not fallback_mediainfo:
                    fallback_mediainfo = mediainfo
        return fallback_mediainfo

    @staticmethod
    def _media_recognize_plugin_payload(
            meta: Optional[MetaBase],
            mtype: Optional[MediaType],
            media_source: Optional[MediaSource],
            media_id: Optional[str],
            is_music: bool,
            music_type: Optional[str] = None,
    ) -> dict:
        """
        构造媒体识别链式事件的已知要素载荷，供插件匹配媒体信息；影视与音乐统一协议，
        仅要素字段随媒体类型不同
        """
        if is_music:
            return {
                "title": getattr(meta, "title", None),
                "artists": list(getattr(meta, "artists", None) or []),
                "album": getattr(meta, "album", None),
                "year": getattr(meta, "year", None),
                "isrc": getattr(meta, "isrc", None),
                "media_source": media_source,
                "media_id": media_id,
                "music_type": music_type,
            }
        return {
            "title": getattr(meta, "title", None) or getattr(meta, "name", None),
            "year": getattr(meta, "year", None),
            "season": getattr(meta, "begin_season", None),
            "type": mtype.value if isinstance(mtype, MediaType) else None,
            "media_source": media_source,
            "media_id": media_id,
        }

    @classmethod
    def _media_info_from_plugin(
            cls,
            event_data: dict,
            is_music: bool,
            mtype: Optional[MediaType] = None,
            music_type: Optional[str] = None,
    ) -> Optional[MediaInfo]:
        """
        解析插件返回的媒体信息，缺少数据源或身份字段的结果不采信；
        音乐构造 MusicInfo，影视构造 MediaInfo
        """
        if not isinstance(event_data, dict):
            return None
        plugin_info = event_data.get("mediainfo")
        if not isinstance(plugin_info, dict):
            return None
        if not plugin_info.get("media_source"):
            logger.warn("插件返回的媒体信息缺少数据源，忽略 ...")
            return None
        try:
            if is_music:
                if not plugin_info.get("media_id"):
                    logger.warn("插件返回的音乐媒体信息缺少媒体ID，忽略 ...")
                    return None
                info: MediaInfo = MusicInfo.from_dict(plugin_info)
                if not info.media_source or not info.media_id:
                    return None
                if music_type and info.music_type != music_type:
                    logger.warn(
                        f"插件返回的音乐实体类型为 {info.music_type}，"
                        f"与请求的 {music_type} 不一致，忽略 ..."
                    )
                    return None
                return info
            # 影视：插件未提供类型时使用请求推断的类型
            if not plugin_info.get("type") and mtype:
                plugin_info = {**plugin_info, "type": mtype}
            info = MediaInfo()
            info.from_dict(plugin_info)
        except Exception as err:
            logger.warn(f"插件返回的媒体信息格式错误：{err}")
            return None
        # 影视与音乐统一要求远端身份，无身份的结果不采信，避免未验证结果进入识别管线
        if not info.media_source or not cls._media_info_has_identity(info):
            logger.warn("插件返回的媒体信息缺少远端身份，忽略 ...")
            return None
        return info

    @staticmethod
    def _media_info_has_identity(mediainfo) -> bool:
        """判断媒体信息是否具备完整的规范媒体身份。"""
        media_source, media_id = resolve_media_identity(media=mediainfo)
        return bool(media_source and media_id)

    def _supplement_media_recognize(
            self,
            meta: Optional[MetaBase],
            mtype: Optional[MediaType],
            media_source: Optional[MediaSource],
            media_id: Optional[str],
            mediainfo,
            music_type: Optional[str] = None,
    ):
        """
        媒体识别插件补充（影视与音乐统一）：原生模块未给出带远端身份的结果时，
        广播媒体识别链式事件，允许插件（如第三方媒体源）按已知要素匹配并返回标准信息
        """
        is_music = (
                isinstance(meta, MetaMusic)
                or mtype == MediaType.MUSIC
                or isinstance(mediainfo, MusicInfo)
        )
        # 已有远端身份时无需插件介入
        if mediainfo and self._media_info_has_identity(mediainfo):
            return mediainfo
        etype = ChainEventType.MusicMediaRecognize if is_music else ChainEventType.MediaRecognize
        if not self.eventmanager.check(etype):
            return mediainfo
        result: Event = self.eventmanager.send_event(
            etype,
            self._media_recognize_plugin_payload(
                meta, mtype, media_source, media_id, is_music, music_type
            ),
        )
        if not result:
            return mediainfo
        plugin_info = self._media_info_from_plugin(
            result.event_data or {}, is_music, mtype, music_type
        )
        if not plugin_info:
            return mediainfo
        logger.info(
            f"插件补充媒体识别成功：{plugin_info.title}"
            f"（{plugin_info.media_source}:{plugin_info.media_id}）"
        )
        return plugin_info

    async def _async_supplement_media_recognize(
            self,
            meta: Optional[MetaBase],
            mtype: Optional[MediaType],
            media_source: Optional[MediaSource],
            media_id: Optional[str],
            mediainfo,
            music_type: Optional[str] = None,
    ):
        """媒体识别插件补充的异步版本，影视与音乐统一流程"""
        is_music = (
                isinstance(meta, MetaMusic)
                or mtype == MediaType.MUSIC
                or isinstance(mediainfo, MusicInfo)
        )
        # 已有远端身份时无需插件介入
        if mediainfo and self._media_info_has_identity(mediainfo):
            return mediainfo
        etype = ChainEventType.MusicMediaRecognize if is_music else ChainEventType.MediaRecognize
        if not self.eventmanager.check(etype):
            return mediainfo
        result: Event = await self.eventmanager.async_send_event(
            etype,
            self._media_recognize_plugin_payload(
                meta, mtype, media_source, media_id, is_music, music_type
            ),
        )
        if not result:
            return mediainfo
        plugin_info = self._media_info_from_plugin(
            result.event_data or {}, is_music, mtype, music_type
        )
        if not plugin_info:
            return mediainfo
        logger.info(
            f"插件补充媒体识别成功：{plugin_info.title}"
            f"（{plugin_info.media_source}:{plugin_info.media_id}）"
        )
        return plugin_info
