# -*- coding: utf-8 -*-
"""Publish committed row revisions promptly; network builders have their own queue."""
import json
import queue
import threading
import time
from datetime import date
from urllib.parse import unquote

import xbmc
import xbmcaddon
import xbmcgui

from wcs.library2 import shared
from wcs.skin import actions, bytecode, commands, performance, refresh, refresher

ADDON_ID = 'plugin.video.milionar'


PUBLISH_GAP = 1.0                         # seconds between two publications of changed rows


def names_from(data):
    try:
        value = json.loads(unquote(data or '{}'))
        names = value.get('names', []) if isinstance(value, dict) else []
        return {name for name in names if isinstance(name, str)}
    except (ValueError, TypeError):
        return set()


class Monitor(xbmc.Monitor):
    def __init__(self):
        super().__init__()
        self.events = queue.Queue()

    def onSettingsChanged(self):
        self.events.put(('settings', set()))

    def onNotification(self, sender, method, data):
        if method.startswith('Other.'):
            method = method[6:]
        if sender == ADDON_ID and method in ('skin_rows_changed', 'skin_widget_refresh',
                                             'skin_widgets_refresh', 'vinyl_refresh'):
            if method == 'skin_widget_refresh':
                try:
                    if json.loads(unquote(data or '{}')).get('force'):
                        method = 'skin_widget_force_refresh'
                except (ValueError, TypeError, AttributeError):
                    pass
            self.events.put((method, names_from(data)))


def _apply_variant():
    try:
        from wcs.skin import installer
        installer.apply_variant()
    except Exception as exc:  # the service must not stop because of the skin
        xbmc.log('[Milionář Skin] variant switch failed: {}: {}'.format(type(exc).__name__, exc), xbmc.LOGERROR)


def _build(name):
    addon = xbmcaddon.Addon(ADDON_ID)
    if name.startswith('library:'):
        from wcs.library2 import tmdb
        _prefix, kind, ident = name.split(':')
        if kind == 'tv':
            return bool(tmdb.refresh_show(int(ident)))
        if kind == 'movie':
            return bool(tmdb.movie(int(ident)))
        return False
    if name in refresh.VINYL_ROWS:
        from wcs.vinyl import widgets
        return bool(widgets.refresh(sources=[name[len('vinyl_'):]]))
    if name == shared.SYNC_JOB:
        return bool(shared.sync())
    return bool(refresher.refresh(addon, name))


def _worker(monitor, jobs, results):
    from wcs.skin import userfirst
    userfirst.background_thread()                      # its jobs step aside for the user between their steps
    while not monitor.abortRequested():
        try:
            name, generation = jobs.get(timeout=0.5)
        except queue.Empty:
            continue
        if userfirst.give_way(monitor):                # a cache build never competes with what the user started
            return
        try:
            ok = _build(name)
        except Exception as exc:
            xbmc.log('[Milionář Skin] {} failed: {}'.format(name, type(exc).__name__), xbmc.LOGWARNING)
            ok = False
        results.put((name, generation, ok))


def _properties():
    addon = xbmcaddon.Addon(ADDON_ID)
    actions.set_provider_properties(addon)
    home = xbmcgui.Window(10000)
    if performance.enabled():
        home.setProperty('milionar.low_power', 'true')
    elif home.getProperty('milionar.low_power'):
        home.clearProperty('milionar.low_power')          # empty = off: the row URLs stay unchanged at start


def _active():
    return set(refresher.active_rows())


def _due(name):
    if name in refresh.VINYL_ROWS:
        import os
        from wcs.vinyl import client
        path = client.cache_path(name[len('vinyl_'):])
        try:
            return time.time() - os.path.getmtime(path) >= 900
        except OSError:
            return True
    age = refresher.cache_age(name)
    return age is None or age >= refresher.SCHEDULE.get(name, 86400)


def run():
    monitor = Monitor()
    from wcs.skin import perflog
    perflog.start(monitor)                             # first: it times everything that follows
    jobs, results = queue.Queue(), queue.Queue()
    while not monitor.abortRequested():
        try:
            _properties()
            refresh.bootstrap()
            observed = refresh.revisions(refresh.ALL_ROWS)
            break
        except Exception as exc:
            xbmc.log('[Milionář Skin] startup deferred: ' + type(exc).__name__, xbmc.LOGWARNING)
            if monitor.waitForAbort(1):
                return
    if monitor.abortRequested():
        return
    threading.Thread(target=_worker, args=(monitor, jobs, results),
                     name='milionar-widget-builders', daemon=True).start()
    from wcs.skin import journal, warm
    refresh.service_monitor = monitor
    commands.start(monitor)
    bytecode.start(monitor)                            # once per version: what Kodi's Python does not keep
    journal.start(monitor)                             # prepares the Home rows the plugin calls hand over
    warm.start(monitor)
    requested, inflight, retry, generations = set(), {}, {}, {}
    manual, manual_failed, manual_running = {}, False, False
    last_poll, last_schedule = 0.0, 0.0
    dirty, last_publish = set(), -PUBLISH_GAP
    day = date.today()
    migration_offered = False
    shared_checked = False

    def request(names, force=False):
        for name in names:
            if (name in refresh.NETWORK_ROWS | refresh.VINYL_ROWS or name == shared.SYNC_JOB
                    or name.startswith(('library:movie:', 'library:tv:'))):
                if not force and (name in requested or name in inflight or time.monotonic() < retry.get(name, (0, 0))[1]):
                    continue
                generations[name] = generations.get(name, 0) + 1
                requested.add(name)
                if force:
                    retry.pop(name, None)

    while not monitor.abortRequested():
        try:
            settings_changed = False
            while True:
                try:
                    event, names = monitor.events.get_nowait()
                except queue.Empty:
                    break
                if event == 'skin_rows_changed':
                    from wcs.skin import perflog
                    perflog.log('change event: {}'.format(sorted(names & refresh.ALL_ROWS)))
                    dirty.update(names & refresh.ALL_ROWS)
                elif event == 'settings':
                    settings_changed = True
                elif event == 'skin_widgets_refresh':
                    refresh.changed(refresh.ALL_ROWS)
                    names = _active()
                    request(names, force=True)
                    manual = {name: generations[name] for name in names}
                    manual_failed, manual_running = False, True
                    if not names:
                        manual = {'local': 0}
                        dirty.update(refresh.ALL_ROWS)
                elif event == 'vinyl_refresh':
                    request(names or (_active() & refresh.VINYL_ROWS), force=True)
                elif event == 'skin_widget_force_refresh':
                    request(names, force=True)
                else:
                    # Repeated cache misses while a build is running are one request.
                    request(names - requested - set(inflight))
            if settings_changed:
                _properties()
                request(_active(), force=True)
                _apply_variant()          # a new choice of look (Klasický / Moderní) switches the running skin
            if settings_changed or not shared_checked:
                # the MyTV "shared progress" switch changed (or its first run): merge the channels and the store
                shared_checked = True
                if shared.pending():
                    request([shared.SYNC_JOB], force=True)
            now = time.monotonic()
            if day != date.today():
                day = date.today()
                refresh.changed(refresh.SERIES_ROWS | {'widget_mytv_next'})
            if now - last_poll >= 5:
                values = refresh.revisions(refresh.ALL_ROWS)
                dirty.update(row for row, value in values.items() if observed.get(row) != value)
                observed = values
                last_poll = now
            if dirty and now - last_publish >= PUBLISH_GAP:
                # one change shows at once; a burst (metadata of many titles arriving) reloads the rows once per gap
                changed, dirty, last_publish = dirty, set(), now
                refresh.publish(changed)
                observed.update(refresh.revisions(changed))
            while True:
                try:
                    name, generation, ok = results.get_nowait()
                except queue.Empty:
                    break
                inflight.pop(name, None)
                if ok:
                    retry.pop(name, None)
                if manual.get(name, generation + 1) <= generation:
                    manual.pop(name)
                    manual_failed = manual_failed or not ok
                if not ok and generations.get(name) == generation:
                    attempts = retry.get(name, (0, 0))[0] + 1
                    retry[name] = (attempts, now + min(900, 5 * 2 ** min(attempts, 8)))
            if now - last_schedule >= 15:
                if not xbmc.getCondVisibility('Player.HasVideo'):
                    for name, (_attempts, due) in tuple(retry.items()):
                        if now >= due and name not in inflight and name not in requested:
                            generations[name] = generations.get(name, 0) + 1
                            requested.add(name)
                    for name in _active():
                        if _due(name) and name not in inflight and name not in requested:
                            attempts, due = retry.get(name, (0, 0))
                            if now >= due:
                                generations[name] = generations.get(name, 0) + 1
                                requested.add(name)
                last_schedule = now
            for name in tuple(requested):
                if name not in inflight:
                    generation = generations[name]
                    inflight[name] = generation
                    requested.remove(name)
                    jobs.put((name, generation))
            if manual == {'local': 0}:
                manual.clear()
            if manual_running and not manual:
                # Finished builders have already committed their revisions before returning.
                refresh.publish()
                home = xbmcgui.Window(10000)
                home.clearProperty('milionar.widgets.refreshing')
                home.setProperty('milionar.widgets.refreshed', str(time.time()))
                text = xbmcaddon.Addon(ADDON_ID).getLocalizedString(32651 if manual_failed else 32536)
                xbmc.executebuiltin('Notification(Milionář,{})'.format(text))
                manual_failed, manual_running = False, False
            if not migration_offered:
                if (not xbmc.getCondVisibility('System.HasActiveModalDialog')
                        and not xbmc.getCondVisibility('Player.HasVideo')
                        and not xbmcgui.Window(10000).getProperty('milionar.skin.installing')):
                    from wcs.library2 import migration
                    pending = migration.pending()
                    migration_offered = True
                    if pending:
                        xbmc.executebuiltin('RunPlugin(plugin://{}/?action=library_migrate&auto=1)'.format(ADDON_ID))
        except Exception as exc:
            xbmc.log('[Milionář Skin] publication deferred: ' + type(exc).__name__, xbmc.LOGWARNING)
        if monitor.waitForAbort(0.25):
            break
