import copy
import json
import re
from typing import Iterable, Mapping, TypeVar, Union, List
from urllib.parse import urlencode

from py_yt.core.constants import ResultMode, searchKey, requestPayload, playlistVideoKey, continuationKeyPath, \
    playlistPrimaryInfoKey, playlistSecondaryInfoKey, continuationItemKey
from py_yt.core.requests import RequestCore

K = TypeVar("K")
T = TypeVar("T")


class PlaylistCore(RequestCore):
    playlistComponent = None
    result = None
    continuationKey = None

    def __init__(self, playlist_link: str, componentMode: str, result_mode: int, timeout: int):
        super().__init__()
        self.componentMode = componentMode
        self.resultMode = result_mode
        self.timeout = timeout
        self.url = playlist_link

    def post_processing(self):
        self.__parseSource()
        self.__getComponents()
        if self.resultMode == ResultMode.json:
            self.result = json.dumps(self.playlistComponent, indent=4)
        else:
            self.result = self.playlistComponent

    async def async_create(self):
        statusCode = await self.__makeAsyncRequest()
        if statusCode == 200:
            self.post_processing()
        else:
            raise Exception('ERROR: Invalid status code.')

    def next_post_processing(self):
        self.__parseSource()
        self.__getNextComponents()
        if self.resultMode == ResultMode.json:
            self.result = json.dumps(self.playlistComponent, indent=4)
        else:
            self.result = self.playlistComponent

    async def _async_next(self):
        if self.continuationKey:
            self.prepare_next_request()
            statusCode = await self.asyncPostRequest()
            self.response = statusCode.text
            if statusCode.status_code == 200:
                self.next_post_processing()
            else:
                raise Exception('ERROR: Invalid status code.')
        else:
            await self.async_create()

    def prepare_first_request(self):
        self.url.strip('/')

        _id = re.search(r"(?<=list=)([a-zA-Z0-9+/=_-]+)", self.url).group()
        browseId = "VL" + _id if not _id.startswith("VL") else _id

        self.url = 'https://www.youtube.com/youtubei/v1/browse' + '?' + urlencode({
            'key': searchKey,
        })
        self.data = {
            "browseId": browseId,
        }
        self.data.update(copy.deepcopy(requestPayload))

    async def __makeAsyncRequest(self) -> int:
        self.prepare_first_request()
        request = await self.asyncPostRequest()
        self.response = request.text
        return request.status_code

    def prepare_next_request(self):
        requestBody = copy.deepcopy(requestPayload)
        requestBody['continuation'] = self.continuationKey
        self.data = requestBody
        self.url = 'https://www.youtube.com/youtubei/v1/browse' + '?' + urlencode({
            'key': searchKey,
        })

    def __parseSource(self) -> None:
        try:
            self.responseSource = json.loads(self.response)
        except:
            raise Exception('ERROR: Could not parse YouTube response.')

    def __getComponents(self) -> None:
        # print(self.responseSource)
        sidebar = self.responseSource["sidebar"]["playlistSidebarRenderer"]["items"]
        inforenderer = sidebar[0]["playlistSidebarPrimaryInfoRenderer"]
        channel_details_available = len(sidebar) != 1
        channelrenderer = sidebar[1]["playlistSidebarSecondaryInfoRenderer"]["videoOwner"][
            "videoOwnerRenderer"] if channel_details_available else None
        videorenderer: list = self.__getFirstValue(self.responseSource,
                                                   ["contents", "twoColumnBrowseResultsRenderer", "tabs", None,
                                                    "tabRenderer", "content", "sectionListRenderer", "contents", None,
                                                    "itemSectionRenderer", "contents", None,
                                                    "playlistVideoListRenderer", "contents"])
        videos = []
        for video in videorenderer:
            try:
                video = video["playlistVideoRenderer"]
                j = {
                    "id": self.__getValue(video, ["videoId"]),
                    "thumbnails": self.__getValue(video, ["thumbnail", "thumbnails"]),
                    "title": self.__getValue(video, ["title", "runs", 0, "text"]),
                    "channel": {
                        "name": self.__getValue(video, ["shortBylineText", "runs", 0, "text"]),
                        "id": self.__getValue(video,
                                              ["shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint",
                                               "browseId"]),
                        "link": self.__getValue(video,
                                                ["shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint",
                                                 "canonicalBaseUrl"]),
                    },
                    "duration": self.__getValue(video, ["lengthText", "simpleText"]),
                    "accessibility": {
                        "title": self.__getValue(video, ["title", "accessibility", "accessibilityData", "label"]),
                        "duration": self.__getValue(video,
                                                    ["lengthText", "accessibility", "accessibilityData", "label"]),
                    },
                    "link": "https://www.youtube.com" + self.__getValue(video, ["navigationEndpoint", "commandMetadata",
                                                                                "webCommandMetadata", "url"]),
                    "isPlayable": self.__getValue(video, ["isPlayable"]),
                }
                videos.append(j)
            except:
                pass

        playlistElement = {
            'info': {
                "id": self.__getValue(inforenderer,
                                      ["title", "runs", 0, "navigationEndpoint", "watchEndpoint", "playlistId"]),
                "thumbnails": self.__getValue(inforenderer,
                                              ["thumbnailRenderer", "playlistVideoThumbnailRenderer", "thumbnail",
                                               "thumbnails"]),
                "title": self.__getValue(inforenderer, ["title", "runs", 0, "text"]),
                "videoCount": self.__getValue(inforenderer, ["stats", 0, "runs", 0, "text"]),
                "viewCount": self.__getValue(inforenderer, ["stats", 1, "simpleText"]),
                "link": self.__getValue(self.responseSource,
                                        ["microformat", "microformatDataRenderer", "urlCanonical"]),
                "channel": {
                    "id": self.__getValue(channelrenderer, ["title", "runs", 0, "navigationEndpoint", "browseEndpoint",
                                                            "browseId"]) if channel_details_available else None,
                    "name": self.__getValue(channelrenderer,
                                            ["title", "runs", 0, "text"]) if channel_details_available else None,
                    "detailsAvailable": channel_details_available,
                    "link": "https://www.youtube.com" + self.__getValue(channelrenderer,
                                                                        ["title", "runs", 0, "navigationEndpoint",
                                                                         "browseEndpoint",
                                                                         "canonicalBaseUrl"]) if channel_details_available else None,
                    "thumbnails": self.__getValue(channelrenderer,
                                                  ["thumbnail", "thumbnails"]) if channel_details_available else None,
                }
            },
            'videos': videos,
        }
        if self.componentMode == "getInfo":
            self.playlistComponent = playlistElement["info"]
        elif self.componentMode == "getVideos":
            self.playlistComponent = {"videos": videos}
        else:
            self.playlistComponent = playlistElement
        self.continuationKey = self.__getValue(videorenderer, [-1, "continuationItemRenderer", "continuationEndpoint",
                                                               "continuationCommand", "token"])

    def __getNextComponents(self) -> None:
        self.continuationKey = None
        playlistComponent = {
            'videos': [],
        }
        continuationElements = self.__getValue(self.responseSource,
                                               ['onResponseReceivedActions', 0, 'appendContinuationItemsAction',
                                                'continuationItems'])
        if continuationElements is None:
            # YouTube Backend issue - See https://github.com/alexmercerind/youtube-search-python/issues/157
            return
        for videoElement in continuationElements:
            if playlistVideoKey in videoElement.keys():
                videoComponent = {
                    'id': self.__getValue(videoElement, [playlistVideoKey, 'videoId']),
                    'title': self.__getValue(videoElement, [playlistVideoKey, 'title', 'runs', 0, 'text']),
                    'thumbnails': self.__getValue(videoElement, [playlistVideoKey, 'thumbnail', 'thumbnails']),
                    'link': "https://www.youtube.com" + self.__getValue(videoElement,
                                                                        [playlistVideoKey, "navigationEndpoint",
                                                                         "commandMetadata", "webCommandMetadata",
                                                                         "url"]),
                    'channel': {
                        'name': self.__getValue(videoElement, [playlistVideoKey, 'shortBylineText', 'runs', 0, 'text']),
                        'id': self.__getValue(videoElement,
                                              [playlistVideoKey, 'shortBylineText', 'runs', 0, 'navigationEndpoint',
                                               'browseEndpoint', 'browseId']),
                        "link": "https://www.youtube.com" + self.__getValue(videoElement,
                                                                            [playlistVideoKey, "shortBylineText",
                                                                             "runs", 0, "navigationEndpoint",
                                                                             "browseEndpoint", "canonicalBaseUrl"])
                    },
                    'duration': self.__getValue(videoElement, [playlistVideoKey, 'lengthText', 'simpleText']),
                    'accessibility': {
                        'title': self.__getValue(videoElement,
                                                 [playlistVideoKey, 'title', 'accessibility', 'accessibilityData',
                                                  'label']),
                        'duration': self.__getValue(videoElement, [playlistVideoKey, 'lengthText', 'accessibility',
                                                                   'accessibilityData', 'label']),
                    },
                }
                playlistComponent['videos'].append(
                    videoComponent
                )
            self.continuationKey = self.__getValue(videoElement, continuationKeyPath)
        self.playlistComponent["videos"].extend(playlistComponent['videos'])

    def __getPlaylistComponent(self, element: dict, mode: str) -> dict:
        playlistComponent = {}
        if mode in ['getInfo', None]:
            for infoElement in element['info']:
                if playlistPrimaryInfoKey in infoElement.keys():
                    component = {
                        'id': self.__getValue(infoElement,
                                              [playlistPrimaryInfoKey, 'title', 'runs', 0, 'navigationEndpoint',
                                               'watchEndpoint', 'playlistId']),
                        'title': self.__getValue(infoElement, [playlistPrimaryInfoKey, 'title', 'runs', 0, 'text']),
                        'videoCount': self.__getValue(infoElement,
                                                      [playlistPrimaryInfoKey, 'stats', 0, 'runs', 0, 'text']),
                        'viewCount': self.__getValue(infoElement, [playlistPrimaryInfoKey, 'stats', 1, 'simpleText']),
                        'thumbnails': self.__getValue(infoElement, [playlistPrimaryInfoKey, 'thumbnailRenderer',
                                                                    'playlistVideoThumbnailRenderer', 'thumbnail']),
                    }
                    if not component['thumbnails']:
                        component['thumbnails'] = self.__getValue(infoElement,
                                                                  [playlistPrimaryInfoKey, 'thumbnailRenderer',
                                                                   'playlistCustomThumbnailRenderer', 'thumbnail',
                                                                   'thumbnails']),
                    component['link'] = 'https://www.youtube.com/playlist?list=' + component['id']
                    playlistComponent.update(component)
                if playlistSecondaryInfoKey in infoElement.keys():
                    component = {
                        'channel': {
                            'name': self.__getValue(infoElement,
                                                    [playlistSecondaryInfoKey, 'videoOwner', 'videoOwnerRenderer',
                                                     'title', 'runs', 0, 'text']),
                            'id': self.__getValue(infoElement,
                                                  [playlistSecondaryInfoKey, 'videoOwner', 'videoOwnerRenderer',
                                                   'title', 'runs', 0, 'navigationEndpoint', 'browseEndpoint',
                                                   'browseId']),
                            'thumbnails': self.__getValue(infoElement,
                                                          [playlistSecondaryInfoKey, 'videoOwner', 'videoOwnerRenderer',
                                                           'thumbnail', 'thumbnails']),
                        },
                    }
                    component['channel']['link'] = 'https://www.youtube.com/channel/' + component['channel']['id']
                    playlistComponent.update(component)
        if mode in ['getVideos', None]:
            self.continuationKey = None
            playlistComponent['videos'] = []
            for videoElement in element['videos']:
                if playlistVideoKey in videoElement.keys():
                    videoComponent = {
                        'id': self.__getValue(videoElement, [playlistVideoKey, 'videoId']),
                        'title': self.__getValue(videoElement, [playlistVideoKey, 'title', 'runs', 0, 'text']),
                        'thumbnails': self.__getValue(videoElement, [playlistVideoKey, 'thumbnail', 'thumbnails']),
                        'channel': {
                            'name': self.__getValue(videoElement,
                                                    [playlistVideoKey, 'shortBylineText', 'runs', 0, 'text']),
                            'id': self.__getValue(videoElement,
                                                  [playlistVideoKey, 'shortBylineText', 'runs', 0, 'navigationEndpoint',
                                                   'browseEndpoint', 'browseId']),
                        },
                        'duration': self.__getValue(videoElement, [playlistVideoKey, 'lengthText', 'simpleText']),
                        'accessibility': {
                            'title': self.__getValue(videoElement,
                                                     [playlistVideoKey, 'title', 'accessibility', 'accessibilityData',
                                                      'label']),
                            'duration': self.__getValue(videoElement, [playlistVideoKey, 'lengthText', 'accessibility',
                                                                       'accessibilityData', 'label']),
                        },
                    }
                    videoComponent['link'] = 'https://www.youtube.com/watch?v=' + videoComponent['id']
                    videoComponent['channel']['link'] = 'https://www.youtube.com/channel/' + videoComponent['channel'][
                        'id']
                    playlistComponent['videos'].append(
                        videoComponent
                    )
                if continuationItemKey in videoElement.keys():
                    self.continuationKey = self.__getValue(videoElement, continuationKeyPath)
        return playlistComponent

    def __result(self, mode: int) -> Union[dict, str]:
        if mode == ResultMode.dict:
            return self.playlistComponent
        elif mode == ResultMode.json:
            return json.dumps(self.playlistComponent, indent=4)

    def __getValue(self, source: dict, path: Iterable[str]) -> Union[str, int, dict, None]:
        value = source
        for key in path:
            if type(key) is str:
                if key in value.keys():
                    value = value[key]
                else:
                    value = None
                    break
            elif type(key) is int:
                if len(value) != 0:
                    value = value[key]
                else:
                    value = None
                    break
        return value

    def __getAllWithKey(self, source: Iterable[Mapping[K, T]], key: K) -> Iterable[T]:
        for item in source:
            if key in item:
                yield item[key]

    def __getValueEx(self, source: dict, path: List[str]) -> Iterable[Union[str, int, dict, None]]:
        if len(path) <= 0:
            yield source
            return
        key = path[0]
        upcoming = path[1:]
        if key is None:
            following_key = upcoming[0]
            upcoming = upcoming[1:]
            if following_key is None:
                raise Exception("Cannot search for a key twice consecutive or at the end with no key given")
            values = self.__getAllWithKey(source, following_key)
            for val in values:
                yield from self.__getValueEx(val, path=upcoming)
        else:
            val = self.__getValue(source, path=[key])
            yield from self.__getValueEx(val, path=upcoming)

    def __getFirstValue(self, source: dict, path: Iterable[str]) -> Union[str, int, dict, list, None]:
        values = self.__getValueEx(source, list(path))
        for val in values:
            if val is not None:
                return val
        return None
