# -*- coding: utf-8 -*-
"""Start a MyTV / MyCinema channel without its dialog (docs/plan/milionar-library/150-widget-clicks.md).

`action=channel_play&kind=tv|cinema&channel_id=<id>&mode=live|live_shuffle|first|random`, started by a click on a channel
of the Home widgets. It does what the buttons of the dialogs do, with the same data:

* MyTV `live`: the channel's program queue (the cache `program_list_<id>` the dialog shows, without the episodes the
  channel already watched, in the user's saved order, filled up to `mytv_upcoming_count` with the next episodes of the
  channel's series) as one playlist, with the countdown monitor of the dialog; `live_shuffle`: a new shuffled queue;
  `first`: the first episode of the queue; `random`: the next episode of a random series.
* MyCinema `live`: up to 20 shuffled films of the cinema as a playlist; `random`: a random unwatched film.

The queue is stored back in the same cache format, so the dialog shows exactly what was played.
"""
import random

import xbmc
import xbmcaddon
import xbmcgui

ADDON_ID = 'plugin.video.milionar'
S_EMPTY = 32399                                    # "Kanál nemá žádný program"
EPISODE_TTL = 7 * 24 * 3600
QUEUE_TTL = 24 * 3600


def _log(message, level=xbmc.LOGINFO):
    xbmc.log('[Milionář ChannelPlay] {}'.format(message), level)


def _notify(addon, text):
    xbmcgui.Dialog().notification('Milionář', text, xbmcgui.NOTIFICATION_INFO, 4000)


def _int_setting(addon, key, default):
    try:
        return int(addon.getSetting(key) or default)
    except (TypeError, ValueError):
        return default


def block_range(addon):
    """(min, max) episodes in a row of one series, from `mytv_block_size` ("2" or "1-3")."""
    value = addon.getSetting('mytv_block_size') or '2'
    try:
        if '-' in value:
            low, high = value.split('-', 1)
            return int(low), int(high)
        return int(value), int(value)
    except (TypeError, ValueError):
        return 1, 1


# --------------------------------------------------------------------------- MyTV queue

def program_item(meta, series_name, series_id):
    """Queue entry in the cache format of the MyTV dialog (`_add_single_program_item` + `_save_program_list_cache`)."""
    plot = meta.get('plot', '')
    props = {
        'tmdb_id': str(series_id), 'series_name': series_name, 'season': str(meta['season']),
        'episode': str(meta['episode']), 'ep_code': 'S{:02d}E{:02d}'.format(meta['season'], meta['episode']),
        'episode_title': meta.get('episode_title', ''), 'plot': plot,
        'plot_short': plot[:77] + '...' if len(plot) > 80 else plot, 'runtime': str(meta.get('runtime', '')),
        'rating': str(meta.get('rating', '')), 'series_logo': meta.get('series_logo', ''),
        'poster': meta.get('poster', ''), 'fanart': meta.get('fanart', ''), 'year': str(meta.get('year', '')),
        'genre': meta.get('genre', ''), 'air_date': ''}
    thumb = meta.get('episode_thumb') or meta.get('poster') or ''
    return {'label': series_name, 'label2': str(meta.get('runtime', '')), 'properties': props,
            'art': {'thumb': thumb, 'fanart': meta.get('fanart', ''), 'clearlogo': '', 'poster': meta.get('poster', '')}}


def _next_meta(addon, cache, series_id, season, episode):
    """Next episode of a series after (season, episode): the dialog's episode cache first, then TMDb."""
    key = 'episode_{}_{}_{}'.format(series_id, season, episode + 1)
    meta = cache.get(key)
    if not meta:
        from wcs.metadata import TMDbClient
        meta = TMDbClient.get_next_episode_from_tmdb(series_id, season, episode, addon)
        if meta:
            cache.set(key, meta, ttl_seconds=EPISODE_TTL)
    return meta


def drop_watched(channel_id, items):
    """The queue without the episodes the channel's progress is already past (what the dialog does when it opens)."""
    from wcs.ai import ProgressManager
    remaining = []
    for item in items:
        props = item.get('properties', {})
        tmdb_id, season = props.get('tmdb_id', ''), int(props.get('season', 0) or 0)
        episode = int(props.get('episode', 0) or 0)
        if tmdb_id and season:
            done_season, done_episode = ProgressManager.get_progress(channel_id, tmdb_id)
            if done_season > season or (done_season == season and done_episode >= episode):
                continue
        remaining.append(item)
    return remaining


def apply_reorder(cache, channel_id, items):
    """The user's saved order (`program_reorder_<id>`): the listed episodes first in that order, new ones after them."""
    saved = cache.get('program_reorder_{}'.format(channel_id))
    if not saved:
        return items
    known, rest = {}, []
    for item in items:
        props = item.get('properties', {})
        key = '{}_{}'.format(props.get('tmdb_id', ''), props.get('ep_code', ''))
        if props.get('tmdb_id') and props.get('ep_code') and key in saved:
            known[key] = item
        else:
            rest.append(item)
    ordered = [known[key] for key in saved if key in known] + rest
    return ordered or items


def fill(addon, cache, channel, items, count, shuffle_series=True):
    """Add next episodes to `items` until it holds `count` (the series take turns in blocks, like the dialog)."""
    from wcs.ai import ProgressManager
    items = list(items)
    series_list = list(channel.series_list or [])
    if not series_list or len(items) >= count:
        return items
    if shuffle_series:
        random.shuffle(series_list)
    existing, last = set(), {}
    for item in items:
        props = item.get('properties', {})
        existing.add('{}_{}'.format(props.get('tmdb_id', ''), props.get('ep_code', '')))
        season, episode = int(props.get('season', 0) or 0), int(props.get('episode', 0) or 0)
        if props.get('tmdb_id') and season and (season, episode) > last.get(props['tmdb_id'], (0, 0)):
            last[props['tmdb_id']] = (season, episode)
    progress = {str(s.get('id')): last.get(str(s.get('id'))) or ProgressManager.get_progress(channel.id, str(s.get('id')))
                for s in series_list}
    low, high = block_range(addon)
    wanted, loops, index = count - len(items), 0, 0
    added = 0
    while added < wanted and loops < wanted * 3:
        loops += 1
        series = series_list[index % len(series_list)]
        index += 1
        series_id, name = str(series.get('id')), series.get('name')
        for _ in range(random.randint(low, max(low, high))):
            if added >= wanted:
                break
            season, episode = progress[series_id]
            meta = _next_meta(addon, cache, series_id, season, episode)
            if not meta:
                break
            progress[series_id] = (meta['season'], meta['episode'])
            key = '{}_S{:02d}E{:02d}'.format(series_id, meta['season'], meta['episode'])
            if key in existing:
                continue
            items.append(program_item(meta, name, series_id))
            existing.add(key)
            added += 1
    return items


def tv_queue(addon, cache, channel, fresh=False):
    """The playable queue of a MyTV channel; stored back in the cache the dialog reads."""
    key = 'program_list_{}'.format(channel.id)
    count = _int_setting(addon, 'mytv_upcoming_count', 12)
    if fresh:
        cache.delete(key)
        cache.delete('program_reorder_{}'.format(channel.id))
        items = fill(addon, cache, channel, [], count, shuffle_series=True)
    else:
        items = apply_reorder(cache, channel.id, drop_watched(channel.id, cache.get(key) or []))
        items = fill(addon, cache, channel, items, count, shuffle_series=bool(items))
    if items:
        cache.set(key, items, ttl_seconds=QUEUE_TTL)
        cache.flush()
        _program_changed()
    return items


def _program_changed():
    """Home shows what is on a channel now (the first entry of its queue)."""
    try:
        from wcs.skin.refresh import changed
        changed({'widget_mytv_channels'})
    except Exception as exc:
        _log('channel row refresh failed: {}'.format(exc), xbmc.LOGWARNING)


def _item_data(item):
    props, art = item.get('properties', {}), item.get('art', {})
    return {'series_name': props.get('series_name', ''), 'season': props.get('season', ''), 'episode': props.get('episode', ''),
            'tmdb_id': props.get('tmdb_id', ''), 'episode_title': props.get('episode_title', ''), 'plot': props.get('plot', ''),
            'rating': props.get('rating', ''), 'poster': props.get('poster', ''), 'fanart': props.get('fanart', ''),
            'genre': props.get('genre', ''), 'year': props.get('year', ''), 'episode_thumb': art.get('thumb', '')}


def _seek_to_resume(channel_id, tmdb_id, season=0, episode=0):
    """Playback of a playlist item starts at 0: jump to the position the channel saved for the series (as the dialog does).
    With the progress shared with the library the position belongs to one episode: only that episode resumes."""
    if not channel_id or not tmdb_id:
        return
    try:
        from wcs.ai import ChannelHistory
        from wcs.library2 import shared
        resume = ChannelHistory.get_resume_point(channel_id, tmdb_id)
        if not resume or resume['position'] <= 10:
            return
        if shared.active('tv') and (int(resume['season']), int(resume['episode'])) != (int(season or 0), int(episode or 0)):
            return
        position = resume['position']
        monitor, player = xbmc.Monitor(), xbmc.Player()
        for _ in range(50):
            if monitor.waitForAbort(0.2):
                return
            if player.isPlaying() and player.getTime() > 0:
                player.seekTime(position)
                _log('resumed at {:.0f}s'.format(position))
                return
    except Exception as exc:
        _log('resume seek failed: {}'.format(exc), xbmc.LOGWARNING)


def play_episodes(addon, channel_id, items):
    """Play the queue entries as one playlist and keep the countdown monitor alive while it plays."""
    from wcs.ai import PlaylistBuilder
    playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
    playlist.clear()
    for item in items:
        data = _item_data(item)
        playlist.add(PlaylistBuilder.build_playlist_url(ADDON_ID, data, channel_id), PlaylistBuilder.build_playlist_listitem(data))
    xbmc.Player().play(playlist)
    from wcs.playback.PlaylistCountdownMonitor import get_playlist_monitor
    first = items[0].get('properties', {})
    if len(items) > 1:
        get_playlist_monitor().activate()
    else:
        get_playlist_monitor().activate(meta={'channel_id': channel_id, 'tmdb_id': first.get('tmdb_id', ''),
                                              'season': int(first.get('season') or 0), 'episode': int(first.get('episode') or 0)})
    _seek_to_resume(channel_id, first.get('tmdb_id', ''), first.get('season'), first.get('episode'))
    _keep_alive()


def _keep_alive(limit=6 * 3600):
    """The monitor thread of the countdown lives in this script: stay while something plays (a gap between two playlist
    items is up to ~30 s), at most 90 s before the first start, and leave when Kodi quits."""
    monitor, player = xbmc.Monitor(), xbmc.Player()
    started, idle, waited = False, 0, 0
    while waited < limit:
        if monitor.waitForAbort(2):
            return
        waited += 2
        if player.isPlaying():
            started, idle = True, 0
        else:
            idle += 2
            if idle >= (30 if started else 90):
                return


def play_tv(addon, channel_id, mode):
    from wcs.ai.MediaChannelManager import MediaChannelManager
    from wcs.caching import get_cache_manager
    channel = MediaChannelManager(addon, 'tv').get_channel_by_id(channel_id)
    if not channel or not channel.series_list:
        return _notify(addon, addon.getLocalizedString(S_EMPTY))
    cache = get_cache_manager()
    if mode == 'random':
        items = fill(addon, cache, channel, [], 1, shuffle_series=True)
    else:
        items = tv_queue(addon, cache, channel, fresh=(mode == 'live_shuffle'))
        if mode == 'first':
            items = items[:1]
    if not items:
        return _notify(addon, addon.getLocalizedString(S_EMPTY))
    _log('tv {} {}: {} item(s)'.format(channel_id, mode, len(items)))
    play_episodes(addon, channel_id, items)


# --------------------------------------------------------------------------- MyCinema

def _poster(path):
    return 'https://image.tmdb.org/t/p/w500{}'.format(path) if path and not path.startswith('http') else (path or '')


def play_cinema(addon, channel_id, mode):
    from urllib.parse import quote_plus
    from wcs.ai import ChannelHistory
    from wcs.ai.MediaChannelManager import MediaChannelManager
    manager = MediaChannelManager(addon, 'cinema')
    channel = manager.get_channel_by_id(channel_id)
    if not channel or not channel.media_list:
        return _notify(addon, addon.getLocalizedString(S_EMPTY))
    if mode == 'random':
        movie = manager.get_random_movie_for_channel(channel_id)
        if not movie:
            return _notify(addon, addon.getLocalizedString(S_EMPTY))
        ChannelHistory.mark_movie_watched(channel_id, movie.get('id'))
        from wcs.utils import play_movie_from_addon
        play_movie_from_addon({'title': movie.get('name') or movie.get('title', ''), 'year': (movie.get('release_date') or '')[:4],
                               'plot': movie.get('overview', ''), 'poster': _poster(movie.get('poster_path') or movie.get('poster')),
                               'fanart': _poster(movie.get('backdrop_path') or movie.get('fanart')), 'tmdb_id': movie.get('id'),
                               'mycinema_channel_id': channel_id}, addon)
        return
    movies = list(channel.media_list)
    random.shuffle(movies)
    playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
    playlist.clear()
    for movie in movies[:20]:
        name, movie_id = movie.get('name') or movie.get('title', ''), movie.get('id')
        if not movie_id or not name:
            continue
        year = (movie.get('release_date') or '')[:4]
        poster = _poster(movie.get('poster_path') or movie.get('poster'))
        fanart = _poster(movie.get('backdrop_path') or movie.get('fanart'))
        url = ('plugin://{}?action=play_movie_from_addon_playlist&movie_name={}&tmdb_id={}&plot={}&poster={}&fanart={}'
               '&year={}&mycinema_channel_id={}').format(ADDON_ID, quote_plus(name), movie_id, quote_plus(movie.get('overview', '')),
                                                          quote_plus(poster), quote_plus(fanart), quote_plus(year), quote_plus(channel_id))
        item = xbmcgui.ListItem(label=name)
        item.setInfo('video', {'title': name, 'plot': movie.get('overview', ''), 'year': int(year) if year.isdigit() else 0,
                               'mediatype': 'movie'})
        item.setArt({'poster': poster, 'fanart': fanart, 'thumb': poster})
        playlist.add(url, item)
    if playlist.size() == 0:
        return _notify(addon, addon.getLocalizedString(S_EMPTY))
    _log('cinema {}: {} film(s)'.format(channel_id, playlist.size()))
    xbmc.Player().play(playlist)


def run(params):
    addon = xbmcaddon.Addon(ADDON_ID)
    kind, channel_id, mode = params.get('kind', 'tv'), params.get('channel_id', ''), params.get('mode', 'live')
    try:
        if kind == 'cinema':
            play_cinema(addon, channel_id, mode)
        else:
            play_tv(addon, channel_id, mode)
    except Exception as exc:
        _log('{} {} failed: {}: {}'.format(kind, mode, type(exc).__name__, exc), xbmc.LOGERROR)
        _notify(addon, addon.getLocalizedString(S_EMPTY))
