# -*- coding: utf-8 -*-
"""Check for a new version of the addon and offer to install it (docs/plan/skin-milionar/250-update.md).

The skin's settings menu (window 1108) opens window 1124 and asks for `action=skin_update`; the window draws what the
Home properties milionar.update.* say. The check reads the index of the repository the addon is published to and
compares versions. Installing is Kodi's own job: `UpdateAddonRepos` makes Kodi refresh its repositories and, with
automatic updates on, install the new version; otherwise Kodi's list of available updates opens. Kodi stops and starts
the service again while it installs, so the new service finishes the window (`resume`).

A development workspace (a git checkout of the addon) never installs: an update from the repository would replace it.
"""
import json
import os
import re
import time
import xml.etree.ElementTree as ET

import xbmc
import xbmcaddon
import xbmcgui
import xbmcvfs

ADDON_ID = 'plugin.video.milionar'
REPO_ID = 'repository.milionar'
REPO_INDEX = 'https://hosting.pilsfree.net/chudy/kodi/repo/addons.xml'     # when the repository addon is not there
WINDOW = 1124
PREFIX = 'milionar.update.'
# the window's fields; `next` is what its main button does (install, retry, open), `target` the version Kodi installs
FIELDS = ('state', 'installed', 'available', 'headline', 'text', 'primary', 'next', 'secondary', 'busy', 'target')
TIMEOUT_S = 15
INSTALL_WAIT_S = 180                     # Kodi had this long to install before the window offers its list of updates
AUTO_INSTALL = 0                         # Kodi setting general.addonupdates: 0 install, 1 notify, 2 never
UPDATES_LIST = 'ActivateWindow(AddonBrowser,addons://outdated/,return)'

S_CHECKING, S_AVAILABLE, S_CURRENT, S_INSTALLING, S_DONE, S_FAILED = 32760, 32761, 32762, 32763, 32764, 32765
T_CURRENT, T_INSTALLING, T_DONE, T_FAILED, T_MANUAL, T_DEV, T_NO_REPO, T_NEWS, T_NOT_INSTALLED = (
    32766, 32767, 32768, 32769, 32770, 32771, 32772, 32778, 32779)
B_INSTALL, B_LATER, B_RETRY, B_OPEN, B_CLOSE = 32773, 32774, 32775, 32776, 32777


def _home():
    return xbmcgui.Window(10000)


def _get(name):
    return _home().getProperty(PREFIX + name)


def _show(**values):
    """Set the window's state: every field not given is cleared."""
    home = _home()
    for name in FIELDS:
        if name == 'target' and name not in values:
            continue                                  # an install in progress keeps its target across states
        home.setProperty(PREFIX + name, str(values.get(name, '')))
    if values.get('primary') and xbmc.getCondVisibility('Window.IsActive({})'.format(WINDOW)):
        xbmc.executebuiltin('SetFocus(11)')           # the button that goes on, as soon as there is one


def _text(addon, string_id, *args):
    text = addon.getLocalizedString(string_id)
    return text.format(*args) if args else text


def version_key(version):
    return tuple(int(part) for part in re.findall(r'\d+', version or ''))


def is_workspace(addon):
    """A git checkout: the development tree, which an update from the repository would replace."""
    return os.path.isdir(os.path.join(xbmcvfs.translatePath(addon.getAddonInfo('path')), '.git'))


def _rpc(method, params):
    request = {'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}
    return json.loads(xbmc.executeJSONRPC(json.dumps(request))).get('result') or {}


def _repository_installed():
    return bool(_rpc('Addons.GetAddonDetails', {'addonid': REPO_ID, 'properties': ['enabled']}).get('addon'))


def _auto_install():
    return _rpc('Settings.GetSettingValue', {'setting': 'general.addonupdates'}).get('value') == AUTO_INSTALL


def _index_url():
    """The <info> of the installed repository addon (it knows where it lives), else the published one."""
    try:
        path = xbmcvfs.translatePath('special://home/addons/{}/addon.xml'.format(REPO_ID))
        info = ET.parse(path).getroot().find(".//extension[@point='xbmc.addon.repository']/dir/info")
        if info is not None and (info.text or '').startswith('https://'):
            return info.text.strip()
    except (OSError, ET.ParseError):
        pass
    return REPO_INDEX


def published():
    """(version, news) of the addon in the repository index; raises on a network or format error."""
    import requests
    response = requests.get(_index_url(), timeout=TIMEOUT_S, headers={'Cache-Control': 'no-cache'})
    response.raise_for_status()
    root = ET.fromstring(response.content)
    addon = next((node for node in root.iter('addon') if node.get('id') == ADDON_ID), None)
    if addon is None or not addon.get('version'):
        raise ValueError('{} is not in the repository index'.format(ADDON_ID))
    news = addon.find(".//extension[@point='xbmc.addon.metadata']/news")
    return addon.get('version'), ' '.join((news.text or '').split()) if news is not None else ''


def check(addon=None):
    """Look for a new version and show the result in window 1124."""
    addon = addon or xbmcaddon.Addon(ADDON_ID)
    installed = addon.getAddonInfo('version')
    _show(state='checking', installed=installed, headline=_text(addon, S_CHECKING), busy='true',
          secondary=_text(addon, B_CLOSE))
    try:
        available, news = published()
    except Exception as exc:
        xbmc.log('[Milionář Update] check failed: {}: {}'.format(type(exc).__name__, exc), xbmc.LOGWARNING)
        _show(state='failed', installed=installed, headline=_text(addon, S_FAILED), text=_text(addon, T_FAILED),
              primary=_text(addon, B_RETRY), next='retry', secondary=_text(addon, B_CLOSE))
        return
    if version_key(available) <= version_key(installed):
        _show(state='current', installed=installed, headline=_text(addon, S_CURRENT),
              text=_text(addon, T_CURRENT, installed), secondary=_text(addon, B_CLOSE))
        return
    if is_workspace(addon):
        text, primary = _text(addon, T_DEV), ''
    elif not _repository_installed():
        text, primary = _text(addon, T_NO_REPO), ''
    else:
        text, primary = news or _text(addon, T_NEWS), _text(addon, B_INSTALL)
    _show(state='available', installed=installed, available=available, headline=_text(addon, S_AVAILABLE), text=text,
          primary=primary, next='install' if primary else '', secondary=_text(addon, B_LATER))


def install(addon=None):
    """Let Kodi install the version the window offers; wait for it while this service still runs."""
    addon = addon or xbmcaddon.Addon(ADDON_ID)
    if is_workspace(addon):                           # never: the window offers no install here, a stale click neither
        xbmc.log('[Milionář Update] development workspace: no update from the repository', xbmc.LOGWARNING)
        return
    installed, available = addon.getAddonInfo('version'), _get('available')
    if not _auto_install():
        xbmc.executebuiltin('UpdateAddonRepos')
        _show(state='installing', installed=installed, available=available, headline=_text(addon, S_INSTALLING),
              text=_text(addon, T_MANUAL), busy='true', secondary=_text(addon, B_CLOSE), target=available)
        xbmc.Monitor().waitForAbort(4)               # the repository is read again before its list opens
        _open_updates()
        return
    _show(state='installing', installed=installed, available=available, headline=_text(addon, S_INSTALLING),
          text=_text(addon, T_INSTALLING), busy='true', secondary=_text(addon, B_CLOSE), target=available)
    xbmc.log('[Milionář Update] asking Kodi to install {} (installed {})'.format(available, installed), xbmc.LOGINFO)
    xbmc.executebuiltin('UpdateAddonRepos')
    # Kodi stops this service when it installs; while it still runs, Kodi has not done it (yet)
    monitor = xbmc.Monitor()
    deadline = time.time() + INSTALL_WAIT_S
    while time.time() < deadline:
        if monitor.waitForAbort(2) or _get('state') != 'installing':
            return
    _show(state='available', installed=installed, available=available, headline=_text(addon, S_AVAILABLE),
          text=_text(addon, T_NOT_INSTALLED), primary=_text(addon, B_OPEN), next='open', secondary=_text(addon, B_CLOSE))


def _open_updates():
    xbmc.executebuiltin('Dialog.Close({})'.format(WINDOW))
    xbmc.executebuiltin(UPDATES_LIST)


def resume(addon=None, monitor=None):
    """Service start: the new version Kodi just installed finishes the window the old one left installing."""
    addon = addon or xbmcaddon.Addon(ADDON_ID)
    target = _get('target')
    if not target:
        return
    installed = addon.getAddonInfo('version')
    if version_key(installed) < version_key(target):
        _show()                                       # the service started again without the update: forget it
        _home().clearProperty(PREFIX + 'target')
        return
    monitor = monitor or xbmc.Monitor()
    # the skin installer may reload the skin first; the window opens on a quiet Home
    while _home().getProperty('milionar.skin.installing') or not xbmc.getCondVisibility('Window.IsActive(home)'):
        if monitor.waitForAbort(1):
            return
    _show(state='done', installed=installed, headline=_text(addon, S_DONE), text=_text(addon, T_DONE, installed),
          secondary=_text(addon, B_CLOSE))
    _home().clearProperty(PREFIX + 'target')
    if not xbmc.getCondVisibility('Window.IsActive({})'.format(WINDOW)):
        xbmc.executebuiltin('ActivateWindow({})'.format(WINDOW))


def run(params):
    """The window's actions: `do=check` (it opens), `do=primary` (its main button)."""
    addon = xbmcaddon.Addon(ADDON_ID)
    step = params.get('do', 'check')
    if step == 'check':
        if not xbmc.getCondVisibility('Window.IsActive({})'.format(WINDOW)):
            xbmc.executebuiltin('ActivateWindow({})'.format(WINDOW))
        check(addon)
    elif step == 'primary':
        action = _get('next')
        if action == 'retry':
            check(addon)
        elif action == 'open':
            _open_updates()
        elif action == 'install':
            install(addon)
