import asyncio
import logging
from typing import Any

import httpx
import msgspec

from models.search import SearchResult
from core.exceptions import ExternalServiceError
from services.preferences_service import PreferencesService
from infrastructure.cache.memory_cache import CacheInterface
from infrastructure.cache.cache_keys import (
    mb_artist_search_key, mb_artist_detail_key,
    MB_ARTISTS_BY_TAG_PREFIX, MB_ARTIST_RELS_PREFIX,
)
from infrastructure.queue.priority_queue import RequestPriority
from infrastructure.resilience.retry import CircuitOpenError
from repositories.musicbrainz_base import (
    mb_api_get,
    mb_deduplicator,
    dedupe_by_id,
    get_score,
    build_musicbrainz_tag_query,
)
from infrastructure.degradation import try_get_degradation_context
from infrastructure.integration_result import IntegrationResult

logger = logging.getLogger(__name__)


def _record_mb_degradation(msg: str) -> None:
    ctx = try_get_degradation_context()
    if ctx:
        ctx.record(IntegrationResult.error(source="musicbrainz", msg=msg))


class _ArtistSearchPayload(msgspec.Struct):
    artists: list[dict[str, Any]] = msgspec.field(default_factory=list)


class _ArtistReleaseGroupsPayload(msgspec.Struct):
    release_groups: list[dict[str, Any]] = msgspec.field(name="release-groups", default_factory=list)
    release_group_count: int = msgspec.field(name="release-group-count", default=0)


FILTERED_ARTIST_MBIDS = {
    "89ad4ac3-39f7-470e-963a-56509c546377",  # Various Artists
    "41ece0f7-91f6-4c87-982c-3a39c5a02586",  # /v/
    "125ec42a-7229-4250-afc5-e057484327fe",  # [Unknown]
}

FILTERED_ARTIST_NAMES = {
    "various artists",
    "[unknown]",
    "/v/",
}


class MusicBrainzArtistMixin:
    _cache: CacheInterface
    _preferences_service: PreferencesService

    def _map_artist_to_result(self, artist: dict[str, Any]) -> SearchResult | None:
        artist_id = artist.get("id", "")
        if artist_id in FILTERED_ARTIST_MBIDS:
            return None
        
        name = artist.get("name", "Unknown Artist")
        if name.lower() in FILTERED_ARTIST_NAMES:
            return None
        
        return SearchResult(
            type="artist",
            title=name,
            musicbrainz_id=artist_id,
            in_library=False,
            disambiguation=artist.get("disambiguation") or None,
            type_info=artist.get("type") or None,
            score=get_score(artist),
        )

    async def search_artists(
        self,
        query: str,
        limit: int = 10,
        offset: int = 0
    ) -> list[SearchResult]:
        cache_key = mb_artist_search_key(query, limit, offset)

        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached

        try:
            search_query = f'artist:"{query}"^3 OR artistaccent:"{query}"^3 OR alias:"{query}"^2 OR {query}'

            result = await mb_api_get(
                "/artist",
                params={
                    "query": search_query,
                    "limit": min(100, max(limit * 2, 25)),
                    "offset": offset,
                },
                priority=RequestPriority.USER_INITIATED,
                decode_type=_ArtistSearchPayload,
            )
            artists = result.artists
            artists = dedupe_by_id(artists)

            results = []
            for a in artists:
                mapped = self._map_artist_to_result(a)
                if mapped:
                    results.append(mapped)
                if len(results) >= limit:
                    break

            advanced_settings = self._preferences_service.get_advanced_settings()
            await self._cache.set(cache_key, results, ttl_seconds=advanced_settings.cache_ttl_search)
            return results
        except Exception as e:  # noqa: BLE001
            logger.error(f"MusicBrainz artist search failed: {e}")
            _record_mb_degradation(f"artist search failed: {e}")
            return []

    async def search_artists_by_tag(
        self,
        tag: str,
        limit: int = 50,
        offset: int = 0
    ) -> list[SearchResult]:
        cache_key = f"{MB_ARTISTS_BY_TAG_PREFIX}{tag.lower()}:{limit}:{offset}"

        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached

        try:
            result = await mb_api_get(
                "/artist",
                params={
                    "query": build_musicbrainz_tag_query(tag),
                    "limit": min(100, limit),
                    "offset": offset,
                },
                priority=RequestPriority.BACKGROUND_SYNC,
                decode_type=_ArtistSearchPayload,
            )
            artists = result.artists
            artists = dedupe_by_id(artists)

            results = [r for a in artists[:limit] if (r := self._map_artist_to_result(a)) is not None]

            advanced_settings = self._preferences_service.get_advanced_settings()
            await self._cache.set(cache_key, results, ttl_seconds=advanced_settings.cache_ttl_search * 2)
            return results
        except Exception as e:  # noqa: BLE001
            logger.error(f"MusicBrainz artist tag search failed for '{tag}': {e}")
            _record_mb_degradation(f"artist tag search failed: {e}")
            return []

    async def get_artist_by_id(self, mbid: str) -> dict | None:
        cache_key = mb_artist_detail_key(mbid)

        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached

        dedupe_key = f"mb:artist:{mbid}"
        return await mb_deduplicator.dedupe(dedupe_key, lambda: self._fetch_artist_by_id(mbid, cache_key))

    async def get_artist_relations(self, mbid: str) -> dict | None:
        detail_key = mb_artist_detail_key(mbid)
        cached = await self._cache.get(detail_key)
        if cached is not None:
            return cached

        rels_key = f"{MB_ARTIST_RELS_PREFIX}{mbid}"
        cached_rels = await self._cache.get(rels_key)
        if cached_rels is not None:
            return cached_rels

        dedupe_key = f"{MB_ARTIST_RELS_PREFIX}{mbid}"
        return await mb_deduplicator.dedupe(dedupe_key, lambda: self._fetch_artist_relations(mbid, rels_key))

    async def _fetch_artist_relations(self, mbid: str, cache_key: str) -> dict | None:
        try:
            result = await mb_api_get(
                f"/artist/{mbid}",
                params={"inc": "url-rels"},
                priority=RequestPriority.IMAGE_FETCH,
            )
            if not result:
                return None
            await self._cache.set(cache_key, result, ttl_seconds=86400)
            return result
        except (CircuitOpenError, httpx.HTTPError, ExternalServiceError):
            raise
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to fetch artist relations {mbid}: {e}")
            _record_mb_degradation(f"artist relations failed: {e}")
            return None

    async def _fetch_artist_by_id(self, mbid: str, cache_key: str) -> dict | None:
        try:
            limit = 50

            artist_result, browse_result = await asyncio.gather(
                mb_api_get(
                    f"/artist/{mbid}",
                    params={"inc": "tags+aliases+url-rels"},
                    priority=RequestPriority.USER_INITIATED,
                ),
                mb_api_get(
                    "/release-group",
                    params={"artist": mbid, "limit": limit, "offset": 0},
                    priority=RequestPriority.USER_INITIATED,
                    decode_type=_ArtistReleaseGroupsPayload,
                ),
            )

            if not artist_result:
                return None

            all_release_groups = browse_result.release_groups
            total_count = int(browse_result.release_group_count)

            if all_release_groups:
                artist_result["release-group-list"] = all_release_groups

            artist_result["release-group-count"] = total_count

            await self._cache.set(cache_key, artist_result, ttl_seconds=21600)

            from core.task_registry import TaskRegistry
            registry = TaskRegistry.get_instance()
            if not registry.is_running("mb-release-group-warmup"):
                _rg_task = asyncio.create_task(self._warm_release_group_cache(all_release_groups[:6]))
                try:
                    registry.register("mb-release-group-warmup", _rg_task)
                except RuntimeError:
                    pass

            return artist_result
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to fetch artist {mbid}: {e}")
            _record_mb_degradation(f"artist fetch failed: {e}")
            return None

    async def _warm_release_group_cache(self, release_groups: list[dict[str, Any]]) -> None:
        for rg in release_groups:
            rg_id = rg.get("id")
            if not rg_id:
                continue
            try:
                await self.get_release_group_by_id(rg_id, priority=RequestPriority.BACKGROUND_SYNC)
            except (CircuitOpenError, ExternalServiceError, httpx.HTTPError) as exc:
                pass

    async def get_artist_release_groups(
        self,
        artist_mbid: str,
        offset: int = 0,
        limit: int = 50
    ) -> tuple[list[dict[str, Any]], int]:
        try:
            result = await mb_api_get(
                "/release-group",
                params={"artist": artist_mbid, "limit": limit, "offset": offset},
                priority=RequestPriority.BACKGROUND_SYNC,
                decode_type=_ArtistReleaseGroupsPayload,
            )

            release_groups = result.release_groups
            total_count = int(result.release_group_count)

            return release_groups, total_count
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to fetch release groups for artist {artist_mbid} at offset {offset}: {e}")
            _record_mb_degradation(f"release groups failed: {e}")
            return [], 0

    async def get_release_groups_by_artist(
        self,
        artist_mbid: str,
        limit: int = 10
    ) -> list[dict[str, Any]]:
        release_groups, _ = await self.get_artist_release_groups(artist_mbid, offset=0, limit=limit)
        return release_groups
