from youtube_autonomous.segments.builder.youtube.enums import YoutubeChannelId
from yta_youtube.video import YoutubeVideo
from yta_youtube.enums import VideoFormatQuality, YoutubeVideoLanguage
from yta_youtube.api import YoutubeAPI
from yta_general_utils.programming.output import Output
from yta_general_utils.file.enums import FileTypeX
from yta_general_utils.programming.parameter_validator import PythonValidator
from yta_general_utils.programming.decorators import singleton
from random import randint
from typing import Union

import requests


@singleton
class YoutubeDownloader:
    """
    Singleton class.

    This object simplifies the access to our Youtube
    provider channels. It uses the Google Youtube Data
    V3 API and, if not available, uses a community API
    to obtain the videos.

    This object is used to download memes, stock videos,
    sounds, music, etc. from our specific Youtube channels.
    """
    
    # TODO: Set '_ids_to_ignore' type
    _ids_to_ignore: list[any]

    def __init__(self, ignore_repeated = True):
        # TODO: Maybe implement the other 'Singleton' pattern method, I don't
        # know if this one is actually working
        if not hasattr(self, '_ids_to_ignore'):
            if not YoutubeAPI.is_youtube_token_valid():
                print('Youtube token is not valid. Please, login to get a new valid token.')
                YoutubeAPI.start_youtube_auth_flow()

            self.service = YoutubeAPI.create_youtube_service()
            # This is to avoid using the same video again in the whole video. This can be
            # modified to avoid repeated videos only in each segment
            self._ids_to_ignore = []
            self.ignore_repeated = ignore_repeated

    def activate_ignore_repeated(self):
        """
        Sets the 'ignore_repeated' flag as True. This
        flag checks that a video that has been returned
        once is not returned again. This method will
        activate it, so any video can be returned only
        once.
        """
        self.ignore_repeated = True

    def deactivate_ignore_repeated(self):
        """
        Sets the 'ignore_repeated' flag as False. This
        flag checks that a video that has been returned
        once is not returned again. This method will
        deactivate it, so any video can be returned
        unlimited times if found.
        """
        self.ignore_repeated = False

    # TODO: Set 'ignore_id' type
    def add_ignored_id(self, ignore_id: any):
        # TODO: Maybe do any more check (?)
        if ignore_id not in self._ids_to_ignore:
            self._ids_to_ignore.append(ignore_id)

    def __search(
        self,
        keywords: str,
        max_results: int = 25,
        channel_id: YoutubeChannelId = None
    ):
        """
        Searchs videos in the provided 'channel_id' channel, using the 'keywords'
        keywords, and returning 'max_results' results as maximum.
        """
        if not PythonValidator.is_string(keywords):
            raise Exception('The "keywords" parameter provided is not a valid string.')
        
        channel_id = YoutubeChannelId.to_enum(channel_id)
        max_results = 25 if max_results is None else max_results

        try:
            response_videos_list = self.service.search().list(
                part = 'snippet',
                channelId = channel_id.value,
                maxResults = max_results,
                order = 'relevance',  # This is the most interesting by far, using the youtube search engine
                type = 'video',
                q = keywords
            ).execute()
        except Exception as e:
            print(e)
            # We try the collaborative known alternative that should work
            no_key_url = f'https://yt.lemnoslife.com/noKey/search?part=snippet&channelId={channel_id.value}&maxResults={str(max_results)}&order=relevance&type=video&q={keywords}&alt=json'

            try:
                response_videos_list = requests.get(no_key_url).json()
            except Exception as e:
                print(e)
                return []

        return (
            response_videos_list['items']
            if response_videos_list['pageInfo']['totalResults'] > 0 else
            []
        )
    
    def get_video(
        self,
        id_or_url: str
    ) -> YoutubeVideo:
        """
        Returns the Youtube video with the given 
        'id_or_url' id or url, provided as a
        YoutubeVideo instance.
        """
        if not PythonValidator.is_string(id_or_url):
            raise Exception('The "url" parameter provided is not a valid string.')
        
        return YoutubeVideo(id_or_url)

    # TODO: Maybe the '_get_videos' (?)
    
    def _get_video_from_keywords(
        self,
        keywords: str,
        channel_id: YoutubeChannelId,
        do_randomize: bool = False
    ) -> YoutubeVideo:
        """
        Looks for videos in the channel with the given 
        'channel_id' according to the also provided 'keywords'.
        This method will return a video, if found, or None if
        not. The 'do_randomize' parameter will return a random
        video if True and more than one video is found.

        This method will ignore any repeated video if
        'ignore_repeated' is True in this class instance.
        """
        if not PythonValidator.is_string(keywords):
            raise Exception('The provided "keywords" parameter is not a valid string.')

        channel_id = YoutubeChannelId.to_enum(channel_id)

        if not PythonValidator.is_boolean(do_randomize):
            raise Exception('The provided "do_randomize" parameter is not a boolean.')

        youtube_videos = self.__search(keywords, 25, channel_id)

        # We look for repeated videos to eliminate from the ones found
        if self.ignore_repeated and len(self._ids_to_ignore) > 0:
            index = 0
            while index < len(youtube_videos):
                video = youtube_videos[index]
                if video['id']['videoId'] in self._ids_to_ignore:
                    del youtube_videos[index]
                    index -= 1
                index += 1

        return (
            YoutubeVideo(youtube_videos[0]['id']['videoId'])
            if len(youtube_videos) > 0 else
            None
        )

    def _download_video(
        self,
        keywords: str,
        channel_id: YoutubeChannelId,
        do_include_audio: bool = False,
        do_randomize: bool = False,
        output_filename: Union[str, None] = None
    ) -> Union[None, str]:
        """
        Looks for the videos in the channel with provided
        'channel_id' id and that match the also provided
        'keywords'. This method will download, if any video
        is found, the first one (or a random one if
        'do_randomize' is True).

        This method will return None if no video found nor
        downloaded.

        This method forces the download to FULL HD quality
        (1920x1080).

        This method returns the locally stored
        'output_filename' (that could change due to video
        extension), or None if something went wrong.
        """
        if not PythonValidator.is_string(keywords):
            raise Exception('The provided "keywords" parameter is not a valid string.')

        channel_id = YoutubeChannelId.to_enum(channel_id)

        if not PythonValidator.is_boolean(do_include_audio):
            raise Exception('The provided "do_include_audio" parameter is not a boolean.')

        if not PythonValidator.is_boolean(do_randomize):
            raise Exception('The provided "do_randomize" parameter is not a boolean.')
        
        youtube_video = self._get_video_from_keywords(keywords, channel_id, do_randomize)

        if not youtube_video:
            return None

        download_method = (
            lambda video_quality, output_filename: youtube_video.download_with_best_quality(
                video_quality = video_quality,
                output_filename = output_filename
            )
            if do_include_audio else
            lambda video_quality, output_filename: youtube_video.download_video_with_best_quality(
                video_quality = video_quality,
                output_filename = output_filename
            )
        )

        output_filename = download_method(
            VideoFormatQuality.FULL_HD,
            Output.get_filename(output_filename, FileTypeX.VIDEO)
        )

        if self.ignore_repeated:
            self.add_ignored_id(youtube_video.id)

        return output_filename
    
    def download_this_video(
        self,
        youtube_video: YoutubeVideo,
        output_filename: Union[str, None] = None,
        do_include_audio: bool = True,
        language: YoutubeVideoLanguage = YoutubeVideoLanguage.DEFAULT
    ) -> str:
        """
        Downloads the provided 'youtube_video' and stores it
        locally as 'output_filename' if this parameter is
        provided, or in a temporary file (that is returned by
        this method) if not. The 'do_include_audio' parameter
        allows the user to include the audio or not. If
        'do_include_audio' is True, the 'language' parameter
        is needed to decide the audio
        language.
        """
        if not PythonValidator.is_instance(youtube_video, YoutubeVideo):
            raise Exception('The "youtube_video" parameter provided is not a valid YoutubeVideo instance.')
        
        language = YoutubeVideoLanguage.to_enum(language)
        output_filename = Output.get_filename(output_filename, FileTypeX.VIDEO)

        if do_include_audio:
            output_filename = youtube_video.download_with_best_quality(
                language = language,
                video_quality = VideoFormatQuality.FULL_HD,
                output_filename = output_filename
            )
        else:
            output_filename = youtube_video.download_video_with_best_quality(
                video_quality = VideoFormatQuality.FULL_HD,
                output_filename = output_filename
            )

        return output_filename
    
    def download_video(
        self,
        url: str,
        output_filename: Union[str, None] = None,
        do_include_sound: bool = True,
        language: YoutubeVideoLanguage = YoutubeVideoLanguage.DEFAULT
    ) -> str:
        """
        Downloads the Youtube video from the provided 'url' and stores it locally
        as 'output_filename' if this parameter is provided, or in a temporary file
        (that is returned by this method) if not. The 'do_include_sound' parameter
        allows the user to include the audio or not. If 'do_include_sound' is True,
        the 'language' parameter is needed to decide the audio language.
        """
        if not PythonValidator.is_string(url):
            raise Exception('The "url" parameter provided is not a valid string.')
        
        language = YoutubeVideoLanguage.to_enum(language)
        output_filename = Output.get_filename(output_filename, FileTypeX.VIDEO)
        video = YoutubeVideo(url)
        
        return self.download_this_video(
            video,
            output_filename,
            do_include_sound,
            language
        )
    
    def _download_audio(
        self,
        keywords: str,
        channel_id: YoutubeChannelId,
        do_randomize: bool = False,
        output_filename: Union[str, None] = None):
        """
        Looks for the videos in the channel with provided 'channel_id' id and that match
        the also provided 'keywords'. This method will download, if any video is found, 
        the first one (or a random one if 'do_randomize' is True).

        This method will return None if no video found nor downloaded.

        This method forces the download to DEFAULT LANGUAGE (the first one in the list).

        This method returns the locally stored 'output_filename' (that could change due to video
        extension), or None if something went wrong.
        """
        youtube_video = self._get_video_from_keywords(keywords, channel_id.value, do_randomize)

        if not youtube_video:
            return None
        
        if self.ignore_repeated:
            self.add_ignored_id(youtube_video.id)

        return youtube_video.download_audio(
            language = YoutubeVideoLanguage.DEFAULT,
            output_filename = Output.get_filename(output_filename, FileTypeX.AUDIO)
        )

    def download_audio(
        self,
        url: str,
        output_filename: str = None,
        language: YoutubeVideoLanguage = YoutubeVideoLanguage.DEFAULT
    ):
        """
        Downloads the Youtube video audio from the provided 'url' and stores it 
        locally as 'output_filename' if this parameter is provided, or in a 
        temporary file (that is returned by this method) if not. The 'language' 
        parameter is needed to decide the audio language.
        """
        if not PythonValidator.is_string(url):
            raise Exception('The "url" parameter provided is not a valid string.')
        
        language = YoutubeVideoLanguage.to_enum(language)
        video = YoutubeVideo(url)

        return video.download_audio(
            language = language,
            output_filename = Output.get_filename(output_filename, FileTypeX.AUDIO)
        )

    def download_meme_video(
        self,
        keywords: str,
        do_include_audio: bool = True,
        do_randomize: bool = False,
        output_filename: Union[str, None] = None
    ):
        """
        Downloads a meme from the specific Youtube channel configured as Meme channel. It will
        include the audio if 'do_include_audio' is True, and will return the first result (based 
        on Youtube search relevance) or a random one if 'do_randomize' is True. It will be 
        stored locally as 'output_filename' if found.

        This method returns the 'output_filename' of the locally stored video, or None if not
        downloaded.
        """
        return self._download_video(
            keywords = keywords,
            channel_id = YoutubeChannelId.MEMES,
            do_include_audio = do_include_audio,
            do_randomize = do_randomize,
            output_filename = Output.get_filename(output_filename, FileTypeX.VIDEO)
        )

    def download_stock_video(
        self,
        keywords: str,
        do_include_audio: bool = True,
        do_randomize: bool = False,
        output_filename: Union[str, None] = None
    ):
        """
        Downloads a stock video from the specific Youtube channel configured as Stock videos 
        channel. It will include the audio if 'do_include_audio' is True, and will return the 
        first result (based on Youtube search relevance) or a random one if 'do_randomize' is
        True. It will be stored locally as 'output_filename' if found.

        This method returns the 'output_filename' of the locally stored video, or None if not
        downloaded.
        """
        return self._download_video(
            keywords = keywords,
            channel_id = YoutubeChannelId.STOCK,
            do_include_audio = do_include_audio,
            do_randomize = do_randomize,
            output_filename = Output.get_filename(output_filename, FileTypeX.VIDEO)
        )
    
    def download_sound_audio(
        self,
        keywords: str,
        output_filename: Union[str, None] = None
    ):
        """
        Downloads a sound from the specific Youtube channel
        configured as Sounds channel. It will be stored
        locally as 'output_filename' if found.

        This method returns the 'output_filename' of the
        locally stored audio, or None if not downloaded.
        """
        return self._download_audio(
            keywords = keywords,
            channel_id = YoutubeChannelId.SOUNDS,
            do_randomize = False,
            output_filename = Output.get_filename(output_filename, FileTypeX.AUDIO)
        )
    
    def download_music_audio(
        self,
        keywords: str,
        output_filename: Union[str, None] = None
    ):
        """
        Downloads a song from the specific Youtube channel
        configured as Music channel. It will be stored locally
        as 'output_filename' if found.

        This method returns the 'output_filename' of the
        locally stored audio, or None if not downloaded.
        """
        return self._download_audio(
            keywords = keywords,
            channel_id = YoutubeChannelId.MUSIC,
            do_randomize = False,
            output_filename = Output.get_filename(output_filename, FileTypeX.AUDIO)
        )
    
    def get_stock_video(
        self,
        keywords: str,
        do_randomize: bool = False
    ) -> YoutubeVideo:
        """
        Searches for the provided 'keywords' in the Stock
        Videos Youtube channel and gets, if available, a
        video matching those 'keywords'. It will return the
        first one or a random one if 'do_randomize' is True.
        """
        if not PythonValidator.is_string(keywords):
            raise Exception('The "keywords" parameter provided is not a valid string.')
        
        return self._get_video_from_keywords(keywords, YoutubeChannelId.STOCK, do_randomize)
    

"""
    Pay attention to this yt-dlp link, it is interesting: 
    https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#download-options
"""