import os.path
import time

from netpyx import HttpClient
from requests import RequestException

from .Widevine import Widevine
from Core import Bootstrap, MpdParser, MediaAssembler, MkvMuxer


class Crunchium(Bootstrap):
    """
    Crunchium is the main class responsible for authenticating with the Crunchyroll API,
    retrieving metadata, handling playback and license acquisition (Widevine),
    downloading encrypted media segments, decrypting, and muxing them into MKV files.
    """

    def __init__(self):
        """
        Initializes the Crunchium class by calling the base Bootstrap initializer.
        """
        super().__init__()

    # ----------------------------- AUTHENTICATION -----------------------------

    def _request_token(self, grant_params: dict):
        """
        Requests an access token using the provided grant parameters.

        Args:
            grant_params (dict): Parameters for OAuth2 token request.
        """
        base_params = self.parameters
        base_params.update(grant_params)

        grant_type = grant_params.get("grant_type", "password")
        self.warning(f"Attempting to get access token using grant_type: {grant_type}...")

        try:
            with HttpClient() as client:
                client.set_auth((self.get_item("client_id"), self.get_item("client_secret")))
                response: dict = client.post(
                    f"{self.API_URL}/auth/v1/token",
                    base_params
                ).json()

                self.set_item("access_token", response.get("access_token"))
                self.set_item("refresh_token", response.get("refresh_token"))
                self.set_item("expires_in", self.get_expiry(response.get("expires_in", 0)))
                self.reload_store()
                self.success("Access token obtained successfully.")
                self.set_auth_bearer(self.access_token)
        except RequestException as e:
            self.error(self.show_exception(e))
            exit(1)

    def get_access_token(self):
        """
        Initiates login and token retrieval using email and password credentials.
        """
        if not all([self.email, self.password]):
            self.error("Email and password are required.")
            exit(1)

        grant_params = {
            "grant_type": "password",
            "username": self.email,
            "password": self.password
        }

        try:
            self._request_token(grant_params)
        except RequestException as e:
            self.error(self.show_exception(e))

    def refresh_access_token(self):
        """
        Refreshes the access token using the stored refresh token.
        """
        grant_params = {
            "grant_type": "refresh_token",
            "refresh_token": self.refresh_token
        }

        try:
            self.critical("Access token expired. Refreshing...")
            self._request_token(grant_params)
        except RequestException as e:
            self.error(self.show_exception(e))

    def _token_required(self):
        """
        Validates token expiry and refreshes if needed. Always sets the bearer token.
        """
        if int(time.time()) > self.get_item("expires_in", 0):
            self.refresh_access_token()
        else:
            self.set_auth_bearer(self.access_token)

    # ----------------------------- CONTENT INFORMATION -----------------------------

    def get_media_info(self, media_id: str):
        """
        Fetches metadata for a given media ID (movie or episode).

        Args:
            media_id (str): The media identifier.

        Returns:
            dict: The metadata for the media item.
        """
        self._token_required()
        self.info(f"Getting media info for media_id: {media_id}...")
        try:
            response = self.get(
                f"{self.API_URL}/content/v2/cms/objects/{media_id}",
                {
                    "locale": self.default_language,
                    "preferred_audio_language": self.default_language
                }
            ).json()
            return response.get("data")[0]
        except RequestException as e:
            self.error(self.show_exception(e))

    def get_seasons(self, serie: dict) -> list[dict] | None:
        """
        Retrieves all seasons for a given series.

        Args:
            serie (dict): A dictionary containing series metadata.

        Returns:
            list[dict] | None: List of season objects.
        """
        self._token_required()
        try:
            response: dict = self.get(
                f"{self.API_URL}/content/v2/cms/series/{serie.get('id')}/seasons",
                self.parameters
            ).json()
            return response.get("data", [])
        except RequestException as e:
            self.error(self.show_exception(e))

    def get_episodes(self, season_id: str) -> list[dict] | None:
        """
        Retrieves all episodes from a specific season.

        Args:
            season_id (str): The season identifier.

        Returns:
            list[dict] | None: List of episodes.
        """
        self._token_required()
        try:
            response: dict = self.get(
                f"{self.API_URL}/content/v2/cms/seasons/{season_id}/episodes",
                self.parameters
            ).json()
            return response.get("data", [])
        except RequestException as e:
            self.error(self.show_exception(e))

    # ----------------------------- MOVIE DOWNLOAD -----------------------------

    def download_movie(self, movie: dict):
        """
        Downloads all available versions of a given movie.

        Args:
            movie (dict): Movie metadata.
        """
        versions = []
        try:
            movie_listing_id = movie.get("movie_metadata", {}).get("movie_listing_id")
            response = self.get(
                f"{self.API_URL}/content/v2/cms/movie_listings/{movie_listing_id}/movies",
                self.parameters
            ).json()
            versions = response.get("data", [])
        except RequestException as e:
            self.error(self.show_exception(e))
        finally:
            if not versions:
                self.error("No available versions for the movie.")
                return

            self.info(f"Found {len(versions)} versions.")

            for version in versions:
                guid = version.get("id")
                title = self.compose_movie_title(movie, guid)

                if os.path.exists(os.path.join(self.get_item("download_dir"), "movie", f"{title}.mkv")):
                    self.warning(f"Skipping already downloaded version: {title}.mkv")
                    continue

                media_dir = self._download_version(guid, movie.get("id"))
                if media_dir:
                    with MkvMuxer(title, media_dir) as muxer:
                        muxer.call()

    # ----------------------------- EPISODE DOWNLOAD -----------------------------

    def download_episode(self, episode: dict):
        """
        Downloads an episode and its best-matching version.

        Args:
            episode (dict): Episode metadata.
        """
        #print(episode)
        self._token_required()
        self.warning(f"Found episode {episode.get('episode_number')}: {episode.get('title')}, Id: {episode.get('id')}")
        versions = []

        try:
            versions = [v for v in episode.get('versions', []) if
                        v["audio_locale"] in self.languages or v.get("original") is True]
        except Exception:
            versions.append({
                "guid": episode.get('id'),
                "original": True,
                "audio_locale": episode.get("audio_locale")
            })

        if not versions:
            self.error("No available versions for this episode.")
            return

        title = self.compose_episode_title(episode)

        if os.path.exists(os.path.join(self.get_item("download_dir"), "serie", f"{title}.mkv")):
            self.success(f"Skipping already downloaded episode version: {os.path.basename(title)}.mkv")
            return

        temp_dir = None
        hay_default_language = any(version['audio_locale'] == self.default_language for version in versions)

        for version in versions:
            guid = version.get("guid")
            with_video = version.get("original")
            if with_video and hay_default_language:
                with_video = False

            if version.get("audio_locale") == self.default_language:
                with_video = True

            media_dir = self._download_version(guid, episode.get("id"), with_video)
            if temp_dir is None:
                temp_dir = media_dir

        if temp_dir:
            with MkvMuxer(title, temp_dir, "serie") as muxer:
                muxer.call()

    # ----------------------------- MEDIA UTILS -----------------------------

    def _get_best_quality_video(self, video_qualities: dict) -> dict | None:
        """
        Selects the best video quality from a dictionary.

        Args:
            video_qualities (dict): Dictionary with resolutions as keys.

        Returns:
            dict | None: Best quality video stream.
        """
        if not video_qualities:
            return None
        best_quality = str(max(map(int, video_qualities)))
        return video_qualities.get(best_quality)

    def close_streams(self):
        self._token_required()
        response = self.get(f"{self.PLAY_URL}/v1/sessions/streaming").json()
        streams = response.get("items", [])
        if len(streams) > 1:
            for stream in streams:
                self._delete_playback_token(stream.get("contentId"), stream.get("token"))


    def _download_version(self, guid: str, ref_id: str, with_video: bool):
        """
        Downloads a specific media version and returns the directory path.

        Args:
            guid (str): Unique content ID.
            ref_id (str): Reference ID (episode or movie).

        Returns:
            str | None: Path to the downloaded and decrypted media directory.
        """
        mpd = None
        token = None
        try:
            playback: dict = self.get(
                f"{self.WEB_URL}/playback/v3/{guid}/web/chrome/play",
                {"queue": 0}
            ).json()

            token = playback.get("token")
            mpd = self.get_mpd_content(playback, guid)
        except RequestException as e:
            error = self.show_exception(e)
            if "TOO_MANY_ACTIVE_STREAMS" in error:
                self.error("Too many active streams. Trying to close some streams.")
                self.close_streams()
                self._download_version(guid, ref_id)

            self.error(error)
            return

        if mpd is None:
            self.error("No valid playback found for this version.")
            self._delete_playback_token(guid, token)
            return

        video = self._get_best_quality_video(mpd.get("video", {}))
        if video is None:
            self.error("No valid video stream found.")
            self._delete_playback_token(guid, token)
            return

        keys = self.get_widevine_keys(guid, token, video.get("pssh"))
        if not keys:
            self.error("No Widevine keys available.")
            self._delete_playback_token(guid, token)
            return None

        assembler = MediaAssembler(video.get("segments"), mpd.get("audio", {}), playback.get("subtitles"), ref_id)
        video_decrypted, audio_decrypted, media_dir = assembler.call(keys, with_video)

        self._delete_playback_token(guid, token)
        return media_dir

    def get_mpd_content(self, playback: dict, guid: str) -> dict | None:
        """
        Parses and returns the MPD manifest for a media item.

        Args:
            playback (dict): Playback object with MPD URL.
            guid (str): Media ID.

        Returns:
            dict | None: Parsed video/audio/subtitle info.
        """
        self._token_required()
        with MpdParser(playback.get("url")) as mpd:
            try:
                mpd.set_auth_bearer(self.access_token)
                return mpd.parse(playback.get("audioLocale", None), self.access_token)
            except RequestException as e:
                self.error(self.show_exception(e))
                self._delete_playback_token(guid, playback.get("token"))

    def get_widevine_keys(self, guid: str, token: str, pssh: str):
        """
        Fetches Widevine keys for a given stream.

        Args:
            guid (str): Media identifier.
            token (str): Playback token.
            pssh (str): PSSH license request data.

        Returns:
            list[dict] | None: Decryption keys if available.
        """
        wk = None
        with Widevine(f"{self.WEB_URL}/license/v1/license/widevine", pssh) as widevine:
            try:
                widevine.set_user_agent(self.user_agent)
                widevine.start_session()
                self._token_required()
                widevine.set_headers(self.get_headers_widevine(guid, token))
                response = widevine.get_response_server().json()
                lic = response.get('license')
                if not lic:
                    self.error("No Widevine license found.")
                    return None
                wk = widevine.get_keys(lic)
            except RequestException as e:
                self.error(self.show_exception(e))
            finally:
                widevine.close_session()
                return wk

    def _delete_playback_token(self, guid: str, token: str):
        """
        Invalidates and deletes a playback token.

        Args:
            guid (str): Media identifier.
            token (str): Playback token.
        """
        self._token_required()
        self.exception(f"Deleting playback token for guid: {guid}...")
        self.delete(f"{self.PLAY_URL}/v1/token/{guid}/{token}")
