import logging
import time
from typing import Any

import httpx
import msgspec

from core.exceptions import ExternalServiceError, RateLimitedError
from infrastructure.resilience.rate_limiter import TokenBucketRateLimiter
from infrastructure.resilience.retry import CircuitBreaker, CircuitOpenError, with_retry
from repositories.audiodb_models import (
    AudioDBAlbumResponse,
    AudioDBArtistResponse,
)
from services.preferences_service import PreferencesService
from infrastructure.degradation import try_get_degradation_context
from infrastructure.integration_result import IntegrationResult

logger = logging.getLogger(__name__)

_SOURCE = "audiodb"


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

AUDIODB_API_URL = "https://www.theaudiodb.com/api/v1/json"


def _log_circuit_state_change(
    breaker: CircuitBreaker,
    previous_state,
    new_state,
    reason: str,
) -> None:
    level = logging.INFO if new_state.value == "closed" else logging.WARNING
    logger.log(
        level,
        "audiodb.circuit_state_change service=%s previous_state=%s state=%s reason=%s",
        breaker.name,
        previous_state.value,
        new_state.value,
        reason,
    )

_audiodb_circuit_breaker = CircuitBreaker(
    failure_threshold=5,
    success_threshold=2,
    timeout=60.0,
    name="audiodb",
    on_state_change=_log_circuit_state_change,
)

AUDIODB_FREE_KEY = "123"

AudioDBJson = dict[str, Any]


def _make_rate_limiter(premium: bool = False) -> TokenBucketRateLimiter:
    if premium:
        return TokenBucketRateLimiter(rate=5.0, capacity=10)
    return TokenBucketRateLimiter(rate=0.5, capacity=2)


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


def _extract_first(data: dict[str, Any], key: str) -> dict[str, Any] | None:
    items = data.get(key)
    if not items or not isinstance(items, list) or len(items) == 0:
        return None
    return items[0]


class AudioDBRepository:
    def __init__(
        self,
        http_client: httpx.AsyncClient,
        preferences_service: PreferencesService,
        api_key: str = "123",
        premium: bool = False,
    ):
        self._client = http_client
        self._preferences_service = preferences_service
        self._api_key = api_key
        self._rate_limiter = _make_rate_limiter(premium)

    def _is_enabled(self) -> bool:
        return self._preferences_service.get_advanced_settings().audiodb_enabled

    def _effective_api_key(self) -> str:
        settings_key = self._preferences_service.get_advanced_settings().audiodb_api_key
        if settings_key and settings_key.strip():
            return settings_key
        return self._api_key

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

    @with_retry(
        max_attempts=3,
        base_delay=2.0,
        max_delay=10.0,
        circuit_breaker=_audiodb_circuit_breaker,
        retriable_exceptions=(httpx.HTTPError, ExternalServiceError, RateLimitedError),
    )
    async def _request(self, endpoint: str, params: dict[str, str] | None = None) -> dict[str, Any] | None:
        await self._rate_limiter.acquire()

        url = f"{AUDIODB_API_URL}/{self._effective_api_key()}/{endpoint}"

        try:
            t0 = time.monotonic()
            response = await self._client.get(url, params=params, timeout=15.0)
            elapsed_ms = (time.monotonic() - t0) * 1000

            if response.status_code == 429:
                logger.warning("audiodb.ratelimit status=429 elapsed_ms=%.1f retry_after_s=60", elapsed_ms)
                raise RateLimitedError("AudioDB rate limit exceeded", retry_after_seconds=60)

            if response.status_code == 404:
                return None

            if response.status_code != 200:
                raise ExternalServiceError(
                    f"AudioDB request failed ({response.status_code})"
                )

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

            return data

        except (ExternalServiceError, RateLimitedError):
            raise
        except httpx.HTTPError as e:
            raise ExternalServiceError(f"AudioDB request failed: {e}")

    async def get_artist_by_mbid(self, mbid: str) -> AudioDBArtistResponse | None:
        if not self._is_enabled() or not mbid:
            return None

        try:
            return await self._get_artist_by_mbid(mbid)
        except CircuitOpenError:
            logger.warning("audiodb.circuit_open entity=artist lookup_type=mbid mbid=%s", mbid)
            _record_degradation(f"Circuit open: artist lookup by mbid {mbid}")
            return None

    async def _get_artist_by_mbid(self, mbid: str) -> AudioDBArtistResponse | None:
        t0 = time.monotonic()
        data = await self._request("artist-mb.php", params={"i": mbid})
        elapsed_ms = (time.monotonic() - t0) * 1000

        if data is None:
            return None

        item = _extract_first(data, "artists")
        if item is None:
            return None

        try:
            result = msgspec.convert(item, type=AudioDBArtistResponse)
        except (msgspec.ValidationError, msgspec.DecodeError, TypeError, KeyError) as exc:
            logger.warning("audiodb.schema_error entity=artist lookup_type=mbid mbid=%s error=%s", mbid, exc)
            _record_degradation(f"Schema error for artist mbid {mbid}: {exc}")
            return None
        return result

    async def get_album_by_mbid(self, mbid: str) -> AudioDBAlbumResponse | None:
        if not self._is_enabled() or not mbid:
            return None

        try:
            return await self._get_album_by_mbid(mbid)
        except CircuitOpenError:
            logger.warning("audiodb.circuit_open entity=album lookup_type=mbid mbid=%s", mbid)
            _record_degradation(f"Circuit open: album lookup by mbid {mbid}")
            return None

    async def _get_album_by_mbid(self, mbid: str) -> AudioDBAlbumResponse | None:
        t0 = time.monotonic()
        data = await self._request("album-mb.php", params={"i": mbid})
        elapsed_ms = (time.monotonic() - t0) * 1000

        if data is None:
            return None

        item = _extract_first(data, "album")
        if item is None:
            return None

        try:
            result = msgspec.convert(item, type=AudioDBAlbumResponse)
        except (msgspec.ValidationError, msgspec.DecodeError, TypeError, KeyError) as exc:
            logger.warning("audiodb.schema_error entity=album lookup_type=mbid mbid=%s error=%s", mbid, exc)
            _record_degradation(f"Schema error for album mbid {mbid}: {exc}")
            return None
        return result

    async def search_artist_by_name(self, name: str) -> AudioDBArtistResponse | None:
        if not self._is_enabled() or not name:
            return None

        try:
            return await self._search_artist_by_name(name)
        except CircuitOpenError:
            logger.warning("audiodb.circuit_open entity=artist lookup_type=name name=%s", name)
            _record_degradation("Circuit open: artist search by name")
            return None

    async def _search_artist_by_name(self, name: str) -> AudioDBArtistResponse | None:
        t0 = time.monotonic()
        data = await self._request("search.php", params={"s": name})
        elapsed_ms = (time.monotonic() - t0) * 1000

        if data is None:
            return None

        item = _extract_first(data, "artists")
        if item is None:
            return None

        try:
            result = msgspec.convert(item, type=AudioDBArtistResponse)
        except (msgspec.ValidationError, msgspec.DecodeError, TypeError, KeyError) as exc:
            logger.warning("audiodb.schema_error entity=artist lookup_type=name name=%s error=%s", name, exc)
            _record_degradation(f"Schema error for artist name search: {exc}")
            return None
        return result

    async def search_album_by_name(self, artist: str, album: str) -> AudioDBAlbumResponse | None:
        if not self._is_enabled() or not artist or not album:
            return None

        try:
            return await self._search_album_by_name(artist, album)
        except CircuitOpenError:
            logger.warning("audiodb.circuit_open entity=album lookup_type=name artist=%s album=%s", artist, album)
            _record_degradation("Circuit open: album search by name")
            return None

    async def _search_album_by_name(self, artist: str, album: str) -> AudioDBAlbumResponse | None:
        t0 = time.monotonic()
        data = await self._request("searchalbum.php", params={"s": artist, "a": album})
        elapsed_ms = (time.monotonic() - t0) * 1000

        if data is None:
            return None

        item = _extract_first(data, "album")
        if item is None:
            return None

        try:
            result = msgspec.convert(item, type=AudioDBAlbumResponse)
        except (msgspec.ValidationError, msgspec.DecodeError, TypeError, KeyError) as exc:
            logger.warning(
                "audiodb.schema_error entity=album lookup_type=name artist=%s album=%s error=%s",
                artist,
                album,
                exc,
            )
            _record_degradation(f"Schema error for album name search: {exc}")
            return None
        return result
