import hashlib
import logging
from typing import Any

import httpx
import msgspec

from core.exceptions import (
    ConfigurationError,
    ExternalServiceError,
    ResourceNotFoundError,
    TokenNotAuthorizedError,
)
from infrastructure.cache.cache_keys import LFM_PREFIX
from infrastructure.cache.memory_cache import CacheInterface
from infrastructure.resilience.rate_limiter import TokenBucketRateLimiter
from infrastructure.resilience.retry import CircuitBreaker, with_retry
from repositories.lastfm_models import (
    ALLOWED_LASTFM_PERIOD,
    LastFmAlbum,
    LastFmAlbumInfo,
    LastFmArtist,
    LastFmArtistInfo,
    LastFmLovedTrack,
    LastFmRecentTrack,
    LastFmSession,
    LastFmSimilarArtist,
    LastFmToken,
    LastFmTrack,
    parse_album_info,
    parse_artist_info,
    parse_loved_track,
    parse_recent_track,
    parse_session,
    parse_similar_artist,
    parse_token,
    parse_top_album,
    parse_top_artist,
    parse_top_track,
    parse_weekly_album_chart_item,
)
from infrastructure.degradation import try_get_degradation_context
from infrastructure.integration_result import IntegrationResult

logger = logging.getLogger(__name__)

_SOURCE = "lastfm"


def _record_degradation(msg: str) -> None:
    ctx = try_get_degradation_context()
    if ctx is not None:
        ctx.record(IntegrationResult.error(source=_SOURCE, msg=msg))

LASTFM_API_URL = "https://ws.audioscrobbler.com/2.0/"

_lastfm_circuit_breaker = CircuitBreaker(
    failure_threshold=5,
    success_threshold=2,
    timeout=60.0,
    name="lastfm",
)

_lastfm_rate_limiter = TokenBucketRateLimiter(rate=5.0, capacity=10)

LASTFM_ERROR_MAP: dict[int, tuple[type[Exception], str]] = {
    2: (ExternalServiceError, "Invalid service - This service does not exist"),
    3: (ExternalServiceError, "Invalid method - No method with that name in this package"),
    4: (ConfigurationError, "Authentication failed - invalid API key or shared secret"),
    6: (ResourceNotFoundError, "Not found"),
    9: (ConfigurationError, "Session key expired - please re-authorize with Last.fm"),
    10: (ConfigurationError, "Invalid API key - check your Last.fm API key"),
    11: (ExternalServiceError, "Last.fm service is temporarily offline"),
    14: (TokenNotAuthorizedError, "Token not yet authorized"),
    17: (ConfigurationError, "Authentication required - re-authorize Last.fm or make your listening history public"),
    26: (ConfigurationError, "API key has been suspended - contact Last.fm support"),
    29: (ExternalServiceError, "Rate limit exceeded"),
}

LASTFM_USER_CACHE_TTL = 300
LASTFM_ENTITY_CACHE_TTL = 3600
LASTFM_GLOBAL_CACHE_TTL = 3600

LastFmJsonObject = dict[str, Any]
LastFmJsonArray = list[LastFmJsonObject]
LastFmJson = LastFmJsonObject | LastFmJsonArray


def _decode_json_response(response: httpx.Response) -> LastFmJson:
    content = getattr(response, "content", None)
    if isinstance(content, (bytes, bytearray, memoryview)):
        return msgspec.json.decode(content, type=LastFmJson)
    return response.json()


class LastFmRepository:
    def __init__(
        self,
        http_client: httpx.AsyncClient,
        cache: CacheInterface,
        api_key: str = "",
        shared_secret: str = "",
        session_key: str = "",
    ):
        self._client = http_client
        self._cache = cache
        self._api_key = api_key
        self._shared_secret = shared_secret
        self._session_key = session_key

    @property
    def _can_sign(self) -> bool:
        return bool(self._shared_secret) and bool(self._session_key)

    def configure(self, api_key: str, shared_secret: str, session_key: str = "") -> None:
        self._api_key = api_key
        self._shared_secret = shared_secret
        self._session_key = session_key

    @staticmethod
    def reset_circuit_breaker() -> None:
        _lastfm_circuit_breaker.reset()

    def _build_api_sig(self, params: dict[str, str]) -> str:
        filtered = {k: v for k, v in sorted(params.items()) if k not in ("format", "callback")}
        sig_string = "".join(f"{k}{v}" for k, v in filtered.items())
        sig_string += self._shared_secret
        return hashlib.md5(sig_string.encode("utf-8")).hexdigest()

    def _handle_error_response(self, data: dict[str, Any]) -> None:
        error_code = data.get("error")
        error_message = data.get("message", "Unknown Last.fm error")

        if error_code is None:
            return

        mapped = LASTFM_ERROR_MAP.get(error_code)
        if mapped:
            exc_type, default_msg = mapped
            raise exc_type(f"{default_msg}: {error_message}")

        logger.warning("Last.fm error code=%d message=%s", error_code, error_message)
        raise ExternalServiceError(f"Last.fm error ({error_code}): {error_message}")

    @with_retry(
        max_attempts=3,
        base_delay=1.0,
        max_delay=3.0,
        circuit_breaker=_lastfm_circuit_breaker,
        retriable_exceptions=(httpx.HTTPError, ExternalServiceError),
    )
    async def _request(
        self,
        method: str,
        params: dict[str, str] | None = None,
        signed: bool = False,
        http_method: str = "GET",
    ) -> dict[str, Any]:
        if not self._api_key:
            raise ConfigurationError("Last.fm API key is not configured")

        await _lastfm_rate_limiter.acquire()

        request_params: dict[str, str] = {
            "method": method,
            "api_key": self._api_key,
            "format": "json",
        }
        if params:
            request_params.update(params)

        if signed:
            if not self._shared_secret:
                raise ConfigurationError("Last.fm shared secret is required for signed requests")
            if self._session_key and "sk" not in request_params:
                request_params["sk"] = self._session_key
            request_params["api_sig"] = self._build_api_sig(request_params)

        try:
            if http_method == "POST":
                response = await self._client.post(
                    LASTFM_API_URL,
                    data=request_params,
                    timeout=15.0,
                )
            else:
                response = await self._client.get(
                    LASTFM_API_URL,
                    params=request_params,
                    timeout=15.0,
                )

            if response.status_code != 200:
                raise ExternalServiceError(
                    f"Last.fm request failed ({response.status_code})",
                    response.text,
                )

            try:
                data = _decode_json_response(response)
            except (msgspec.DecodeError, ValueError, TypeError):
                raise ExternalServiceError("Last.fm returned invalid JSON")

            self._handle_error_response(data)
            _lastfm_circuit_breaker.record_success()
            return data

        except (ConfigurationError, ExternalServiceError, ResourceNotFoundError):
            raise
        except httpx.HTTPError as e:
            raise ExternalServiceError(f"Last.fm request failed: {e}")

    async def get_token(self) -> LastFmToken:
        data = await self._request("auth.getToken", signed=True, http_method="GET")
        return parse_token(data)

    async def get_session(self, token: str) -> LastFmSession:
        data = await self._request(
            "auth.getSession",
            params={"token": token},
            signed=True,
            http_method="GET",
        )
        return parse_session(data)

    async def validate_api_key(self) -> tuple[bool, str]:
        try:
            await self._request(
                "chart.getTopArtists",
                params={"limit": "1"},
                http_method="GET",
            )
            return True, "API key is valid"
        except ConfigurationError as e:
            return False, str(e.message)
        except ExternalServiceError as e:
            return False, f"Connection failed: {e.message}"

    async def validate_session(self) -> tuple[bool, str]:
        if not self._session_key:
            return False, "No session key configured"
        try:
            data = await self._request(
                "user.getInfo",
                signed=True,
                http_method="GET",
            )
            user = data.get("user", {})
            username = user.get("name", "")
            return True, f"Connected as {username}"
        except ConfigurationError as e:
            return False, str(e.message)
        except ExternalServiceError as e:
            return False, f"Session validation failed: {e.message}"

    async def update_now_playing(
        self,
        artist: str,
        track: str,
        album: str = "",
        duration: int = 0,
        mbid: str | None = None,
    ) -> bool:
        params: dict[str, str] = {"artist": artist, "track": track}
        if album:
            params["album"] = album
        if duration > 0:
            params["duration"] = str(duration)
        if mbid:
            params["mbid"] = mbid
        await self._request(
            "track.updateNowPlaying",
            params=params,
            signed=True,
            http_method="POST",
        )
        return True

    async def scrobble(
        self,
        artist: str,
        track: str,
        timestamp: int,
        album: str = "",
        duration: int = 0,
        mbid: str | None = None,
    ) -> bool:
        params: dict[str, str] = {
            "artist": artist,
            "track": track,
            "timestamp": str(timestamp),
        }
        if album:
            params["album"] = album
        if duration > 0:
            params["duration"] = str(duration)
        if mbid:
            params["mbid"] = mbid
        await self._request(
            "track.scrobble",
            params=params,
            signed=True,
            http_method="POST",
        )
        return True


    async def get_user_top_artists(
        self, username: str, period: str = "overall", limit: int = 50
    ) -> list[LastFmArtist]:
        if period not in ALLOWED_LASTFM_PERIOD:
            period = "overall"
        cache_key = f"{LFM_PREFIX}user_top_artists:{username}:{period}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "user.getTopArtists",
            params={"user": username, "period": period, "limit": str(limit)},
            signed=self._can_sign,
        )
        artists = [
            parse_top_artist(item)
            for item in data.get("topartists", {}).get("artist", [])
        ]
        await self._cache.set(cache_key, artists, ttl_seconds=LASTFM_USER_CACHE_TTL)
        return artists

    async def get_user_top_albums(
        self, username: str, period: str = "overall", limit: int = 50
    ) -> list[LastFmAlbum]:
        if period not in ALLOWED_LASTFM_PERIOD:
            period = "overall"
        cache_key = f"{LFM_PREFIX}user_top_albums:{username}:{period}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "user.getTopAlbums",
            params={"user": username, "period": period, "limit": str(limit)},
            signed=self._can_sign,
        )
        albums = [
            parse_top_album(item)
            for item in data.get("topalbums", {}).get("album", [])
        ]
        await self._cache.set(cache_key, albums, ttl_seconds=LASTFM_USER_CACHE_TTL)
        return albums

    async def get_user_top_tracks(
        self, username: str, period: str = "overall", limit: int = 50
    ) -> list[LastFmTrack]:
        if period not in ALLOWED_LASTFM_PERIOD:
            period = "overall"
        cache_key = f"{LFM_PREFIX}user_top_tracks:{username}:{period}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "user.getTopTracks",
            params={"user": username, "period": period, "limit": str(limit)},
            signed=self._can_sign,
        )
        tracks = [
            parse_top_track(item)
            for item in data.get("toptracks", {}).get("track", [])
        ]
        await self._cache.set(cache_key, tracks, ttl_seconds=LASTFM_USER_CACHE_TTL)
        return tracks

    async def get_user_recent_tracks(
        self, username: str, limit: int = 50
    ) -> list[LastFmRecentTrack]:
        cache_key = f"{LFM_PREFIX}user_recent:{username}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "user.getRecentTracks",
            params={"user": username, "limit": str(limit), "extended": "0"},
            signed=self._can_sign,
        )
        tracks = [
            parse_recent_track(item)
            for item in data.get("recenttracks", {}).get("track", [])
        ]
        await self._cache.set(cache_key, tracks, ttl_seconds=LASTFM_USER_CACHE_TTL)
        return tracks

    async def get_user_loved_tracks(
        self, username: str, limit: int = 50
    ) -> list[LastFmLovedTrack]:
        cache_key = f"{LFM_PREFIX}user_loved_tracks:{username}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "user.getLovedTracks",
            params={"user": username, "limit": str(limit)},
            signed=self._can_sign,
        )
        tracks = [
            parse_loved_track(item)
            for item in data.get("lovedtracks", {}).get("track", [])
        ]
        await self._cache.set(cache_key, tracks, ttl_seconds=LASTFM_USER_CACHE_TTL)
        return tracks

    async def get_user_weekly_artist_chart(
        self, username: str
    ) -> list[LastFmArtist]:
        cache_key = f"{LFM_PREFIX}user_weekly_artists:{username}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "user.getWeeklyArtistChart",
            params={"user": username},
            signed=self._can_sign,
        )
        artists = [
            parse_top_artist(item)
            for item in data.get("weeklyartistchart", {}).get("artist", [])
        ]
        await self._cache.set(cache_key, artists, ttl_seconds=LASTFM_USER_CACHE_TTL)
        return artists

    async def get_user_weekly_album_chart(
        self, username: str
    ) -> list[LastFmAlbum]:
        cache_key = f"{LFM_PREFIX}user_weekly_albums:{username}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "user.getWeeklyAlbumChart",
            params={"user": username},
            signed=self._can_sign,
        )
        albums = [
            parse_weekly_album_chart_item(item)
            for item in data.get("weeklyalbumchart", {}).get("album", [])
        ]
        await self._cache.set(cache_key, albums, ttl_seconds=LASTFM_USER_CACHE_TTL)
        return albums


    async def get_artist_top_tracks(
        self, artist: str, mbid: str | None = None, limit: int = 10
    ) -> list[LastFmTrack]:
        lookup = mbid or artist
        cache_key = f"{LFM_PREFIX}artist_top_tracks:{lookup}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        params: dict[str, str] = {"limit": str(limit)}
        if mbid:
            params["mbid"] = mbid
        else:
            params["artist"] = artist
        data = await self._request("artist.getTopTracks", params=params)
        tracks = [
            parse_top_track(item)
            for item in data.get("toptracks", {}).get("track", [])
        ]
        await self._cache.set(cache_key, tracks, ttl_seconds=LASTFM_ENTITY_CACHE_TTL)
        return tracks

    async def get_artist_top_albums(
        self, artist: str, mbid: str | None = None, limit: int = 10
    ) -> list[LastFmAlbum]:
        lookup = mbid or artist
        cache_key = f"{LFM_PREFIX}artist_top_albums:{lookup}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        params: dict[str, str] = {"limit": str(limit)}
        if mbid:
            params["mbid"] = mbid
        else:
            params["artist"] = artist
        data = await self._request("artist.getTopAlbums", params=params)
        albums = [
            parse_top_album(item)
            for item in data.get("topalbums", {}).get("album", [])
        ]
        await self._cache.set(cache_key, albums, ttl_seconds=LASTFM_ENTITY_CACHE_TTL)
        return albums

    async def get_artist_info(
        self, artist: str, mbid: str | None = None, username: str | None = None
    ) -> LastFmArtistInfo | None:
        lookup = mbid or artist
        cache_key = f"{LFM_PREFIX}artist_info:{lookup}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        params: dict[str, str] = {}
        if mbid:
            params["mbid"] = mbid
        else:
            params["artist"] = artist
        if username:
            params["username"] = username
        try:
            data = await self._request("artist.getInfo", params=params)
        except ResourceNotFoundError:
            return None
        info = parse_artist_info(data)
        await self._cache.set(cache_key, info, ttl_seconds=LASTFM_ENTITY_CACHE_TTL)
        return info

    async def get_album_info(
        self,
        artist: str,
        album: str,
        mbid: str | None = None,
        username: str | None = None,
    ) -> LastFmAlbumInfo | None:
        lookup = mbid or f"{artist}:{album}"
        cache_key = f"{LFM_PREFIX}album_info:{lookup}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        params: dict[str, str] = {}
        if mbid:
            params["mbid"] = mbid
        else:
            params["artist"] = artist
            params["album"] = album
        if username:
            params["username"] = username
        try:
            data = await self._request("album.getInfo", params=params)
        except ResourceNotFoundError:
            return None
        info = parse_album_info(data)
        await self._cache.set(cache_key, info, ttl_seconds=LASTFM_ENTITY_CACHE_TTL)
        return info

    async def get_similar_artists(
        self, artist: str, mbid: str | None = None, limit: int = 30
    ) -> list[LastFmSimilarArtist]:
        lookup = mbid or artist
        cache_key = f"{LFM_PREFIX}similar_artists:{lookup}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        params: dict[str, str] = {"limit": str(limit)}
        if mbid:
            params["mbid"] = mbid
        else:
            params["artist"] = artist
        data = await self._request("artist.getSimilar", params=params)
        similar = [
            parse_similar_artist(item)
            for item in data.get("similarartists", {}).get("artist", [])
        ]
        await self._cache.set(cache_key, similar, ttl_seconds=LASTFM_ENTITY_CACHE_TTL)
        return similar


    async def get_global_top_artists(self, limit: int = 50) -> list[LastFmArtist]:
        cache_key = f"{LFM_PREFIX}global_top_artists:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "chart.getTopArtists",
            params={"limit": str(limit)},
        )
        artists = [
            parse_top_artist(item)
            for item in data.get("artists", {}).get("artist", [])
        ]
        await self._cache.set(cache_key, artists, ttl_seconds=LASTFM_GLOBAL_CACHE_TTL)
        return artists

    async def get_global_top_tracks(self, limit: int = 50) -> list[LastFmTrack]:
        cache_key = f"{LFM_PREFIX}global_top_tracks:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "chart.getTopTracks",
            params={"limit": str(limit)},
        )
        tracks = [
            parse_top_track(item)
            for item in data.get("toptracks", {}).get("track", [])
        ]
        await self._cache.set(cache_key, tracks, ttl_seconds=LASTFM_GLOBAL_CACHE_TTL)
        return tracks

    async def get_tag_top_artists(
        self, tag: str, limit: int = 50
    ) -> list[LastFmArtist]:
        cache_key = f"{LFM_PREFIX}tag_top_artists:{tag}:{limit}"
        cached = await self._cache.get(cache_key)
        if cached is not None:
            return cached
        data = await self._request(
            "tag.getTopArtists",
            params={"tag": tag, "limit": str(limit)},
        )
        artists = [
            parse_top_artist(item)
            for item in data.get("topartists", {}).get("artist", [])
        ]
        await self._cache.set(cache_key, artists, ttl_seconds=LASTFM_GLOBAL_CACHE_TTL)
        return artists
