import asyncio
from .constants import *
from .exceptions import *
from .util import *
from ..util import apply_tags_to_audio

from typing import Union
import httpx, logging, tempfile, io, aiofiles

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes



httpx_logger = logging.getLogger("httpx")
httpx_logger.setLevel(logging.ERROR)
class Deezer:
    def __init__(self, arl, debug) -> None:
        self.arl = arl
        self.cookies = {'arl': self.arl}
        self.debug = debug
            
        self.token = None
        self.user = {}
        self.__temp_cookies = {}

        asyncio.get_event_loop().run_until_complete(self.get_user_data())
        self.cookies = self.__temp_cookies

    ###
    ### API methods
    ###
    async def get_cookies(self):
        return self.cookies if self.cookies else None
    async def _api_call(self, method, params={}):
        token = self.token if self.token else 'null'
        if method != api_methods.GET_USER_DATA:
            token = self.token


        async with httpx.AsyncClient(http2=True,
            headers=networking_settings.HTTP_HEADERS, params={
            "api_version": "1.0",
            "api_token": token,
            "input": "3",
            "method": method
        }, cookies=await self.get_cookies()) as client:
            response = await client.post(api_urls.API_URL, json=params)
            data = response.json()
            self.__temp_cookies = response.cookies
                 




        if "error" in data and data["error"]:
            error_type = list(data["error"].keys())[0]
            error_message = data["error"][error_type]

            raise APIRequestError(
                "{0} : {1}".format(error_type, error_message))

        return data
    async def _legacy_search(self, method, query, limit=30, index=0):
        query = clean_query(query)

        data = await self._legacy_api_call(method, {
            "q": query,
            "limit": limit,
            "index": index
        })

        return data["data"]
    async def _legacy_api_call(self, method, params={}):
        url = "{0}/{1}".format(api_urls.LEGACY_API_URL, method)

        async with httpx.AsyncClient(headers=networking_settings.HTTP_HEADERS, params=params, cookies=await self.get_cookies()) as client:
            response = await client.get(url)
            data = response.json()

            
        if "error" in data and data["error"]:
            error_type = list(data["error"].keys())[0]
            error_message = data["error"][error_type]
            raise APIRequestError(
                "{0} : {1}".format(error_type, error_message))

        return data  
    async def get_user_data(self):
        """Gets the data of the user, this will only work arl is the cookie. Make sure you have run login_via_arl() before using this.

        Raises:
            LoginError: Will raise if the arl given is not identified by Deezer
        """

        data = await self._api_call(api_methods.GET_USER_DATA)


        data = data['results']
        self.token = data["checkForm"]

        if not data["USER"]["USER_ID"]:
            raise LoginError("Arl is invalid.")

        raw_user = data["USER"]


        if raw_user["USER_PICTURE"]:
            self.user = {
                "id": raw_user["USER_ID"],
                "name": raw_user["BLOG_NAME"],
                "arl": self.cookies["arl"],
                "image": "https://e-cdns-images.dzcdn.net/images/user/{0}/250x250-000000-80-0-0.jpg".format(raw_user["USER_PICTURE"])
            }
        else:
            self.user = {
                "id": raw_user["USER_ID"],
                "name": raw_user["BLOG_NAME"],
                "arl": self.cookies["arl"],
                "image": "https://e-cdns-images.dzcdn.net/images/user/250x250-000000-80-0-0.jpg"
            }


    """
        ALBUM
    """
    async def get_album(self, album_id):
        """Gets the album data of the given {album_id}

        Arguments:
            album_id {str} -- Album Id

        Returns:
            dict -- Album data
        """

        data = await self._legacy_api_call("/album/{0}".format(album_id))
        if data["cover_small"]:
            data["cover_id"] = str(data["cover_small"]).split(
                "cover/")[1].split("/")[0]
        else:
            data["cover_id"] = -1

        return data
    async def get_album_poster(self, album, size=500, ext="jpg"):
        """Gets the album poster as a binary data

        Arguments:
            album {dict} -- Album data

        Keyword Arguments:
            size {int} -- Size of the image, {size}x{size} (default: {500})
            ext {str} -- Extension of the image, can be ('.jpg' or '.png') (default: {"jpg"})

        Returns:
            bytes -- Binary data of the image
        """
        return await self._get_poster(album["cover_id"], size=size, ext=ext)
    async def _get_poster(self, poster_id, size=500, ext="jpg"):
        ext = ext.lower()
        if ext != "jpg" and ext != "png":
            raise ValueError("Image extension should only be jpg or png!")
        if poster_id == -1:
            url = "https://static.vecteezy.com/system/resources/thumbnails/022/059/000/small/no-image-available-icon-vector.jpg"
        else:
            url = f'https://e-cdns-images.dzcdn.net/images/cover/{poster_id}/{size}x{size}.{ext}'
        

        async with httpx.AsyncClient(headers=networking_settings.HTTP_HEADERS, cookies=await self.get_cookies()) as client:
            res = await client.get(url)
            image_bytes = res.read()

                
        return {
            "image": image_bytes,
            "size": (size, size),
            "ext": ext,
            "mime_type": "image/jpeg" if ext == "jpg" else "image/png"
        }
    async def get_album_tracks(self, album_id):
        """Gets the tracks of the given {album_id}

        Arguments:
            album_id {str} -- Album Id

        Returns:
            list -- List of tracks
        """

        data = await self._api_call(api_methods.ALBUM_TRACKS, params={
            "ALB_ID": album_id,
            "NB": -1
        })

        for i, track in enumerate(data["results"]["data"]):
            track["_POSITION"] = i + 1

        return data["results"]["data"]
    async def search_albums(self, query, limit=30, index=0):
        """Searches albums on a given query

        Arguments:
            query {str} -- Query keyword

        Keyword Arguments:
            limit {int} -- Number of results (default: {30})
            index {int} -- Offset (default: {0})

        Returns:
            list -- List of albums
        """

        return await self._legacy_search(api_methods.SEARCH_ALBUM, query, limit=limit, index=index)
    """
        ARTIST
    """
    async def get_artist(self, artist_id):
        """Gets the artist data from the given {artist_id}

        Arguments:
            artist_id {str} -- Artist Id

        Returns:
            dict -- Artist data
        """

        data = await self._api_call(api_methods.PAGE_ARTIST, params={
            "ART_ID": artist_id,
            "LANG": "en"
        })

        return data["results"]
    async def get_artist_poster(self, artist, size=500, ext="jpg"):
        """Gets the artist poster as a binary data

        Arguments:
            artist {dict} -- artist data

        Keyword Arguments:
            size {int} -- Size of the image, {size}x{size} (default: {500})
            ext {str} -- Extension of the image, can be ('.jpg' or '.png') (default: {"jpg"})

        Returns:
            bytes -- Binary data of the image
        """

        if not "ART_PICTURE" in artist and "DATA" in artist:
            artist = artist["DATA"]

        return await self._get_poster(artist["ART_PICTURE"], size=size, ext=ext)
    async def get_artist_discography(self, artist_id):
        """Gets the artist's discography (tracks)

        Arguments:
            artist_id {str} -- Artist Id

        Returns:
            dict -- Artist discography data
        """

        data = await self._api_call(api_methods.ARTIST_DISCOGRAPHY, params={
            "ART_ID": artist_id,
            "NB": 500,
            "NB_SONGS": -1,
            "START": 0
        })

        return data["results"]["data"]
    async def get_artist_top_tracks(self, artist_id):
        """Gets the top tracks of the given artist

        Arguments:
            artist_id {str} -- Artist Id

        Returns:
            list -- List of track
        """

        data = await self._api_call(api_methods.ARTIST_TOP_TRACKS, params={
            "ART_ID": artist_id,
            "NB": 100
        })

        for i, track in enumerate(data["results"]["data"]):
            track["_POSITION"] = i + 1

        return data["results"]["data"]
    async def search_artists(self, query, limit=30, index=0):
        """Searches artists on a given query

        Arguments:
            query {str} -- Query keyword

        Keyword Arguments:
            limit {int} -- Number of tracks (default: {30})
            index {int} -- Offset (default: {0})

        Returns:
            list -- List of artists
        """

        return await self._legacy_search(api_methods.SEARCH_ARTIST, query, limit=limit, index=index)
    """
        PLAYLIST
    """
    async def get_playlist(self, playlist_id):
        """Gets the playlist data from the given playlist_id

        Arguments:
            playlist_id {str} -- Playlist Id

        Returns:
            dict -- Playlist data
        """

        data = await self._api_call(api_methods.PAGE_PLAYLIST, params={
            "playlist_id": playlist_id,
            "LANG": "en"
        })

        return data["results"]
    async def get_playlist_tracks(self, playlist_id):
        """Gets the tracks inside the playlist

        Arguments:
            playlist_id {str} -- Playlist Id

        Returns:
            list -- List of tracks
        """

        data = await self._api_call(api_methods.PLAYLIST_TRACKS, params={
            "PLAYLIST_ID": playlist_id,
            "NB": -1
        })

        for i, track in enumerate(data["results"]["data"]):
            track["_POSITION"] = i + 1

        return data["results"]["data"]
    async def search_playlists(self, query, limit=30, index=0):
        """Searches playlists on a given query

        Arguments:
            query {str} -- Query keyword

        Keyword Arguments:
            limit {int} -- Number of tracks (default: {30})
            index {int} -- Offset (default: {0})

        Returns:
            list -- List of playlists
        """

        return await self._legacy_search(api_methods.SEARCH_PLAYLIST, query, limit=limit, index=index)

    """
        TRACKS
    """
    async def search_tracks(self, query, limit=30, index=0):
        """Searches tracks on a given query

        Arguments:
            query {str} -- Query keyword

        Keyword Arguments:
            limit {int} -- Number of results (default: {30})
            index {int} -- Offset (default: {0})

        Returns:
            list -- List of tracks
        """

        return await self._legacy_search(api_methods.SEARCH_TRACK, query, limit=limit, index=index)
    async def get_track_tags(self, track, separator=", ", with_cover:bool=True):
        """Gets the possible ID3 tags of the track.

        Arguments:
            track {dict} -- Track dictionary, similar to the {info} value that is returned {using get_track()}

        Keyword Arguments:
            separator {str} -- Separator to separate multiple artists (default: {", "})

        Returns:
            dict -- Tags
        """

        track = track["DATA"] if "DATA" in track else track

        album_data = await self.get_album(track["ALB_ID"])

        if "main_artist" in track["SNG_CONTRIBUTORS"]:
            main_artists = track["SNG_CONTRIBUTORS"]["main_artist"]
            artists = main_artists[0]
            for i in range(1, len(main_artists)):
                artists += separator + main_artists[i]
        else:
            artists = track["ART_NAME"]

        title = track["SNG_TITLE"]

        if "VERSION" in track and track["VERSION"] != "":
            title += " " + track["VERSION"]

        def should_include_featuring():
            feat_keywords = ["feat.", "featuring", "ft."]

            for keyword in feat_keywords:
                if keyword in title.lower():
                    return False
            return True

        if should_include_featuring() and "featuring" in track["SNG_CONTRIBUTORS"]:
            featuring_artists_data = track["SNG_CONTRIBUTORS"]["featuring"]
            featuring_artists = featuring_artists_data[0]
            for i in range(1, len(featuring_artists_data)):
                featuring_artists += separator + featuring_artists_data[i]

            title += f" (feat. {featuring_artists})"

        total_tracks = album_data["nb_tracks"]
        track_number = str(track["TRACK_NUMBER"]) + "/" + str(total_tracks)

        if with_cover:
            cover = await self.get_album_poster(album_data, size=1000)

        tags = {
            "title": title,
            "artist": artists,
            "genre": None,
            "album": track["ALB_TITLE"],
            "albumartist": track["ART_NAME"],
            "label": album_data["label"],
            "date": track["PHYSICAL_RELEASE_DATE"],
            "discnumber": track["DISK_NUMBER"],
            "tracknumber": track_number,
            "isrc": track["ISRC"],
            "copyright": track["COPYRIGHT"],
            "_albumart": cover if with_cover else None,
        }

        if len(album_data["genres"]["data"]) > 0:
            tags["genre"] = album_data["genres"]["data"][0]["name"]

        if "author" in track["SNG_CONTRIBUTORS"]:
            _authors = track["SNG_CONTRIBUTORS"]["author"]

            authors = _authors[0]
            for i in range(1, len(_authors)):
                authors += separator + _authors[i]

            tags["author"] = authors

        return tags
    async def get_track(self, track_id, with_cover:bool=True):
        """Gets the track info using the Deezer API

        Arguments:
            track_id {str} -- Track Id

        Returns:
            dict -- Dictionary that contains the {info}, {download} partial function, {tags}, and {get_tag} partial function.
        """

        method = api_methods.SONG_GET_DATA
        params = {
            "SNG_ID": track_id
        }

        if not int(track_id) < 0:
            method = api_methods.PAGE_TRACK

        data = await self._api_call(method, params=params)
        data = data["results"]

        return {
            "info": data,
            "tags": await self.get_track_tags(data, with_cover=with_cover),
        }
    async def get_track_download_url(self, track, quality=None, fallback=True, renew=False, **kwargs):
        """Gets and decrypts the download url of the given track in the given quality

        Arguments:
            track {dict} -- Track dictionary, similar to the {info} value that is returned {using get_track()}

        Keyword Arguments:
            quality {str} -- Use values from {track_formats}, will get the default quality if None or an invalid is given. (default: {None})
            fallback {bool} -- Set to True to if you want to use fallback qualities when the given quality is not available. (default: {False})
            renew {bool} -- Will renew the track object (default: {False})

        Raises:
            DownloadLinkDecryptionError: Will be raised if the track dictionary does not have an MD5
            ValueError: Will be raised if valid track argument was given

        Returns:
            str -- Download url
        """

        if renew:
            track = await self.get_track(track["SNG_ID"])
            track = track["info"]

        if not quality:
            quality = track_formats.MP3_128
            fallback = True

        try:
            track = track["DATA"] if "DATA" in track else track

            if not "MD5_ORIGIN" in track:
                raise DownloadLinkDecryptionError(
                    "MD5 is needed to decrypt the download link.")

            md5_origin = track["MD5_ORIGIN"]
            track_id = track["SNG_ID"]
            media_version = track["MEDIA_VERSION"]
        except ValueError:
            raise ValueError(
                "You have passed an invalid argument. This method needs the \"DATA\" value in the dictionary returned by the get_track() method.")

        def decrypt_url(quality_code):
            magic_char = "¤"
            step1 = magic_char.join((md5_origin,
                                     str(quality_code),
                                     track_id,
                                     media_version))
            m = hashlib.md5()
            m.update(bytes([ord(x) for x in step1]))

            step2 = m.hexdigest() + magic_char + step1 + magic_char
            step2 = step2.ljust(80, " ")

            cipher = Cipher(algorithms.AES(bytes('jo6aey6haid2Teih', 'ascii')),
                            modes.ECB(), default_backend())

            encryptor = cipher.encryptor()
            step3 = encryptor.update(bytes([ord(x) for x in step2])).hex()

            cdn = track["MD5_ORIGIN"][0]

            return f'https://e-cdns-proxy-{cdn}.dzcdn.net/mobile/1/{step3}'

        url = decrypt_url(track_formats.TRACK_FORMAT_MAP[quality]["code"])


        async with httpx.AsyncClient(headers=networking_settings.HTTP_HEADERS, cookies=await self.get_cookies()) as client:
            response = await client.get(url)
            if not fallback or (response.status_code == 200 and int(response.headers.get("Content-Length", 0)) > 0):
                return (url, quality)
            else:
                if "fallback_qualities" in kwargs:
                    fallback_qualities = kwargs["fallback_qualities"]
                else:
                    fallback_qualities = track_formats.FALLBACK_QUALITIES

                for key in fallback_qualities:
                    url = decrypt_url(
                        track_formats.TRACK_FORMAT_MAP[key]["code"])

                    async with httpx.AsyncClient(headers=networking_settings.HTTP_HEADERS, cookies=await self.get_cookies()) as client:
                        response = await client.get(url)
                        if not fallback or (response.status == 200 and int(response.headers.get("Content-Length", 0)) > 0):
                            return (url, key) 
    async def download_track(self, track, download_dir=None, quality=None, fallback=True, filename=None, 
                             renew=False,with_metadata=True, with_lyrics=True, 
                             tag_separator=", ", **kwargs):
        
        if with_lyrics:
            if "LYRICS" in track:
                lyric_data = track["LYRICS"]
            else:
                try:
                    if "DATA" in track:
                        _lyric_data = await self.get_track_lyrics(
                            track["DATA"]["SNG_ID"])
                        lyric_data = _lyric_data["info"]
                    else:
                        _lyric_data = await self.get_track_lyrics(
                            track["SNG_ID"])
                        lyric_data = _lyric_data["info"]
                except APIRequestError:
                    with_lyrics = False

        track = track["DATA"] if "DATA" in track else track
        tags = await self.get_track_tags(track, separator=tag_separator, with_cover=with_metadata)
        title = tags["title"]
        artist = tags['artist']
        if self.debug: logging.info(f'[DEEZER]: Download started [{artist} - {title}]')
        url, quality_key = await self.get_track_download_url(
            track, quality, fallback=fallback, renew=renew, **kwargs)
        blowfish_key = get_blowfish_key(track["SNG_ID"])

        quality = track_formats.TRACK_FORMAT_MAP[quality_key]
        ext = quality["ext"]

        if not filename:
            filename =  f"{artist} - {title}{ext}"
        if not str(filename).endswith(ext):
            filename += ext

        filename = clean_filename(filename)
        if download_dir:
            download_dir = path.normpath(download_dir)
            download_path = path.join(download_dir, filename)
            create_folders(download_dir)

        chunk_size = 2 * 1024
        async with httpx.AsyncClient(headers=networking_settings.HTTP_HEADERS, cookies=self.cookies) as client:
            res = await client.get(url, follow_redirects=True)
            data_iter = res.iter_bytes(chunk_size)

            i = 0

            if download_dir:
                async with aiofiles.open(download_path, 'wb') as f:
                    f.seek(0)
                
                    for chunk in data_iter:
                        try:
                            if i % 3 > 0:
                                await f.write(chunk)
                            elif len(chunk) < chunk_size:
                                await f.write(chunk)
                                break
                            else:
                                cipher = Cipher(algorithms.Blowfish(blowfish_key),
                                            modes.CBC(
                                                bytes([i for i in range(8)])),
                                            default_backend())

                                decryptor = cipher.decryptor()
                                dec_data = decryptor.update(
                                    chunk) + decryptor.finalize()

                                await f.write(dec_data)
                        except:
                            if self.debug: logging.error(f"[DEEZER] DL-ERROR: (bad chunk) - {type(chunk)}")
                        i += 1
            else:
                audio = io.BytesIO()
                audio.seek(0)
                for chunk in data_iter:
                    try:
                        if i % 3 > 0 or len(chunk) < chunk_size:
                            audio.write(chunk)
                        else:
                            cipher = Cipher(algorithms.Blowfish(blowfish_key),
                                            modes.CBC(bytes([i for i in range(8)])),
                                            default_backend())
                            decryptor = cipher.decryptor()
                            dec_data = decryptor.update(chunk) + decryptor.finalize()
                            audio.write(dec_data)

                    except Exception as e:
                        if self.debug: logging.error(f"[DEEZER] DL-ERROR: (bad chunk) - {type(chunk)}\nERROR: {e}")
                    
                    i += 1
                audio.seek(0)

        if with_metadata:
            if self.debug: logging.info(f'[DEEZER]: Tag editing [{artist} - {title}]')
            if ext.lower() == ".flac":
                pass #self._write_flac_tags(download_path, track, tags=tags)
            else:
                if download_dir:
                    await self._write_mp3_tags(audio=download_path, 
                                               track=track, tags=tags)
                else:
                    audio = await self._write_mp3_tags(audio=audio, 
                                               track=track, tags=tags, return_bytes=True)
                
        if with_lyrics:
            lyrics_path = tempfile.mkstemp(prefix=filename[:-len(ext)], 
                                           suffix='.lrc')
            lyric_check = await self.save_lyrics(lyric_data, lyrics_path[1])



        return {
            'track': download_path if download_dir else audio,
            'lyric': lyric_check[1] if with_lyrics and lyric_check[0] else None
        }


    """
        TAGS
    """
    async def _write_mp3_tags(self, audio:Union[str, io.BytesIO], track, tags=None):
        track = track["DATA"] if "DATA" in track else track

        if not tags:
            tags = await self.get_track_tags(track)

        _task = await asyncio.get_event_loop().run_in_executor(None, 
                                    lambda: apply_tags_to_audio(audio, tags))
        result = await _task
        return result

    """
        LYRIC
    """
    async def get_track_lyrics(self, track_id):
        """Gets the lyrics data of the given {track_id}

        Arguments:
            track_id {str} -- Track Id

        Returns:
            dict -- Dictionary that containts the {info}, and {save} partial function.
        """

        data = await self._api_call(api_methods.SONG_LYRICS, params={
            "SNG_ID": track_id
        })
        data = data["results"]

        return {
            "info": data
        }
    async def save_lyrics(self, lyric_data, save_path):
        """Saves the {lyric_data} into a .lrc file.

        Arguments:
            lyric_data {dict} -- The 'info' value returned from {get_track_lyrics()}
            save_path {str} -- Full path on where the file is to be saved

        Returns:
            bool -- Operation success
        """

        filename = path.basename(save_path)
        filename = clean_filename(filename)
        save_path = path.join(path.dirname(save_path), filename)

        if not str(save_path).endswith(".lrc"):
            save_path += ".lrc"

        create_folders(path.dirname(save_path))

        async with aiofiles.open(save_path, 'w', encoding="utf-8") as f:
            if not "LYRICS_SYNC_JSON" in lyric_data:
                return (False, save_path)

            sync_data = lyric_data["LYRICS_SYNC_JSON"]

            for line in sync_data:
                if str(line["line"]):
                    await f.write("{0}{1}".format(
                        line["lrc_timestamp"], line["line"]))
                await f.write("\n")

        return (True, save_path)
