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
