from ssl import SSLError
from time import sleep
import logging

from requests import get
from requests.exceptions import (
    ConnectionError as RequestsConnectionError,
    ReadTimeout,
    ChunkedEncodingError,
)
from urllib3.exceptions import SSLError as u3SSLError

from deemix.utils.crypto import (
    _md5,
    _ecbCrypt,
    _ecbDecrypt,
    generateBlowfishKey,
    decryptChunk,
)

from deemix.utils import USER_AGENT_HEADER
from deemix.types.DownloadObjects import Single
from deemix.errors import DownloadCanceled, DownloadEmpty

logger = logging.getLogger("deemix")


def generateStreamPath(sng_id, md5, media_version, media_format):
    urlPart = b"\xa4".join(
        [
            md5.encode(),
            str(media_format).encode(),
            str(sng_id).encode(),
            str(media_version).encode(),
        ]
    )
    md5val = _md5(urlPart)
    step2 = md5val.encode() + b"\xa4" + urlPart + b"\xa4"
    step2 = step2 + (b"." * (16 - (len(step2) % 16)))
    urlPart = _ecbCrypt("jo6aey6haid2Teih", step2)
    return urlPart.decode("utf-8")


def reverseStreamPath(urlPart):
    step2 = _ecbDecrypt("jo6aey6haid2Teih", urlPart)
    (_, md5, media_format, sng_id, media_version, _) = step2.split(b"\xa4")
    return (
        sng_id.decode("utf-8"),
        md5.decode("utf-8"),
        media_version.decode("utf-8"),
        media_format.decode("utf-8"),
    )


def generateCryptedStreamURL(sng_id, md5, media_version, media_format):
    urlPart = generateStreamPath(sng_id, md5, media_version, media_format)
    return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/mobile/1/" + urlPart


def generateStreamURL(sng_id, md5, media_version, media_format):
    urlPart = generateStreamPath(sng_id, md5, media_version, media_format)
    return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/api/1/" + urlPart


def reverseStreamURL(url):
    urlPart = url[url.find("/1/") + 3 :]
    return reverseStreamPath(urlPart)


def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None):
    if downloadObject and downloadObject.isCanceled:
        raise DownloadCanceled
    headers = {"User-Agent": USER_AGENT_HEADER}
    chunkLength = start
    isCryptedStream = "/mobile/" in track.downloadURL or "/media/" in track.downloadURL

    itemData = {"id": track.id, "title": track.title, "artist": track.mainArtist.name}

    try:
        with get(
            track.downloadURL, headers=headers, stream=True, timeout=10
        ) as request:
            request.raise_for_status()
            if isCryptedStream:
                blowfish_key = generateBlowfishKey(str(track.id))

            complete = int(request.headers["Content-Length"])
            if complete == 0:
                raise DownloadEmpty
            if start != 0:
                responseRange = request.headers["Content-Range"]
                if listener:
                    listener.send(
                        "downloadInfo",
                        {
                            "uuid": downloadObject.uuid,
                            "data": itemData,
                            "state": "downloading",
                            "alreadyStarted": True,
                            "value": responseRange,
                        },
                    )
            else:
                if listener:
                    listener.send(
                        "downloadInfo",
                        {
                            "uuid": downloadObject.uuid,
                            "data": itemData,
                            "state": "downloading",
                            "alreadyStarted": False,
                            "value": complete,
                        },
                    )

            isStart = True
            for chunk in request.iter_content(2048 * 3):
                if isCryptedStream:
                    if len(chunk) >= 2048:
                        chunk = decryptChunk(blowfish_key, chunk[0:2048]) + chunk[2048:]

                if isStart and chunk[0] == 0 and chunk[4:8].decode("utf-8") != "ftyp":
                    for i, byte in enumerate(chunk):
                        if byte != 0:
                            break
                    chunk = chunk[i:]
                isStart = False

                outputStream.write(chunk)
                chunkLength += len(chunk)

                if downloadObject:
                    if isinstance(downloadObject, Single):
                        chunkProgres = (chunkLength / (complete + start)) * 100
                        downloadObject.progressNext = chunkProgres
                    else:
                        chunkProgres = (
                            (len(chunk) / (complete + start))
                            / downloadObject.size
                            * 100
                        )
                        downloadObject.progressNext += chunkProgres
                    downloadObject.updateProgress(listener)

    except (SSLError, u3SSLError):
        streamTrack(outputStream, track, chunkLength, downloadObject, listener)
    except (RequestsConnectionError, ReadTimeout, ChunkedEncodingError):
        sleep(2)
        streamTrack(outputStream, track, start, downloadObject, listener)
