#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Manages getting playstate from Kodi and sending it to the PMS as well as
subscribed Plex Companion clients.
"""
from logging import getLogger
from threading import Thread

from ..downloadutils import DownloadUtils as DU
from .. import timing
from .. import app
from .. import variables as v
from .. import json_rpc as js
from .. import playqueue as PQ

###############################################################################
LOG = getLogger('PLEX.subscribers')
###############################################################################

# What is Companion controllable?
CONTROLLABLE = {
    v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
        'subtitleStream,seekTo,skipPrevious,skipNext,'
        'stepBack,stepForward',
    v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
        'skipPrevious,skipNext,stepBack,stepForward',
    v.PLEX_PLAYLIST_TYPE_PHOTO: 'playPause,stop,skipPrevious,skipNext'
}

STREAM_DETAILS = {
    'video': 'currentvideostream',
    'audio': 'currentaudiostream',
    'subtitle': 'currentsubtitle'
}

XML = ('%s<MediaContainer commandID="{command_id}" location="{location}">\n'
       '  <Timeline {%s}/>\n'
       '  <Timeline {%s}/>\n'
       '  <Timeline {%s}/>\n'
       '</MediaContainer>\n') % (v.XML_HEADER,
                                 v.PLEX_PLAYLIST_TYPE_VIDEO,
                                 v.PLEX_PLAYLIST_TYPE_AUDIO,
                                 v.PLEX_PLAYLIST_TYPE_PHOTO)

# Headers are different for Plex Companion - use these for PMS notifications
HEADERS_PMS = {
    'Connection': 'keep-alive',
    'Accept': 'text/plain, */*; q=0.01',
    'Accept-Language': 'en',
    'Accept-Encoding': 'gzip, deflate',
    'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.DEVICE)
}


def params_pms():
    """
    Returns the url parameters for communicating with the PMS
    """
    return {
        'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
        'X-Plex-Device': v.DEVICE,
        'X-Plex-Device-Name': v.DEVICENAME,
        'X-Plex-Model': v.MODEL,
        'X-Plex-Platform': v.PLATFORM,
        'X-Plex-Platform-Version': v.PLATFORM_VERSION,
        'X-Plex-Product': v.ADDON_NAME,
        'X-Plex-Version': v.ADDON_VERSION,
    }


def headers_companion_client():
    """
    Headers are different for Plex Companion - use these for a Plex Companion
    client
    """
    return {
        'Content-Type': 'application/xml',
        'Connection': 'Keep-Alive',
        'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
        'X-Plex-Device-Name': v.DEVICENAME,
        'X-Plex-Platform': v.PLATFORM,
        'X-Plex-Platform-Version': v.PLATFORM_VERSION,
        'X-Plex-Product': v.ADDON_NAME,
        'X-Plex-Version': v.ADDON_VERSION,
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'en,*'
    }


def update_player_info(playerid):
    """
    Updates all player info for playerid [int] in state.py.
    """
    app.PLAYSTATE.player_states[playerid].update(js.get_player_props(playerid))
    app.PLAYSTATE.player_states[playerid]['volume'] = js.get_volume()
    app.PLAYSTATE.player_states[playerid]['muted'] = js.get_muted()


class SubscriptionMgr(object):
    """
    Manages Plex companion subscriptions
    """
    def __init__(self, request_mgr, player):
        self.serverlist = []
        self.subscribers = {}
        self.info = {}
        self.server = ""
        self.protocol = "http"
        self.port = ""
        self.isplaying = False
        self.location = 'navigation'
        # In order to be able to signal a stop at the end
        self.last_params = {}
        self.lastplayers = {}
        # In order to signal a stop to Plex Web ONCE on playback stop
        self.stop_sent_to_web = True
        self.request_mgr = request_mgr

    def _server_by_host(self, host):
        if len(self.serverlist) == 1:
            return self.serverlist[0]
        for server in self.serverlist:
            if (server.get('serverName') in host or
                    server.get('server') in host):
                return server
        return {}

    @staticmethod
    def _get_correct_position(info, playqueue):
        """
        Kodi tells us the PLAYLIST position, not PLAYQUEUE position, if the
        user initiated playback of a playlist
        """
        if playqueue.kodi_playlist_playback:
            position = 0
        else:
            position = info['position'] or 0
        return position

    def msg(self, players):
        """
        Returns a timeline xml as str
        (xml containing video, audio, photo player state)
        """
        self.isplaying = False
        self.location = 'navigation'
        answ = str(XML)
        timelines = {
            v.PLEX_PLAYLIST_TYPE_VIDEO: None,
            v.PLEX_PLAYLIST_TYPE_AUDIO: None,
            v.PLEX_PLAYLIST_TYPE_PHOTO: None
        }
        for typus in timelines:
            if players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]) is None:
                timeline = {
                    'controllable': CONTROLLABLE[typus],
                    'type': typus,
                    'state': 'stopped'
                }
            else:
                timeline = self._timeline_dict(players[
                        v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]],
                    typus)
            timelines[typus] = self._dict_to_xml(timeline)
        timelines.update({'command_id': '{command_id}',
                         'location': self.location})
        return answ.format(**timelines)

    @staticmethod
    def _dict_to_xml(dictionary):
        """
        Returns the string 'key1="value1" key2="value2" ...' for dictionary
        """
        answ = ''
        for key, value in dictionary.items():
            answ += '%s="%s" ' % (key, value)
        return answ

    def _timeline_dict(self, player, ptype):
        with app.APP.lock_playqueues:
            playerid = player['playerid']
            info = app.PLAYSTATE.player_states[playerid]
            playqueue = PQ.PLAYQUEUES[playerid]
            position = self._get_correct_position(info, playqueue)
            try:
                item = playqueue.items[position]
            except IndexError:
                # E.g. for direct path playback for single item
                return {
                    'controllable': CONTROLLABLE[ptype],
                    'type': ptype,
                    'state': 'stopped'
                }
            self.isplaying = True
            self.stop_sent_to_web = False
            if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO,
                         v.PLEX_PLAYLIST_TYPE_PHOTO):
                self.location = 'fullScreenVideo'
            pbmc_server = app.CONN.server
            if pbmc_server:
                (self.protocol, self.server, self.port) = pbmc_server.split(':')
                self.server = self.server.replace('/', '')
            status = 'paused' if int(info['speed']) == 0 else 'playing'
            duration = timing.kodi_time_to_millis(info['totaltime'])
            shuffle = '1' if info['shuffled'] else '0'
            mute = '1' if info['muted'] is True else '0'
            answ = {
                'controllable': CONTROLLABLE[ptype],
                'protocol': self.protocol,
                'address': self.server,
                'port': self.port,
                'machineIdentifier': app.CONN.machine_identifier,
                'state': status,
                'type': ptype,
                'itemType': ptype,
                'time': timing.kodi_time_to_millis(info['time']),
                'duration': duration,
                'seekRange': '0-%s' % duration,
                'shuffle': shuffle,
                'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
                'volume': info['volume'],
                'mute': mute,
                'mediaIndex': 0,  # Still to implement from here
                'partIndex': 0,
                'partCount': 1,
                'providerIdentifier': 'com.plexapp.plugins.library',
            }
            # Get the plex id from the PKC playqueue not info, as Kodi jumps to
            # next playqueue element way BEFORE kodi monitor onplayback is
            # called
            if item.plex_id:
                answ['key'] = '/library/metadata/%s' % item.plex_id
                answ['ratingKey'] = item.plex_id
            # PlayQueue stuff
            if info['container_key']:
                answ['containerKey'] = info['container_key']
            if (info['container_key'] is not None and
                    info['container_key'].startswith('/playQueues')):
                answ['playQueueID'] = playqueue.id
                answ['playQueueVersion'] = playqueue.version
                answ['playQueueItemID'] = item.id
            if playqueue.items[position].guid:
                answ['guid'] = item.guid
            # Temp. token set?
            if app.CONN.plex_transient_token:
                answ['token'] = app.CONN.plex_transient_token
            elif playqueue.plex_transient_token:
                answ['token'] = playqueue.plex_transient_token
            # Process audio and subtitle streams
            if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
                strm_id = self._plex_stream_index(playerid, 'audio')
                if strm_id:
                    answ['audioStreamID'] = strm_id
                else:
                    LOG.error('We could not select a Plex audiostream')
                strm_id = self._plex_stream_index(playerid, 'video')
                if strm_id:
                    answ['videoStreamID'] = strm_id
                else:
                    LOG.error('We could not select a Plex videostream')
                if info['subtitleenabled']:
                    try:
                        strm_id = self._plex_stream_index(playerid, 'subtitle')
                    except KeyError:
                        # subtitleenabled can be True while currentsubtitle can
                        # still be {}
                        strm_id = None
                    if strm_id is not None:
                        # If None, then the subtitle is only present on Kodi
                        # side
                        answ['subtitleStreamID'] = strm_id
            return answ

    def signal_stop(self):
        """
        Externally called on PKC shutdown to ensure that PKC signals a stop to
        the PMS. Otherwise, PKC might be stuck at "currently playing"
        """
        LOG.info('Signaling a complete stop to PMS')
        # To avoid RuntimeError, don't use self.lastplayers
        for playerid in (0, 1, 2):
            self.last_params['state'] = 'stopped'
            self._send_pms_notification(playerid,
                                        self.last_params,
                                        timeout=0.0001)

    def _plex_stream_index(self, playerid, stream_type):
        """
        Returns the current Plex stream index [str] for the player playerid

        stream_type: 'video', 'audio', 'subtitle'
        """
        playqueue = PQ.PLAYQUEUES[playerid]
        info = app.PLAYSTATE.player_states[playerid]
        position = self._get_correct_position(info, playqueue)
        if info[STREAM_DETAILS[stream_type]] == -1:
            kodi_stream_index = -1
        else:
            kodi_stream_index = info[STREAM_DETAILS[stream_type]]['index']
        return playqueue.items[position].plex_stream_index(kodi_stream_index,
                                                           stream_type)

    def update_command_id(self, uuid, command_id):
        """
        Updates the Plex Companien client with the machine identifier uuid with
        command_id
        """
        with app.APP.lock_subscriber:
            if command_id and self.subscribers.get(uuid):
                self.subscribers[uuid].command_id = int(command_id)

    def _playqueue_init_done(self, players):
        """
        update_player_info() can result in values BEFORE kodi monitor is called.
        Hence we'd have a missmatch between the state.PLAYER_STATES and our
        playqueues.
        """
        for player in list(players.values()):
            info = app.PLAYSTATE.player_states[player['playerid']]
            playqueue = PQ.PLAYQUEUES[player['playerid']]
            position = self._get_correct_position(info, playqueue)
            try:
                item = playqueue.items[position]
            except IndexError:
                # E.g. for direct path playback for single item
                return False
            if item.plex_id != info['plex_id']:
                # Kodi playqueue already progressed; need to wait until
                # everything is loaded
                return False
        return True

    def notify(self):
        """
        Causes PKC to tell the PMS and Plex Companion players to receive a
        notification what's being played.
        """
        with app.APP.lock_subscriber:
            self._cleanup()
            # Get all the active/playing Kodi players (video, audio, pictures)
            players = js.get_players()
            # Update the PKC info with what's playing on the Kodi side
            for player in list(players.values()):
                update_player_info(player['playerid'])
            # Check whether we can use the CURRENT info or whether PKC is still
            # initializing
            if self._playqueue_init_done(players) is False:
                LOG.debug('PKC playqueue is still initializing - skip update')
                return
            self._notify_server(players)
            if self.subscribers:
                msg = self.msg(players)
                for subscriber in list(self.subscribers.values()):
                    subscriber.send_update(msg)
            self.lastplayers = players

    def _notify_server(self, players):
        for typus, player in players.items():
            self._send_pms_notification(
                player['playerid'], self._get_pms_params(player['playerid']))
            try:
                del self.lastplayers[typus]
            except KeyError:
                pass
        # Process the players we have left (to signal a stop)
        for player in list(self.lastplayers.values()):
            self.last_params['state'] = 'stopped'
            self._send_pms_notification(player['playerid'], self.last_params)

    def _get_pms_params(self, playerid):
        info = app.PLAYSTATE.player_states[playerid]
        playqueue = PQ.PLAYQUEUES[playerid]
        position = self._get_correct_position(info, playqueue)
        try:
            item = playqueue.items[position]
        except IndexError:
            return self.last_params
        status = 'paused' if int(info['speed']) == 0 else 'playing'
        params = {
            'state': status,
            'ratingKey': item.plex_id,
            'key': '/library/metadata/%s' % item.plex_id,
            'time': timing.kodi_time_to_millis(info['time']),
            'duration': timing.kodi_time_to_millis(info['totaltime'])
        }
        if info['container_key'] is not None:
            # params['containerKey'] = info['container_key']
            if info['container_key'].startswith('/playQueues/'):
                # params['playQueueVersion'] = playqueue.version
                # params['playQueueID'] = playqueue.id
                params['playQueueItemID'] = item.id
        self.last_params = params
        return params

    def _send_pms_notification(self, playerid, params, timeout=None):
        """
        Pass a really low timeout in seconds if shutting down Kodi and we don't
        need the PMS' response
        """
        serv = self._server_by_host(self.server)
        playqueue = PQ.PLAYQUEUES[playerid]
        xargs = params_pms()
        xargs.update(params)
        if app.CONN.plex_transient_token:
            xargs['X-Plex-Token'] = app.CONN.plex_transient_token
        elif playqueue.plex_transient_token:
            xargs['X-Plex-Token'] = playqueue.plex_transient_token
        elif app.ACCOUNT.pms_token:
            xargs['X-Plex-Token'] = app.ACCOUNT.pms_token
        url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
                                         serv.get('server', 'localhost'),
                                         serv.get('port', '32400'))
        DU().downloadUrl(url,
                         authenticate=False,
                         parameters=xargs,
                         headerOverride=HEADERS_PMS,
                         timeout=timeout)
        LOG.debug("Sent server notification with parameters: %s to %s",
                  xargs, url)

    def add_subscriber(self, protocol, host, port, uuid, command_id):
        """
        Adds a new Plex Companion subscriber to PKC.
        """
        subscriber = Subscriber(protocol,
                                host,
                                port,
                                uuid,
                                command_id,
                                self,
                                self.request_mgr)
        with app.APP.lock_subscriber:
            self.subscribers[subscriber.uuid] = subscriber
        return subscriber

    def remove_subscriber(self, uuid):
        """
        Removes a connected Plex Companion subscriber with machine identifier
        uuid from PKC notifications.
        (Calls the cleanup() method of the subscriber)
        """
        with app.APP.lock_subscriber:
            for subscriber in list(self.subscribers.values()):
                if subscriber.uuid == uuid or subscriber.host == uuid:
                    subscriber.cleanup()
                    del self.subscribers[subscriber.uuid]

    def _cleanup(self):
        for subscriber in list(self.subscribers.values()):
            if subscriber.age > 30:
                subscriber.cleanup()
                del self.subscribers[subscriber.uuid]


class Subscriber(object):
    """
    Plex Companion subscribing device
    """
    def __init__(self, protocol, host, port, uuid, command_id, sub_mgr,
                 request_mgr):
        self.protocol = protocol or "http"
        self.host = host
        self.port = port or 32400
        self.uuid = uuid or host
        self.command_id = int(command_id) or 0
        self.age = 0
        self.sub_mgr = sub_mgr
        self.request_mgr = request_mgr

    def __eq__(self, other):
        return self.uuid == other.uuid

    def cleanup(self):
        """
        Closes the connection to the Plex Companion client
        """
        self.request_mgr.closeConnection(self.protocol, self.host, self.port)

    def send_update(self, msg):
        """
        Sends msg to the Plex Companion client (via .../:/timeline)
        """
        self.age += 1
        msg = msg.format(command_id=self.command_id)
        LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s",
                  self.uuid, self.command_id, msg)
        url = '%s://%s:%s/:/timeline' % (self.protocol, self.host, self.port)
        thread = Thread(target=self._threaded_send, args=(url, msg))
        thread.start()

    def _threaded_send(self, url, msg):
        """
        Threaded POST request, because they stall due to response missing
        the Content-Length header :-(
        """
        response = DU().downloadUrl(url,
                                    action_type="POST",
                                    postBody=msg,
                                    authenticate=False,
                                    headerOverride=headers_companion_client())
        if response in (False, None, 401):
            self.sub_mgr.remove_subscriber(self.uuid)