import asyncio
from dataclasses import dataclass
import hashlib
import time
from typing import Optional, List, Dict, Any
from urllib.parse import urljoin
import random

import httpx
from loguru import logger

from embykeeper.utils import get_proxy_str

logger = logger.bind(scheme="subsonic")


@dataclass
class ServerPingInfo:
    is_ok: bool = True
    type: str = None
    version: str = None
    server_version: str = None
    open_subsonic: bool = True
    error_code: int = None
    error_message: str = None


class Subsonic:
    """
    An async client for interacting with Subsonic API.
    """

    def __init__(
        self,
        server: str,
        username: str,
        password: str,
        proxy: Optional[Dict] = None,
        ua: Optional[str] = None,
        client: Optional[str] = None,
        version: Optional[str] = None,
    ):
        self.server = server.rstrip("/")
        self.username = username
        self.password = password
        self.proxy = proxy
        self.salt = self._generate_salt()
        self.token = self._generate_token()
        self.version = version or "1.15.0"
        self.client = client or "Stream Music"
        self.ua = ua or "Dart/3.4 (dart:io)"

        self._session = None
        self._session_lock = asyncio.Lock()

    def _generate_salt(self, length: int = 6) -> str:
        """Generate a random salt for authentication"""
        return hashlib.md5(str(time.time()).encode()).hexdigest()[:length]

    def _generate_token(self) -> str:
        """Generate authentication token using password and salt"""
        return hashlib.md5(f"{self.password}{self.salt}".encode()).hexdigest()

    async def _get_session(self) -> httpx.AsyncClient:
        """Get or create an HTTP session"""
        async with self._session_lock:
            if not self._session or self._session.is_closed:

                proxy = proxy = get_proxy_str(self.proxy)

                timeout = httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0)

                headers = {"User-Agent": self.ua}
                self._session = httpx.AsyncClient(
                    http2=True,
                    verify=False,
                    follow_redirects=True,
                    timeout=timeout,
                    proxy=proxy,
                    headers=headers,
                )
                logger.debug("Created new Subsonic session")
            return self._session

    async def _make_request(
        self, endpoint: str, params: Optional[Dict[str, Any]] = None, retries: int = 3
    ) -> Dict:
        """
        Make an async API request to Subsonic server
        """
        base_params = {
            "u": self.username,
            "t": self.token,
            "s": self.salt,
            "c": self.client,
            "v": self.version,
            "f": "json",
        }

        if params:
            base_params.update(params)

        url = urljoin(self.server, f"rest/{endpoint}")

        for i in range(retries):
            try:
                session = await self._get_session()
                response = await session.get(url, params=base_params)
                response.raise_for_status()
                return response.json()["subsonic-response"]
            except httpx.HTTPError as e:
                logger.debug(f'Connection to "{url}" failed, retrying: {e.__class__.__name__}: {e}')
                if i == retries - 1:
                    raise
                await asyncio.sleep(1 * (i + 1))

    async def ping(self):
        """Test connection to server"""
        try:
            response = await self._make_request("ping")
            if response.get("status", None) == "ok":
                return ServerPingInfo(
                    type=response.get("type", None),
                    version=response.get("version", None),
                    server_version=response.get("serverVersion", None),
                    open_subsonic=response.get("openSubsonic", None),
                )
            else:
                return ServerPingInfo(
                    is_ok=False,
                    error_code=response.get("error", {}).get("code", None),
                    error_message=response.get("error", {}).get("message", None),
                )
        except Exception as e:
            return ServerPingInfo(
                is_ok=False, error_message=f"Client error occurs: {e.__class__.__name__}: {e}"
            )

    async def get_music_folders(self) -> List[Dict]:
        """Get all music folders"""
        response = await self._make_request("getMusicFolders")
        return response.get("musicFolders", {}).get("musicFolder", [])

    async def get_artists(self, folder_id: Optional[str] = None) -> List[Dict]:
        """Get all artists in a music folder"""
        params = {"musicFolderId": folder_id} if folder_id else {}
        response = await self._make_request("getArtists", params)
        return response.get("artists", {}).get("index", [])

    async def get_artist(self, artist_id: str) -> Dict:
        """Get artist details"""
        response = await self._make_request("getArtist", {"id": artist_id})
        return response.get("artist", {})

    async def get_album(self, album_id: str) -> Dict:
        """Get album details"""
        response = await self._make_request("getAlbum", {"id": album_id})
        return response.get("album", {})

    async def get_song(self, song_id: str) -> Dict:
        """Get song details"""
        response = await self._make_request("getSong", {"id": song_id})
        return response.get("song", {})

    async def get_random_songs(
        self, size: int = 1, genre: Optional[str] = None, folder_id: Optional[str] = None
    ) -> List[Dict]:
        """Get random songs from the server"""
        params = {"size": size}
        if genre:
            params["genre"] = genre
        if folder_id:
            params["musicFolderId"] = folder_id

        response = await self._make_request("getRandomSongs", params)
        return response.get("randomSongs", {}).get("song", [])

    async def scrobble(self, song_id: str, submission: bool = True, time: Optional[int] = None) -> None:
        """
        Submit listening data to the server
        submission=True: The song was played fully
        submission=False: The song just started playing
        """
        params = {"id": song_id, "submission": "true" if submission else "false"}
        if time:
            params["time"] = time

        await self._make_request("scrobble", params)

    async def close(self):
        """Close the client and cleanup resources"""
        if self._session:
            await self._session.aclose()

    async def stream_noreturn(self, song_id: str) -> None:
        """Stream a song very slowly to simulate playback"""
        try:
            params = {
                "u": self.username,
                "t": self.token,
                "s": self.salt,
                "c": self.client,
                "v": self.version,
                "id": song_id,
            }
            url = urljoin(self.server, "rest/stream")

            session = await self._get_session()
            async with session.stream("GET", url, params=params) as resp:
                resp.raise_for_status()
                async for _ in resp.aiter_bytes(chunk_size=4096):
                    await asyncio.sleep(random.uniform(5, 10))
        except asyncio.CancelledError:
            pass
