import re
from collections.abc import Iterable, Mapping
from typing import Any, Optional, Protocol

from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.domain.context import MusicInfo
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.application.service import ServiceBaseHelper
from app.schemas.system import MediaServerConf
from app.schemas.system import ServiceInfo
from app.schemas.types import (
    MUSIC_ENTITY_ALBUM,
    MediaSource,
    ModuleType,
    SystemConfigKey,
)


class AsyncMediaServerQueryRepository(Protocol):
    """媒体服务器本地条目查询所需的异步持久化端口。"""

    async def async_exists(self, **kwargs: Any) -> Any | None:
        """按标题或统一媒体身份查找已同步条目。"""
        ...


class MediaServerQueryService:
    """封装媒体服务器本地存在性查询与 ORM 投影。"""

    def __init__(self, repository: AsyncMediaServerQueryRepository):
        """使用显式媒体服务器查询端口初始化服务。"""
        self._repository = repository

    async def find_item_id(
            self,
            *,
            title: Optional[str] = None,
            year: Optional[str] = None,
            mtype: Optional[str] = None,
            media_source: Optional[MediaSource] = None,
            media_id: Optional[str] = None,
            season: Optional[int] = None,
    ) -> Optional[str]:
        """返回匹配条目的服务器 item_id，未命中时返回 None。"""
        item = await self._repository.async_exists(
            title=title,
            year=year,
            mtype=mtype,
            media_source=media_source,
            media_id=media_id,
            season=season,
        )
        return item.item_id if item else None


class MediaServerIdentityHelper:
    """将媒体服务器专有 ProviderIds 适配为统一媒体身份。"""

    _provider_keys = (
        (MediaSource.TMDB, ("Tmdb", "TMDB", "tmdb", "tmdb_id")),
        (MediaSource.Douban, ("Douban", "douban", "douban_id")),
        (MediaSource.Bangumi, ("Bangumi", "bangumi", "bangumi_id")),
        (MediaSource.AniList, ("AniList", "Anilist", "anilist", "anilist_id")),
        (MediaSource.IMDb, ("Imdb", "IMDb", "imdb", "imdb_id")),
        (MediaSource.TVDB, ("Tvdb", "TVDB", "tvdb", "tvdb_id")),
        (MediaSource.MusicBrainz, ("MusicBrainz", "musicbrainz", "musicbrainz_id")),
    )

    @classmethod
    def from_provider_ids(
        cls,
        provider_ids: Optional[Mapping[str, Any]],
    ) -> tuple[Optional[MediaSource], Optional[str]]:
        """按固定优先级从外部 ProviderIds 选择一个规范媒体身份。"""
        if not isinstance(provider_ids, Mapping):
            return None, None
        for media_source, keys in cls._provider_keys:
            for key in keys:
                value = provider_ids.get(key)
                if value is not None and str(value).strip():
                    return media_source, str(value).strip()
        return None, None

    @staticmethod
    def are_compatible(
        left_source: Optional[MediaSource | str],
        left_id: Optional[str],
        right_source: Optional[MediaSource | str],
        right_id: Optional[str],
    ) -> bool:
        """判断两组身份是否没有可证实的同来源 ID 冲突。"""
        left_source, left_id = resolve_media_identity(
            media_source=left_source,
            media_id=left_id,
        )
        right_source, right_id = resolve_media_identity(
            media_source=right_source,
            media_id=right_id,
        )
        if not left_source or not right_source:
            return True
        if normalize_media_source(left_source) != normalize_media_source(right_source):
            return True
        return left_id == right_id

    @classmethod
    def is_compatible(
        cls,
        item: _SchemaMediaServerItem,
        media_source: Optional[MediaSource | str],
        media_id: Optional[str],
    ) -> bool:
        """判断目标与媒体库条目是否无明确身份冲突。"""
        item_source, item_id = resolve_media_identity(media=item)
        return cls.are_compatible(item_source, item_id, media_source, media_id)


class MusicMediaServerHelper:
    """统一音乐媒体库条目的字段转换、精确匹配和整专完整性判断。"""

    _name_pattern = re.compile(r"[\W_]+", re.UNICODE)

    @classmethod
    def normalize_name(cls, value: Optional[str]) -> str:
        """忽略大小写、空白和标点，生成用于音乐名称精确比较的稳定文本。"""
        return cls._name_pattern.sub("", str(value or "").casefold())

    @classmethod
    def same_name(cls, left: Optional[str], right: Optional[str]) -> bool:
        """判断两个非空音乐名称在规范化后是否完全一致。"""
        normalized_left = cls.normalize_name(left)
        normalized_right = cls.normalize_name(right)
        return bool(normalized_left) and normalized_left == normalized_right

    @staticmethod
    def _first_value(data: Mapping[str, Any], *keys: str) -> Any:
        """按候选键顺序返回第一个非空字段，兼容不同媒体服务器命名。"""
        for key in keys:
            value = data.get(key)
            if value not in (None, "", []):
                return value
        return None

    @classmethod
    def _extract_names(cls, value: Any) -> list[str]:
        """从字符串、对象列表或名称列表中提取非空名称。"""
        if isinstance(value, str):
            return [value] if value.strip() else []
        if isinstance(value, Mapping):
            name = cls._first_value(value, "Name", "name", "Title", "title")
            return [str(name)] if name and str(name).strip() else []
        if not isinstance(value, Iterable) or isinstance(value, bytes):
            return []
        names: list[str] = []
        for item in value:
            if isinstance(item, Mapping):
                name = cls._first_value(item, "Name", "name", "Title", "title")
            else:
                name = item
            if name and str(name).strip():
                names.append(str(name))
        return names

    @classmethod
    def build_note(cls, item: Mapping[str, Any]) -> dict[str, Any]:
        """把 Emby 系和 NAS 搜索结果中的音乐字段转换为统一备注结构。"""
        artists = cls._extract_names(
            cls._first_value(item, "Artists", "artists", "ArtistItems", "artist_items")
        )
        album_artists = cls._extract_names(
            cls._first_value(item, "AlbumArtists", "album_artists")
        )
        artist = cls._first_value(
            item,
            "AlbumArtist",
            "album_artist",
            "Artist",
            "artist",
            "artist_name",
            "singer",
        )
        explicit_artists = cls._extract_names(artist)
        if explicit_artists:
            artist = explicit_artists[0]
        if not artist:
            artist = next(iter(album_artists or artists), None)

        item_type = cls.normalize_name(
            cls._first_value(item, "Type", "type", "item_type")
        )
        album = cls._first_value(item, "Album", "album", "album_name")
        if not album and item_type in {"musicalbum", "album"}:
            album = cls._first_value(item, "Name", "name", "Title", "title")

        song_count = cls._first_value(
            item,
            "ChildCount",
            "child_count",
            "SongCount",
            "songCount",
            "song_count",
            "TrackCount",
            "trackCount",
            "track_count",
            "LeafCount",
            "leafCount",
        )
        return {
            "artist": str(artist) if artist is not None else None,
            "artists": artists or album_artists,
            "album": str(album) if album is not None else None,
            "song_count": song_count,
        }

    @staticmethod
    def search_params(mediainfo: MusicInfo) -> dict[str, Optional[str]]:
        """按单曲或专辑实体构造媒体服务器音乐搜索参数。"""
        is_album = getattr(mediainfo, "music_type", None) == MUSIC_ENTITY_ALBUM
        artists = getattr(mediainfo, "artists", None) or []
        artist = (
            getattr(mediainfo, "album_artist", None)
            or next(iter(artists), None)
            or getattr(mediainfo, "artist", None)
        )
        title = getattr(mediainfo, "title", None)
        album = getattr(mediainfo, "album", None) or title
        return {
            "title": None if is_album else title,
            "artist": artist,
            "album": album if is_album else None,
        }

    @classmethod
    def item_matches(
        cls,
        mediainfo: MusicInfo,
        item: _SchemaMediaServerItem,
    ) -> bool:
        """校验媒体库条目是否精确对应单曲，或完整覆盖目标专辑。"""
        note = item.note if isinstance(item.note, Mapping) else {}
        is_album = getattr(mediainfo, "music_type", None) == MUSIC_ENTITY_ALBUM
        target_title = getattr(mediainfo, "title", None)
        actual_title = item.title
        if is_album:
            target_title = getattr(mediainfo, "album", None) or target_title
            actual_title = note.get("album") or actual_title
        if not cls.same_name(actual_title, target_title):
            return False

        target_artists = [
            getattr(mediainfo, "artist", None),
            getattr(mediainfo, "album_artist", None),
            *(getattr(mediainfo, "artists", None) or []),
        ]
        target_artists = [artist for artist in target_artists if artist]
        actual_artists = [note.get("artist"), *cls._extract_names(note.get("artists"))]
        actual_artists = [artist for artist in actual_artists if artist]
        if target_artists and not any(
            cls.same_name(actual, target)
            for actual in actual_artists
            for target in target_artists
        ):
            return False

        if not is_album:
            return True
        try:
            expected_tracks = int(getattr(mediainfo, "total_tracks", None) or 0)
            actual_tracks = int(note.get("song_count") or 0)
        except (TypeError, ValueError):
            return False
        return expected_tracks > 0 and actual_tracks >= expected_tracks

    @classmethod
    def find_match(
        cls,
        mediainfo: MusicInfo,
        items: Optional[Iterable[_SchemaMediaServerItem]],
    ) -> Optional[_SchemaMediaServerItem]:
        """返回首个满足单曲精确匹配或整专完整性要求的媒体库条目。"""
        return next(
            (item for item in items or [] if item and cls.item_matches(mediainfo, item)),
            None,
        )


class MediaServerHelper(ServiceBaseHelper[MediaServerConf]):
    """管理媒体服务器配置，并按类型发现已启用的服务实例。"""

    def __init__(self) -> None:
        """绑定媒体服务器配置键、配置模型和模块类型。"""
        super().__init__(
            config_key=SystemConfigKey.MediaServers,
            conf_type=MediaServerConf,
            module_type=ModuleType.MediaServer,
        )

    def is_media_server(
        self,
        service_type: Optional[str] = None,
        service: Optional[ServiceInfo] = None,
        name: Optional[str] = None,
    ) -> bool:
        """判断给定服务或服务名称是否属于指定媒体服务器类型。"""
        service = service or self.get_service(name=name)
        return bool(service and service.type == service_type)
