"""AudioDB image pre-warming phase."""

from __future__ import annotations

import asyncio
import logging
from typing import Any, TYPE_CHECKING

import httpx

from repositories.protocols import CoverArtRepositoryProtocol
from repositories.coverart_disk_cache import get_cache_filename, VALID_IMAGE_CONTENT_TYPES
from services.cache_status_service import CacheStatusService
from infrastructure.queue.priority_queue import RequestPriority, get_priority_queue
from infrastructure.validators import validate_audiodb_image_url

if TYPE_CHECKING:
    from services.audiodb_image_service import AudioDBImageService

logger = logging.getLogger(__name__)

_AUDIODB_PREWARM_INTER_ITEM_DELAY = 2.0
_AUDIODB_PREWARM_LOG_INTERVAL = 100


class AudioDBPhase:
    def __init__(
        self,
        cover_repo: CoverArtRepositoryProtocol,
        preferences_service: Any,
        audiodb_image_service: 'AudioDBImageService | None' = None,
    ):
        self._cover_repo = cover_repo
        self._preferences_service = preferences_service
        self._audiodb_image_service = audiodb_image_service

    async def check_cache_needs(
        self,
        artists: list[dict],
        albums: list[Any],
    ) -> tuple[list[dict], list[Any]]:
        from infrastructure.validators import is_unknown_mbid
        svc = self._audiodb_image_service

        async def check_artist(artist: dict) -> dict | None:
            mbid = artist.get('mbid')
            if not mbid or is_unknown_mbid(mbid):
                return None
            cached = await svc.get_cached_artist_images(mbid)
            return None if cached is not None else artist

        async def check_album(album: Any) -> Any | None:
            mbid = getattr(album, 'musicbrainz_id', None) if hasattr(album, 'musicbrainz_id') else album.get('mbid') if isinstance(album, dict) else None
            if not mbid or is_unknown_mbid(mbid):
                return None
            cached = await svc.get_cached_album_images(mbid)
            return None if cached is not None else album

        artist_results = await asyncio.gather(
            *(check_artist(a) for a in artists), return_exceptions=True
        )
        album_results = await asyncio.gather(
            *(check_album(a) for a in albums), return_exceptions=True
        )
        needed_artists = [r for r in artist_results if r is not None and not isinstance(r, Exception)]
        needed_albums = [r for r in album_results if r is not None and not isinstance(r, Exception)]
        return needed_artists, needed_albums

    async def download_bytes(self, url: str, entity_type: str, mbid: str) -> bool:
        try:
            if not validate_audiodb_image_url(url):
                logger.warning(
                    "audiodb.prewarm action=rejected_unsafe_url entity_type=%s mbid=%s",
                    entity_type, mbid[:8],
                )
                return False

            if entity_type == "artist":
                identifier = f"artist_{mbid}_500"
                suffix = "img"
            else:
                identifier = f"rg_{mbid}"
                suffix = "500"
            file_path = self._cover_repo.cache_dir / f"{get_cache_filename(identifier, suffix)}.bin"
            if file_path.exists():
                return True

            priority_mgr = get_priority_queue()
            semaphore = await priority_mgr.acquire_slot(RequestPriority.BACKGROUND_SYNC)
            http_client = getattr(self._cover_repo, '_client', None)
            async with semaphore:
                if http_client is not None:
                    response = await http_client.get(url, follow_redirects=True)
                else:
                    logger.debug("audiodb.prewarm action=http_client_fallback entity_type=%s mbid=%s", entity_type, mbid[:8])
                    async with httpx.AsyncClient(timeout=httpx.Timeout(15.0, connect=5.0)) as client:
                        response = await client.get(url, headers={"User-Agent": "MusicSeerr/1.0"}, follow_redirects=True)

            if response.status_code != 200:
                logger.debug(
                    "audiodb.prewarm action=byte_download_failed entity_type=%s mbid=%s status=%d",
                    entity_type, mbid[:8], response.status_code,
                )
                return False

            content_type = response.headers.get("content-type", "").split(";")[0].strip().lower()
            if content_type not in VALID_IMAGE_CONTENT_TYPES:
                logger.debug(
                    "audiodb.prewarm action=byte_download_invalid_type entity_type=%s mbid=%s content_type=%s",
                    entity_type, mbid[:8], content_type,
                )
                return False

            _MAX_IMAGE_BYTES = 20 * 1024 * 1024
            content_length = response.headers.get("content-length")
            if content_length and int(content_length) > _MAX_IMAGE_BYTES:
                logger.warning(
                    "audiodb.prewarm action=byte_download_too_large entity_type=%s mbid=%s size=%s",
                    entity_type, mbid[:8], content_length,
                )
                return False
            if len(response.content) > _MAX_IMAGE_BYTES:
                logger.warning(
                    "audiodb.prewarm action=byte_download_too_large entity_type=%s mbid=%s size=%d",
                    entity_type, mbid[:8], len(response.content),
                )
                return False

            disk_cache = getattr(self._cover_repo, '_disk_cache', None)
            if disk_cache is None:
                file_path.parent.mkdir(parents=True, exist_ok=True)
                import aiofiles
                async with aiofiles.open(file_path, 'wb') as f:
                    await f.write(response.content)
                return True

            await disk_cache.write(
                file_path, response.content, content_type,
                extra_meta={"source": "audiodb"},
                is_monitored=True,
            )
            return True
        except Exception as e:  # noqa: BLE001
            logger.warning(
                "audiodb.prewarm action=byte_download_error entity_type=%s mbid=%s error=%s",
                entity_type, mbid[:8], e,
            )
            return False

    def sort_by_cover_priority(self, items: list, entity_type: str) -> list:
        def has_cover(item: Any) -> bool:
            if entity_type == "artist":
                mbid = item.get('mbid') if isinstance(item, dict) else None
                if not mbid:
                    return True
                identifier = f"artist_{mbid}_500"
                suffix = "img"
            else:
                mbid = getattr(item, 'musicbrainz_id', None) if hasattr(item, 'musicbrainz_id') else item.get('mbid') if isinstance(item, dict) else None
                if not mbid:
                    return True
                identifier = f"rg_{mbid}"
                suffix = "500"
            file_path = self._cover_repo.cache_dir / f"{get_cache_filename(identifier, suffix)}.bin"
            return file_path.exists()

        return sorted(items, key=has_cover)

    async def precache_audiodb_data(
        self,
        artists: list[dict],
        albums: list[Any],
        status_service: CacheStatusService,
    ) -> None:
        if self._audiodb_image_service is None:
            await status_service.skip_phase('audiodb_prewarm')
            return

        settings = self._preferences_service.get_advanced_settings()
        if not settings.audiodb_enabled:
            logger.info("AudioDB pre-warming skipped (audiodb_enabled=false)")
            await status_service.skip_phase('audiodb_prewarm')
            return

        needed_artists, needed_albums = await self.check_cache_needs(artists, albums)
        total = len(needed_artists) + len(needed_albums)
        if total == 0:
            logger.info("AudioDB prewarm: all items already cached")
            await status_service.skip_phase('audiodb_prewarm')
            return

        original_total = len(artists) + len(albums)
        hit_rate = ((original_total - total) / original_total * 100) if original_total > 0 else 100
        logger.info(
            "Phase 5 (AudioDB): Pre-warming %d items (%d artists, %d albums) — %.0f%% already cached",
            total, len(needed_artists), len(needed_albums), hit_rate,
        )
        await status_service.update_phase('audiodb_prewarm', total)

        needed_artists = self.sort_by_cover_priority(needed_artists, "artist")
        needed_albums = self.sort_by_cover_priority(needed_albums, "album")

        processed = 0
        bytes_ok = 0
        bytes_fail = 0
        svc = self._audiodb_image_service

        for artist in needed_artists:
            if status_service.is_cancelled():
                logger.info("AudioDB pre-warming cancelled during artist phase")
                break
            if not self._preferences_service.get_advanced_settings().audiodb_enabled:
                logger.info("AudioDB disabled during pre-warming, stopping")
                break

            mbid = artist.get('mbid')
            name = artist.get('name', 'Unknown')
            processed += 1
            try:
                result = await svc.fetch_and_cache_artist_images(mbid, name, is_monitored=True)
                if result and not result.is_negative and result.thumb_url:
                    if await self.download_bytes(result.thumb_url, "artist", mbid):
                        bytes_ok += 1
                    else:
                        bytes_fail += 1
            except Exception as e:  # noqa: BLE001
                logger.warning("audiodb.prewarm action=artist_error mbid=%s error=%s", mbid[:8] if mbid else '?', e)

            await status_service.update_progress(processed, f"AudioDB: {name}")

            if processed % _AUDIODB_PREWARM_LOG_INTERVAL == 0:
                logger.info(
                    "audiodb.prewarm processed=%d total=%d hit_rate=%.0f%% bytes_ok=%d bytes_fail=%d remaining=%d",
                    processed, total, hit_rate, bytes_ok, bytes_fail, total - processed,
                )

            await asyncio.sleep(_AUDIODB_PREWARM_INTER_ITEM_DELAY)

        for album in needed_albums:
            if status_service.is_cancelled():
                logger.info("AudioDB pre-warming cancelled during album phase")
                break
            if not self._preferences_service.get_advanced_settings().audiodb_enabled:
                logger.info("AudioDB disabled during pre-warming, stopping")
                break

            mbid = getattr(album, 'musicbrainz_id', None) if hasattr(album, 'musicbrainz_id') else album.get('mbid') if isinstance(album, dict) else None
            artist_name = getattr(album, 'artist_name', None) if hasattr(album, 'artist_name') else album.get('artist_name') if isinstance(album, dict) else None
            album_name = getattr(album, 'title', None) if hasattr(album, 'title') else album.get('title') if isinstance(album, dict) else None
            processed += 1
            try:
                result = await svc.fetch_and_cache_album_images(
                    mbid, artist_name=artist_name, album_name=album_name, is_monitored=True,
                )
                if result and not result.is_negative and result.album_thumb_url:
                    if await self.download_bytes(result.album_thumb_url, "album", mbid):
                        bytes_ok += 1
                    else:
                        bytes_fail += 1
            except Exception as e:  # noqa: BLE001
                logger.warning("audiodb.prewarm action=album_error mbid=%s error=%s", mbid[:8] if mbid else '?', e)

            await status_service.update_progress(processed, f"AudioDB: {album_name or 'Unknown'}")

            if processed % _AUDIODB_PREWARM_LOG_INTERVAL == 0:
                logger.info(
                    "audiodb.prewarm processed=%d total=%d hit_rate=%.0f%% bytes_ok=%d bytes_fail=%d remaining=%d",
                    processed, total, hit_rate, bytes_ok, bytes_fail, total - processed,
                )

            await asyncio.sleep(_AUDIODB_PREWARM_INTER_ITEM_DELAY)

        logger.info(
            "audiodb.prewarm action=complete processed=%d total=%d bytes_ok=%d bytes_fail=%d",
            processed, total, bytes_ok, bytes_fail,
        )
