import logging
import math
import time as _time
from collections.abc import Callable, Coroutine
from datetime import datetime, timezone
from typing import Any, Optional, TYPE_CHECKING

from api.v1.schemas.requests_page import (
    ActiveRequestItem,
    ActiveRequestsResponse,
    CancelRequestResponse,
    RequestHistoryItem,
    RequestHistoryResponse,
    RetryRequestResponse,
    StatusMessage,
)
from infrastructure.cover_urls import prefer_release_group_cover_url
from infrastructure.persistence.request_history import RequestHistoryRecord, RequestHistoryStore
from repositories.protocols import LidarrRepositoryProtocol
from services.request_utils import extract_cover_url, parse_eta, resolve_display_status

if TYPE_CHECKING:
    from infrastructure.queue.request_queue import RequestQueue

logger = logging.getLogger(__name__)

_CANCELLABLE_STATUSES = {"pending", "downloading"}
_RETRYABLE_STATUSES = {"failed", "cancelled", "incomplete"}
_CLEARABLE_STATUSES = {"imported", "incomplete", "failed", "cancelled"}

_QUEUE_CACHE_TTL = 10
_LIBRARY_MBIDS_CACHE_TTL = 30


class RequestsPageService:
    def __init__(
        self,
        lidarr_repo: LidarrRepositoryProtocol,
        request_history: RequestHistoryStore,
        library_mbids_fn: Callable[..., Coroutine[Any, Any, set[str]]],
        on_import_callback: Callable[[RequestHistoryRecord], Coroutine[Any, Any, None]] | None = None,
        request_queue: Optional["RequestQueue"] = None,
    ):
        self._lidarr_repo = lidarr_repo
        self._request_history = request_history
        self._library_mbids_fn = library_mbids_fn
        self._on_import_callback = on_import_callback
        self._request_queue = request_queue
        self._queue_cache: list[dict] | None = None
        self._queue_cache_time: float = 0
        self._library_mbids_cache: set[str] | None = None
        self._library_mbids_cache_time: float = 0

    async def get_active_requests(self) -> ActiveRequestsResponse:
        active_records = await self._request_history.async_get_active_requests()
        if not active_records:
            return ActiveRequestsResponse(items=[], count=0)

        queue_by_mbid = await self._load_queue_map(
            {r.musicbrainz_id for r in active_records}
        )
        library_mbids = await self._fetch_library_mbids()

        items: list[ActiveRequestItem] = []
        for record in active_records:
            queue_item = queue_by_mbid.get(record.musicbrainz_id)

            if queue_item:
                await self._sync_active_record(record, queue_item)
                items.append(self._build_active_item_from_queue(record, queue_item))
            else:
                completed = await self._check_if_completed(record, library_mbids)
                if completed:
                    continue
                items.append(self._build_pending_item(record))

        return ActiveRequestsResponse(items=items, count=len(items))

    async def get_request_history(
        self,
        page: int = 1,
        page_size: int = 20,
        status_filter: Optional[str] = None,
        sort: Optional[str] = None,
    ) -> RequestHistoryResponse:
        records, total = await self._request_history.async_get_history(
            page=page, page_size=page_size, status_filter=status_filter, sort=sort
        )

        library_mbids = await self._fetch_library_mbids()

        items = [
            RequestHistoryItem(
                musicbrainz_id=r.musicbrainz_id,
                artist_name=r.artist_name,
                album_title=r.album_title,
                artist_mbid=r.artist_mbid,
                year=r.year,
                cover_url=r.cover_url,
                requested_at=datetime.fromisoformat(r.requested_at),
                completed_at=(
                    datetime.fromisoformat(r.completed_at)
                    if r.completed_at
                    else None
                ),
                status=r.status,
                in_library=r.musicbrainz_id.lower() in library_mbids,
            )
            for r in records
        ]

        total_pages = max(1, math.ceil(total / page_size))

        return RequestHistoryResponse(
            items=items,
            total=total,
            page=page,
            page_size=page_size,
            total_pages=total_pages,
        )

    async def cancel_request(
        self, musicbrainz_id: str
    ) -> CancelRequestResponse:
        record = await self._request_history.async_get_record(musicbrainz_id)
        if not record:
            return CancelRequestResponse(
                success=False, message="Request not found"
            )

        if record.status not in _CANCELLABLE_STATUSES:
            return CancelRequestResponse(
                success=False,
                message=f"Cannot cancel request with status '{record.status}'",
            )

        # Cancel from local queue first
        queue_cancelled = False
        if self._request_queue:
            queue_cancelled = await self._request_queue.cancel(musicbrainz_id)

        try:
            queue_items = await self._get_cached_queue()
        except Exception as e:  # noqa: BLE001
            logger.error("Failed to fetch queue for cancel: %s", e)
            return CancelRequestResponse(
                success=False, message="Failed to reach Lidarr"
            )

        queue_id = None
        for item in queue_items:
            album_data = item.get("album", {})
            if album_data.get("foreignAlbumId", "").lower() == musicbrainz_id.lower():
                queue_id = item.get("id")
                break

        if queue_id:
            removed = await self._lidarr_repo.remove_queue_item(queue_id)
            if not removed:
                return CancelRequestResponse(
                    success=False, message="Couldn't remove the item from the download queue"
                )
            self._invalidate_queue_cache()
        elif not queue_cancelled:
            # Not in the local queue or Lidarr's download queue
            library_mbids = await self._fetch_library_mbids()
            if musicbrainz_id.lower() in library_mbids:
                return CancelRequestResponse(
                    success=False,
                    message="Album already imported, cannot cancel",
                )

        now_iso = datetime.now(timezone.utc).isoformat()
        await self._request_history.async_update_status(
            musicbrainz_id, "cancelled", completed_at=now_iso
        )

        return CancelRequestResponse(
            success=True,
            message=f"Cancelled download of {record.album_title}",
        )

    async def retry_request(
        self, musicbrainz_id: str
    ) -> RetryRequestResponse:
        record = await self._request_history.async_get_record(musicbrainz_id)
        if not record:
            return RetryRequestResponse(
                success=False, message="Request not found"
            )

        if record.status not in _RETRYABLE_STATUSES:
            return RetryRequestResponse(
                success=False,
                message=f"Cannot retry request with status '{record.status}'",
            )

        # If we have a Lidarr album ID, try a targeted search first
        if record.lidarr_album_id:
            result = await self._lidarr_repo.trigger_album_search(
                [record.lidarr_album_id]
            )
            if result:
                await self._request_history.async_update_status(musicbrainz_id, "pending")
                return RetryRequestResponse(
                    success=True,
                    message=f"Retrying search for {record.album_title}",
                )

        # Route through queue for dedup, per-artist locking, and history callbacks
        if self._request_queue:
            try:
                await self._request_history.async_update_status(musicbrainz_id, "pending")
                enqueued = await self._request_queue.enqueue(musicbrainz_id)
                if enqueued:
                    return RetryRequestResponse(
                        success=True,
                        message=f"Re-requested {record.album_title}",
                    )
                return RetryRequestResponse(
                    success=True,
                    message=f"Request already in queue for {record.album_title}",
                )
            except Exception as e:  # noqa: BLE001
                logger.error("Retry via queue failed for %s: %s", musicbrainz_id, e)
                return RetryRequestResponse(
                    success=False, message=f"Retry failed: {e}"
                )

        # Fallback: direct add_album (only if no queue available)
        try:
            add_result = await self._lidarr_repo.add_album(musicbrainz_id)
            payload = add_result.get("payload", {})
            if payload and isinstance(payload, dict):
                new_id = payload.get("id")
                if new_id:
                    await self._request_history.async_update_lidarr_album_id(
                        musicbrainz_id, new_id
                    )
            await self._request_history.async_update_status(musicbrainz_id, "pending")
            return RetryRequestResponse(
                success=True,
                message=f"Re-requested {record.album_title}",
            )
        except Exception as e:  # noqa: BLE001
            logger.error("Retry failed for %s: %s", musicbrainz_id, e)
            return RetryRequestResponse(
                success=False, message=f"Retry failed: {e}"
            )

    async def clear_history_item(self, musicbrainz_id: str) -> bool:
        record = await self._request_history.async_get_record(musicbrainz_id)
        if not record or record.status not in _CLEARABLE_STATUSES:
            return False
        return await self._request_history.async_delete_record(musicbrainz_id)

    async def get_active_count(self) -> int:
        return await self._request_history.async_get_active_count()

    async def sync_request_statuses(self) -> None:
        active_records = await self._request_history.async_get_active_requests()
        if not active_records:
            return

        try:
            queue_items = await self._get_cached_queue()
        except Exception as e:  # noqa: BLE001
            logger.warning("Status sync failed - cannot reach Lidarr: %s", e)
            return

        queue_mbids: set[str] = set()
        for item in queue_items:
            album_data = item.get("album", {})
            mbid = album_data.get("foreignAlbumId")
            if mbid:
                queue_mbids.add(mbid.lower())

        library_mbids = await self._fetch_library_mbids()

        for record in active_records:
            if record.musicbrainz_id.lower() in queue_mbids:
                if record.status != "downloading":
                    await self._request_history.async_update_status(
                        record.musicbrainz_id, "downloading"
                    )
            else:
                await self._check_if_completed(record, library_mbids)


    async def _fetch_library_mbids(self) -> set[str]:
        now = _time.monotonic()
        if self._library_mbids_cache is not None and (now - self._library_mbids_cache_time) < _LIBRARY_MBIDS_CACHE_TTL:
            return self._library_mbids_cache
        try:
            result = await self._library_mbids_fn()
            self._library_mbids_cache = result
            self._library_mbids_cache_time = now
            return result
        except Exception:  # noqa: BLE001
            if self._library_mbids_cache is not None:
                return self._library_mbids_cache
            return set()

    async def _get_cached_queue(self) -> list[dict]:
        now = _time.monotonic()
        if self._queue_cache is not None and (now - self._queue_cache_time) < _QUEUE_CACHE_TTL:
            return self._queue_cache
        try:
            queue_items = await self._lidarr_repo.get_queue_details()
            self._queue_cache = queue_items
            self._queue_cache_time = now
            return queue_items
        except Exception as e:  # noqa: BLE001
            logger.warning("Failed to fetch Lidarr queue: %s", e)
            return self._queue_cache or []

    def _invalidate_queue_cache(self) -> None:
        self._queue_cache = None
        self._queue_cache_time = 0

    async def _load_queue_map(
        self, active_mbids: set[str]
    ) -> dict[str, dict]:
        queue_items = await self._get_cached_queue()

        normalized_active = {m.lower() for m in active_mbids}
        queue_by_mbid: dict[str, dict] = {}
        for item in queue_items:
            album_data = item.get("album", {})
            mbid = album_data.get("foreignAlbumId")
            if mbid and mbid.lower() in normalized_active:
                queue_by_mbid[mbid] = item
        return queue_by_mbid

    async def _sync_active_record(
        self,
        record: RequestHistoryRecord,
        queue_item: dict,
    ) -> None:
        if record.status != "downloading":
            await self._request_history.async_update_status(
                record.musicbrainz_id, "downloading"
            )

        if not record.cover_url:
            album_data = queue_item.get("album", {})
            cover_url = extract_cover_url(album_data)
            if cover_url:
                await self._request_history.async_update_cover_url(
                    record.musicbrainz_id, cover_url
                )
                record.cover_url = cover_url

    @staticmethod
    def _build_active_item_from_queue(
        record: RequestHistoryRecord,
        queue_item: dict,
    ) -> ActiveRequestItem:
        album_data = queue_item.get("album", {})
        artist_data = album_data.get("artist", {}) or queue_item.get("artist", {})

        cover_url = prefer_release_group_cover_url(
            record.musicbrainz_id,
            record.cover_url or extract_cover_url(album_data),
            size=500,
        )
        artist_mbid = record.artist_mbid or artist_data.get("foreignArtistId")

        size = queue_item.get("size")
        sizeleft = queue_item.get("sizeleft")
        progress = (
            round((size - (sizeleft or 0)) / size * 100, 1)
            if size and size > 0
            else None
        )

        eta = parse_eta(queue_item.get("estimatedCompletionTime"))

        status_messages = [
            StatusMessage(
                title=msg.get("title"),
                messages=msg.get("messages") or [],
            )
            for msg in (queue_item.get("statusMessages") or [])
        ] or None

        download_state = queue_item.get("trackedDownloadState")
        display_status = resolve_display_status(download_state)

        quality_data = queue_item.get("quality", {})
        quality_name = None
        if isinstance(quality_data, dict):
            quality_obj = quality_data.get("quality", {})
            if isinstance(quality_obj, dict):
                quality_name = quality_obj.get("name")

        return ActiveRequestItem(
            musicbrainz_id=record.musicbrainz_id,
            artist_name=record.artist_name,
            album_title=record.album_title,
            artist_mbid=artist_mbid,
            year=record.year,
            cover_url=cover_url,
            requested_at=datetime.fromisoformat(record.requested_at),
            status=display_status,
            progress=progress,
            eta=eta,
            size=size,
            size_remaining=sizeleft,
            download_status=queue_item.get("trackedDownloadStatus"),
            download_state=download_state,
            status_messages=status_messages,
            error_message=queue_item.get("errorMessage"),
            lidarr_queue_id=queue_item.get("id"),
            quality=quality_name,
            protocol=queue_item.get("protocol"),
            download_client=queue_item.get("downloadClient"),
        )

    @staticmethod
    def _build_pending_item(record: RequestHistoryRecord) -> ActiveRequestItem:
        return ActiveRequestItem(
            musicbrainz_id=record.musicbrainz_id,
            artist_name=record.artist_name,
            album_title=record.album_title,
            artist_mbid=record.artist_mbid,
            year=record.year,
            cover_url=prefer_release_group_cover_url(
                record.musicbrainz_id,
                record.cover_url,
                size=500,
            ),
            requested_at=datetime.fromisoformat(record.requested_at),
            status=record.status,
            progress=None,
            eta=None,
            size=None,
            size_remaining=None,
            download_status=None,
            download_state=None,
            status_messages=None,
            lidarr_queue_id=None,
        )

    async def _check_if_completed(
        self,
        record: RequestHistoryRecord,
        library_mbids: set[str],
    ) -> bool:
        now_iso = datetime.now(timezone.utc).isoformat()

        if record.musicbrainz_id.lower() in library_mbids:
            await self._request_history.async_update_status(
                record.musicbrainz_id, "imported", completed_at=now_iso
            )
            await self._notify_import(record)
            return True

        if record.lidarr_album_id:
            try:
                history = await self._lidarr_repo.get_history_for_album(
                    record.lidarr_album_id
                )
                for event in history:
                    event_type = event.get("eventType", "")
                    if event_type in (
                        "downloadImported",
                        "trackFileImported",
                    ):
                        await self._request_history.async_update_status(
                            record.musicbrainz_id,
                            "imported",
                            completed_at=now_iso,
                        )
                        await self._notify_import(record)
                        return True
                    if event_type == "albumImportIncomplete":
                        await self._request_history.async_update_status(
                            record.musicbrainz_id,
                            "incomplete",
                            completed_at=now_iso,
                        )
                        return True
                    if event_type == "downloadFailed":
                        await self._request_history.async_update_status(
                            record.musicbrainz_id,
                            "failed",
                            completed_at=now_iso,
                        )
                        return True
            except Exception as e:  # noqa: BLE001
                logger.debug("Lidarr history check failed for %s: %s", record.musicbrainz_id, e)

        return False

    async def _notify_import(self, record: RequestHistoryRecord) -> None:
        self._library_mbids_cache = None
        self._library_mbids_cache_time = 0
        if self._on_import_callback:
            try:
                await self._on_import_callback(record)
            except Exception as e:  # noqa: BLE001
                logger.warning("Import callback failed for %s: %s", record.musicbrainz_id, e)
