# -*- coding: utf-8 -*-
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""This module collects and prepares stream info for Kodi Player."""

from __future__ import absolute_import, division, unicode_literals

try:  # Python 3
    from urllib.error import HTTPError
    from urllib.parse import quote, unquote, urlencode
    from urllib.request import build_opener, install_opener, urlopen, ProxyHandler
except ImportError:  # Python 2
    from urllib import urlencode
    from urllib2 import build_opener, install_opener, urlopen, ProxyHandler, quote, unquote, HTTPError

from helperobjects import ApiData, StreamURLS
from kodiutils import (addon_profile, can_play_drm, container_reload, exists, end_of_directory, get_max_bandwidth,
                       get_proxies, get_setting_bool, get_url_json, has_inputstream_adaptive,
                       invalidate_caches, kodi_version_major, localize, log, log_error, mkdir, ok_dialog, open_settings,
                       supports_drm, to_unicode)


class StreamService:
    """Collect and prepare stream info for Kodi Player"""

    _VUPLAY_API_URL = 'https://api.vuplay.co.uk'
    _VUALTO_API_URL = 'https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v1'
    _CLIENT = 'vrtvideo@PROD'
    _UPLYNK_LICENSE_URL = 'https://content.uplynk.com/wv'
    _INVALID_LOCATION = 'INVALID_LOCATION'
    _INCOMPLETE_ROAMING_CONFIG = 'INCOMPLETE_ROAMING_CONFIG'
    _GEOBLOCK_ERROR_CODES = (_INCOMPLETE_ROAMING_CONFIG, _INVALID_LOCATION)

    def __init__(self, _tokenresolver):
        """Initialize Stream Service class"""
        install_opener(build_opener(ProxyHandler(get_proxies())))
        self._tokenresolver = _tokenresolver
        self._create_settings_dir()
        self._can_play_drm = can_play_drm()
        self._vualto_license_url = None

    def _get_vualto_license_url(self):
        """Get Widevine license URL from Vualto API"""
        json_data = get_url_json(url=self._VUPLAY_API_URL, fail={})
        self._vualto_license_url = json_data.get('drm_providers', {}).get('widevine', {}).get('la_url')

    @staticmethod
    def _create_settings_dir():
        """Create settings directory"""
        settingsdir = addon_profile()
        if not exists(settingsdir):
            mkdir(settingsdir)

    @staticmethod
    def _get_license_key(key_url, key_type='R', key_headers=None, key_value=None):
        """Generates a proper Widevine license key value

            # A{SSM} -> not implemented
            # R{SSM} -> raw format
            # B{SSM} -> base64 format
            # D{SSM} -> decimal format

            The generic format for a LicenseKey is:
            |<url>|<headers>|<key with placeholders|

            The Widevine Decryption Key Identifier (KID) can be inserted via the placeholder {KID}

            @type key_url: str
            @param key_url: the URL where the license key can be obtained

            @type key_type: str
            @param key_type: the key type (A, R, B or D)

            @type key_headers: dict
            @param key_headers: A dictionary that contains the HTTP headers to pass

            @type key_value: str
            @param key_value: i
            @return:
       """
        header = ''
        if key_headers:
            header = urlencode(key_headers)

        if key_type in ('A', 'R', 'B'):
            key_value = key_type + '{SSM}'
        elif key_type == 'D':
            if 'D{SSM}' not in key_value:
                raise ValueError('Missing D{SSM} placeholder')
            key_value = quote(key_value)

        return '{key_url}|{header}|{key_value}|'.format(key_url=key_url, header=header, key_value=key_value)

    def _get_api_data(self, video):
        """Create api data object from video dictionary"""
        video_url = video.get('video_url')
        video_id = video.get('video_id')
        publication_id = video.get('publication_id')
        # Prepare api_data for on demand streams by video_id and publication_id
        if video_id and publication_id:
            api_data = ApiData(self._CLIENT, self._VUALTO_API_URL, video_id, publication_id + quote('$'), False)
        # Prepare api_data for livestreams by video_id, e.g. vualto_strubru, vualto_mnm, ketnet_jr
        elif video_id and not video_url:
            api_data = ApiData(self._CLIENT, self._VUALTO_API_URL, video_id, '', True)
        # Webscrape api_data with video_id fallback
        elif video_url:
            api_data = self._webscrape_api_data(video_url)
            if video_id:
                api_data = ApiData(self._CLIENT, self._VUALTO_API_URL, video_id, '', True)
        return api_data

    def _webscrape_api_data(self, video_url):
        """Scrape api data from VRT NU html page"""
        from webscraper import get_video_attributes
        video_data = get_video_attributes(video_url)

        # Web scraping failed, log error
        if not video_data:
            log_error('Web scraping api data failed, empty video_data')
            return None

        # Store required html data attributes
        client = video_data.get('client') or self._CLIENT
        media_api_url = video_data.get('mediaapiurl')
        video_id = video_data.get('videoid')
        publication_id = video_data.get('publicationid', '')
        # Live stream or on demand
        if video_id is None:
            is_live_stream = True
            video_id = video_data.get('livestream')
        else:
            is_live_stream = False
            publication_id += quote('$')

        if client is None or media_api_url is None or (video_id is None and publication_id is None):
            log_error('Web scraping api data failed, required attributes missing')
            return None

        return ApiData(client, media_api_url, video_id, publication_id, is_live_stream)

    def _get_stream_json(self, api_data, roaming=False):
        """Get JSON with stream details from VRT API"""
        if not api_data:
            return None
        token_url = api_data.media_api_url + '/tokens'
        if api_data.is_live_stream:
            playertoken = self._tokenresolver.get_token('vrtPlayerToken', 'live', token_url, roaming=roaming)
        else:
            playertoken = self._tokenresolver.get_token('vrtPlayerToken', 'ondemand', token_url, roaming=roaming)

        # Construct api_url and get video json
        if not playertoken:
            return None
        api_url = api_data.media_api_url + '/videos/' + api_data.publication_id + \
            api_data.video_id + '?vrtPlayerToken=' + playertoken + '&client=' + api_data.client
        return get_url_json(url=api_url)

    @staticmethod
    def _fix_virtualsubclip(manifest_url, duration):
        '''VRT NU uses virtual subclips to provide on demand programs (mostly current affair programs)
           already from a livestream while or shortly after live broadcasting them.
           But this feature doesn't work always as expected because Kodi doesn't play the program from
           the beginning when the ending timestamp of the program is missing from the stream_url.
           When begintime is present in the stream_url and endtime is missing, we must add endtime
           to the stream_url so Kodi treats the program as an on demand program and starts the stream
           from the beginning like a real on demand program.'''
        begin = manifest_url.split('?t=')[1] if '?t=' in manifest_url else None
        if begin and len(begin) == 19:
            from datetime import datetime, timedelta
            import dateutil.parser
            begin_time = dateutil.parser.parse(begin)
            end_time = begin_time + duration
            # If on demand program is not yet broadcasted completely,
            # use current time minus 5 seconds safety margin as endtime.
            now = datetime.utcnow()
            if end_time > now:
                end_time = now - timedelta(seconds=5)
                manifest_url += '-' + end_time.strftime('%Y-%m-%dT%H:%M:%S')
        return manifest_url

    def get_stream(self, video, roaming=False, api_data=None):
        """Main streamservice function"""
        if not api_data:
            api_data = self._get_api_data(video)

        stream_json = self._get_stream_json(api_data, roaming)

        if not stream_json:

            # Roaming token failed
            if roaming:
                message = localize(30964)  # Geoblock error: Cannot be played, need Belgian phone number validation
                return self._handle_stream_api_error(message)
            # X-VRT-Token failed
            message = localize(30963)  # You need a VRT NU account to play this stream.
            return self._handle_stream_api_error(message)

        if 'targetUrls' in stream_json:

            # DRM support for ketnet junior/uplynk streaming service
            uplynk = 'uplynk.com' in stream_json.get('targetUrls')[0].get('url')

            vudrm_token = stream_json.get('drm')
            drm_stream = (vudrm_token or uplynk)

            # Select streaming protocol
            if not drm_stream and has_inputstream_adaptive():
                protocol = 'mpeg_dash'
            elif drm_stream and self._can_play_drm:
                protocol = 'mpeg_dash'
            elif vudrm_token:
                protocol = 'hls_aes'
            else:
                protocol = 'hls'

            # Get stream manifest url
            manifest_url = next(stream.get('url') for stream in stream_json.get('targetUrls') if stream.get('type') == protocol)

            # External virtual subclip, live-to-VOD from past 24 hours archived livestream (airdate feature)
            if video.get('start_date') and video.get('end_date'):
                manifest_url += '?t=' + video.get('start_date') + '-' + video.get('end_date')

            # Fix virtual subclip
            from datetime import timedelta
            duration = timedelta(milliseconds=stream_json.get('duration', 0))
            manifest_url = self._fix_virtualsubclip(manifest_url, duration)

            # Prepare stream for Kodi player
            if protocol == 'mpeg_dash' and drm_stream:
                log(2, 'Protocol: mpeg_dash drm')
                if vudrm_token:
                    if self._vualto_license_url is None:
                        self._get_vualto_license_url()
                    encryption_json = '{{"token":"{0}","drm_info":[D{{SSM}}],"kid":"{{KID}}"}}'.format(vudrm_token)
                    license_key = self._get_license_key(key_url=self._vualto_license_url,
                                                        key_type='D',
                                                        key_value=encryption_json,
                                                        key_headers={'Content-Type': 'text/plain;charset=UTF-8'})
                else:
                    license_key = self._get_license_key(key_url=self._UPLYNK_LICENSE_URL, key_type='R')

                stream = StreamURLS(manifest_url, license_key=license_key, use_inputstream_adaptive=True)
            elif protocol == 'mpeg_dash':
                log(2, 'Protocol: mpeg_dash')
                stream = StreamURLS(manifest_url, use_inputstream_adaptive=True)
            else:
                log(2, 'Protocol: {protocol}', protocol=protocol)
                # Fix 720p quality for HLS livestreams
                manifest_url += '?hd' if '.m3u8?' not in manifest_url else '&hd'
                stream = self._select_hls_substreams(manifest_url, protocol)
            return stream

        # VRT Geoblock: failed to get stream, now try again with roaming enabled
        if stream_json.get('code') in self._GEOBLOCK_ERROR_CODES:
            log_error('VRT Geoblock: {msg}', msg=stream_json.get('message'))
            if not roaming:
                return self.get_stream(video, roaming=True, api_data=api_data)

            if stream_json.get('code') == self._INVALID_LOCATION:
                message = localize(30965)  # Geoblock error: Blocked on your geographical location based on your IP address
                return self._handle_stream_api_error(message, stream_json)

            message = localize(30964)  # Geoblock error: Cannot be played, need Belgian phone number validation
            return self._handle_stream_api_error(message, stream_json)
        if stream_json.get('code') == 'VIDEO_NOT_FOUND':
            # Refresh listing
            invalidate_caches('*.json')
            container_reload()
            message = localize(30987)  # No stream found
            return self._handle_stream_api_error(message, stream_json)

        # Failed to get stream, handle error
        message = localize(30954)  # Whoops something went wrong
        return self._handle_stream_api_error(message, stream_json)

    @staticmethod
    def _handle_stream_api_error(message, video_json=None):
        """Show localized stream api error messages in Kodi GUI"""
        if video_json:
            log_error(video_json.get('message'))
        ok_dialog(message=message)
        end_of_directory()

    @staticmethod
    def _handle_bad_stream_error(protocol, code=None, reason=None):
        """Show a localized error message in Kodi GUI for a failing VRT NU stream based on protocol: hls, hls_aes, mpeg_dash)
            message: VRT NU stream <stream_type> problem, try again with (InputStream Adaptive) (and) (DRM) enabled/disabled:
                30959=and DRM, 30960=disabled, 30961=enabled
       """
        # HLS AES DRM failed
        if protocol == 'hls_aes' and not supports_drm():
            message = localize(30962, protocol=protocol.upper(), version=kodi_version_major())
        elif protocol == 'hls_aes' and not has_inputstream_adaptive() and not get_setting_bool('usedrm', default=True):
            message = localize(30958, protocol=protocol.upper(), component=localize(30959), state=localize(30961))
        elif protocol == 'hls_aes' and has_inputstream_adaptive():
            message = localize(30958, protocol=protocol.upper(), component='Widevine DRM', state=localize(30961))
        elif protocol == 'hls_aes' and get_setting_bool('usedrm', default=True):
            message = localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=localize(30961))
        else:
            message = localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=localize(30960))
        heading = 'HTTP Error {code}: {reason}'.format(code=code, reason=reason) if code and reason else None
        log_error('Unable to play stream. {error}', error=heading)
        ok_dialog(heading=heading, message=message)
        end_of_directory()

    def _select_hls_substreams(self, master_hls_url, protocol):
        """Select HLS substreams to speed up Kodi player start, workaround for slower kodi selection"""
        hls_variant_url = None
        subtitle_url = None
        hls_audio_id = None
        hls_subtitle_id = None
        hls_base_url = master_hls_url.split('.m3u8')[0]
        log(2, 'URL get: {url}', url=unquote(master_hls_url))
        try:
            hls_playlist = to_unicode(urlopen(master_hls_url).read())
        except HTTPError as exc:
            if exc.code == 415:
                self._handle_bad_stream_error(protocol, exc.code, exc.reason)
                return None
            raise
        max_bandwidth = get_max_bandwidth()
        stream_bandwidth = None

        # Get hls variant url based on max_bandwidth setting
        import re
        hls_variant_regex = re.compile(r'#EXT-X-STREAM-INF:[\w\-.,=\"]*?BANDWIDTH=(?P<BANDWIDTH>\d+),'
                                       r'[\w\-.,=\"]+\d,(?:AUDIO=\"(?P<AUDIO>[\w\-]+)\",)?(?:SUBTITLES=\"'
                                       r'(?P<SUBTITLES>\w+)\",)?[\w\-.,=\"]+?[\r\n](?P<URI>[\w:\/\-.=?&]+)')
        # reverse sort by bandwidth
        for match in sorted(re.finditer(hls_variant_regex, hls_playlist), key=lambda m: int(m.group('BANDWIDTH')), reverse=True):
            stream_bandwidth = int(match.group('BANDWIDTH')) // 1000
            if max_bandwidth == 0 or stream_bandwidth < max_bandwidth:
                if match.group('URI').startswith('http'):
                    hls_variant_url = match.group('URI')
                else:
                    hls_variant_url = hls_base_url + match.group('URI')
                hls_audio_id = match.group('AUDIO')
                hls_subtitle_id = match.group('SUBTITLES')
                break

        if stream_bandwidth > max_bandwidth and not hls_variant_url:
            message = localize(30057, max=max_bandwidth, min=stream_bandwidth)
            ok_dialog(message=message)
            open_settings()

        # Get audio url
        if hls_audio_id:
            audio_regex = re.compile(r'#EXT-X-MEDIA:TYPE=AUDIO[\w\-=,\.\"\/]+?GROUP-ID=\"' + hls_audio_id + ''
                                     r'\"[\w\-=,\.\"\/]+?URI=\"(?P<AUDIO_URI>[\w\-=]+)\.m3u8\"')
            match_audio = re.search(audio_regex, hls_playlist)
            if match_audio:
                hls_variant_url = hls_base_url + match_audio.group('AUDIO_URI') + '-' + hls_variant_url.split('-')[-1]

        # Get subtitle url, works only for on demand streams
        if get_setting_bool('showsubtitles', default=True) and '/live/' not in master_hls_url and hls_subtitle_id:
            subtitle_regex = re.compile(r'#EXT-X-MEDIA:TYPE=SUBTITLES[\w\-=,\.\"\/]+?GROUP-ID=\"' + hls_subtitle_id + ''
                                        r'\"[\w\-=,\.\"\/]+URI=\"(?P<SUBTITLE_URI>[\w\-=]+)\.m3u8\"')
            match_subtitle = re.search(subtitle_regex, hls_playlist)
            if match_subtitle:
                subtitle_url = hls_base_url + match_subtitle.group('SUBTITLE_URI') + '.webvtt'

        return StreamURLS(hls_variant_url, subtitle_url)