# -*- coding: utf-8 -*-
"""Pro tebe in the service: when to rebuild the snapshot, and the manual rebuild (docs/plan/pro-tebe/030 §2).

A rebuild runs when there is no snapshot, the day or the time-of-day slot changed, it is older than REBUILD_AFTER, or
the household's signals changed (a play ended, feedback) and the last build is at least SIGNAL_GAP old. One build at a
time (a lock file), rows are told through the row revisions (wcs/skin/refresh.py), never by touching controls.
"""
import os
import threading
import time
from datetime import datetime

import xbmc

FIRST_DELAY = 45                 # let Home and the other service jobs start first
CHECK_EVERY = 300
REBUILD_AFTER = 6 * 3600
SIGNAL_GAP = 20 * 60
LOCK_STALE = 15 * 60
_lock = threading.Lock()


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


def _lock_path():
    from wcs.library2 import store
    return os.path.join(os.path.dirname(store.db_path()), 'foryou.lock')


def _acquire():
    """Cross-process lock (service and a manual rebuild from the plugin)."""
    path = _lock_path()
    try:
        if os.path.exists(path) and time.time() - os.path.getmtime(path) > LOCK_STALE:
            os.remove(path)                          # left behind by a crash
        fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
        os.write(fd, str(os.getpid()).encode())
        os.close(fd)
        return True
    except OSError:
        return False


def _release():
    try:
        os.remove(_lock_path())
    except OSError:
        pass


def signals_stamp():
    """Changes when the household's signals change: plays, finished plays, feedback, the lists."""
    from wcs.library2 import store
    conn = store.connect()
    try:
        plays = conn.execute('SELECT COUNT(*), MAX(COALESCE(ended_at, started_at)) FROM play_log').fetchone()
        feedback = conn.execute('SELECT COUNT(*), MAX(at) FROM feedback').fetchone()
        lists = conn.execute('SELECT SUM(in_list), MAX(list_added_at) FROM titles').fetchone()
    finally:
        conn.close()
    return '{}:{}|{}:{}|{}:{}'.format(*(tuple(plays) + tuple(feedback) + tuple(lists)))


def build_now(reason=''):
    """Build and store a new snapshot, then tell the rows. Returns the snapshot, or None (busy / failed)."""
    from wcs.foryou import engine, widgets
    if not _lock.acquire(False):
        return None
    try:
        if not _acquire():
            _log('rebuild skipped: another build is running')
            return None
        try:
            started = time.time()
            stamp = signals_stamp()
            try:
                snapshot = engine.build(log=_log)
            except engine.NotEnough as exc:            # Webshare answered badly: the rows keep the last good page
                _log('rebuild kept the old snapshot: {}'.format(exc), xbmc.LOGWARNING)
                return None
            snapshot['signals'] = stamp
            engine.save(snapshot)
            from wcs.skin import refresh
            refresh.changed(widgets.ALL_ROWS)
            _log('snapshot built ({}): {} titles, {} rows, {:.1f} s, TMDb {}'.format(
                reason or 'manual', len(snapshot['items']), len(snapshot['modes']['all']), time.time() - started,
                snapshot['stats']['tmdb']))
            return snapshot
        finally:
            _release()
    except Exception as exc:
        _log('rebuild failed: {}: {}'.format(type(exc).__name__, exc), xbmc.LOGERROR)
        return None
    finally:
        _lock.release()


def due(snapshot, now=None):
    """Why a rebuild is due ('' when it is not)."""
    from wcs.foryou import compose
    now = now or time.time()
    if not snapshot:
        return 'missing'
    age = now - int(snapshot.get('built_at') or 0)
    moment = datetime.fromtimestamp(now)
    if snapshot.get('day') != moment.date().isoformat():
        return 'new day'
    if snapshot.get('slot') != compose.slot_of(moment.hour):
        return 'time of day'
    if age >= REBUILD_AFTER:
        return 'age'
    if age >= SIGNAL_GAP and snapshot.get('signals') != signals_stamp():
        return 'signals'
    return ''


IDLE_BEFORE_REBUILD = 120


def busy():
    """Not now: a video is playing (no network next to the stream), or the user is moving around Pro tebe (the rows
    must not reshuffle under the cursor). The rebuild waits for the next check."""
    if xbmc.getCondVisibility('Player.HasVideo'):
        return True
    browsing = xbmc.getCondVisibility('Window.IsActive(home) + String.IsEqual(Container(9000).ListItem.Property(id),'
                                      'mil_foryou)')
    return browsing and not xbmc.getCondVisibility('System.IdleTime({})'.format(IDLE_BEFORE_REBUILD))


def run(monitor=None):
    """Service thread."""
    from wcs.foryou import engine
    from wcs.skin import userfirst
    userfirst.background_thread()                    # the rebuild steps aside for the user between its phases
    monitor = monitor or xbmc.Monitor()
    if monitor.waitForAbort(FIRST_DELAY):
        return
    while not monitor.abortRequested():
        try:
            from wcs.skin import warm
            reason = None if warm.hidden('foryou') else due(engine.load())   # switched off: nothing is built
            if reason and (reason == 'missing' or not busy()):
                build_now(reason)
        except Exception as exc:
            _log('check failed: {}: {}'.format(type(exc).__name__, exc), xbmc.LOGWARNING)
        if monitor.waitForAbort(CHECK_EVERY):
            return


def manual(params=None):
    """Plugin action `foryou_build`: rebuild now (the empty-state button). A first build with the Webshare check
    takes a while, so it only tells the user; the rows refresh by themselves when it is done."""
    import xbmcaddon
    import xbmcgui
    from wcs.foryou.widgets import S_SECTION, _text
    addon = xbmcaddon.Addon('plugin.video.milionar')

    def note(string_id, fallback):
        xbmcgui.Dialog().notification(_text(S_SECTION), addon.getLocalizedString(string_id) or fallback,
                                      xbmcgui.NOTIFICATION_INFO, 4000, False)
    note(35328, 'Připravuji doporučení…')
    snapshot = build_now('manual')
    note(35329 if snapshot else 35330, 'Doporučení jsou připravená' if snapshot else 'Doporučení se teď nepodařilo připravit')
