from typing import Any, Dict, List, Optional, Union

import aiohttp

from .exceptions import (
    JioSaavnAPIError,
    JioSaavnConnectionError,
    JioSaavnNotFoundError,
    JioSaavnRateLimitError,
)
from .models import Album, Artist, Playlist, SearchResult, Track, RecommendationResult
from .utils.urls import extract_id, is_jiosaavn_url
from .utils.constants import (
    BASE_URL_DEFAULT,
    DEFAULT_PLAYLIST_LIMIT,
    DEFAULT_RECOMMENDATION_LIMIT,
    DEFAULT_TIMEOUT,
)


class AsyncJioSaavnClient:
    def __init__(
        self,
        base_url: str = BASE_URL_DEFAULT,
        timeout: int = DEFAULT_TIMEOUT,
        headers: Optional[Dict[str, str]] = None,
    ):
        self.base_url = base_url.rstrip("/")
        self.timeout = aiohttp.ClientTimeout(total=timeout)
        self._session: Optional[aiohttp.ClientSession] = None
        self._headers = {
            "User-Agent": "jiosaavn-python/1.0.0",
            "Accept": "application/json",
        }
        if headers:
            self._headers.update(headers)

    async def _get_session(self) -> aiohttp.ClientSession:
        if self._session is None or self._session.closed:
            self._session = aiohttp.ClientSession(
                headers=self._headers,
                timeout=self.timeout,
            )
        return self._session

    async def _request(self, method: str, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        url = f"{self.base_url}{path}"
        session = await self._get_session()
        try:
            async with session.request(method, url, params=params) as response:
                if response.status == 404:
                    raise JioSaavnNotFoundError()
                if response.status == 429:
                    raise JioSaavnRateLimitError()
                if response.status >= 400:
                    raise JioSaavnAPIError(response.status)
                data = await response.json()
                if isinstance(data, dict) and data.get("error"):
                    raise JioSaavnNotFoundError(str(data["error"]))
                return data
        except aiohttp.ClientConnectionError as e:
            raise JioSaavnConnectionError(str(e))
        except aiohttp.ClientError as e:
            raise JioSaavnConnectionError(str(e))

    async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        return await self._request("GET", path, params)

    async def search(self, query: str) -> SearchResult:
        data = await self._get("/search", {"q": query})
        return SearchResult.from_dict(data)

    async def get_track(self, identifier: str) -> Track:
        if is_jiosaavn_url(identifier):
            data = await self._get("/track", {"url": identifier})
        else:
            data = await self._get("/track", {"id": identifier})
        track_data = data.get("track", data)
        return Track.from_dict(track_data)

    async def get_track_by_id(self, track_id: str) -> Track:
        data = await self._get("/track", {"id": track_id})
        track_data = data.get("track", data)
        return Track.from_dict(track_data)

    async def get_track_by_url(self, url: str) -> Track:
        data = await self._get("/track", {"url": url})
        track_data = data.get("track", data)
        return Track.from_dict(track_data)

    async def get_album(self, identifier: str) -> Album:
        if is_jiosaavn_url(identifier):
            data = await self._get("/album", {"url": identifier})
        else:
            data = await self._get("/album", {"id": identifier})
        return Album.from_dict(data)

    async def get_album_by_id(self, album_id: str) -> Album:
        data = await self._get("/album", {"id": album_id})
        return Album.from_dict(data)

    async def get_album_by_url(self, url: str) -> Album:
        data = await self._get("/album", {"url": url})
        return Album.from_dict(data)

    async def get_artist(self, identifier: str) -> Artist:
        if is_jiosaavn_url(identifier):
            data = await self._get("/artist", {"url": identifier})
        else:
            data = await self._get("/artist", {"id": identifier})
        return Artist.from_dict(data)

    async def get_artist_by_id(self, artist_id: str) -> Artist:
        data = await self._get("/artist", {"id": artist_id})
        return Artist.from_dict(data)

    async def get_artist_by_url(self, url: str) -> Artist:
        data = await self._get("/artist", {"url": url})
        return Artist.from_dict(data)

    async def get_playlist(self, identifier: str, limit: int = DEFAULT_PLAYLIST_LIMIT) -> Playlist:
        if is_jiosaavn_url(identifier):
            data = await self._get("/playlist", {"url": identifier, "limit": limit})
        else:
            data = await self._get("/playlist", {"id": identifier, "limit": limit})
        return Playlist.from_dict(data)

    async def get_playlist_by_id(self, playlist_id: str, limit: int = DEFAULT_PLAYLIST_LIMIT) -> Playlist:
        data = await self._get("/playlist", {"id": playlist_id, "limit": limit})
        return Playlist.from_dict(data)

    async def get_playlist_by_url(self, url: str, limit: int = DEFAULT_PLAYLIST_LIMIT) -> Playlist:
        data = await self._get("/playlist", {"url": url, "limit": limit})
        return Playlist.from_dict(data)

    async def get_recommendations(self, track_id: str, limit: int = DEFAULT_RECOMMENDATION_LIMIT) -> RecommendationResult:
        data = await self._get("/recommendations", {"id": track_id, "limit": limit})
        return RecommendationResult.from_dict(data)

    async def get_encrypted_media_url(self, encrypted_url: str) -> Dict[str, Any]:
        data = await self._get("/encrypted-media-url", {"url": encrypted_url})
        return data

    async def close(self):
        if self._session and not self._session.closed:
            await self._session.close()

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.close()

    def __repr__(self):
        return f"AsyncJioSaavnClient(base_url={self.base_url!r})"
