# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4


from contextlib import closing
import json
import os
import tarfile
from threading import Thread
import urllib.request, urllib.error, urllib.parse
import shutil
import subprocess
import sys
import time

import ed25519
import ox
from oxtornado import actions

import settings

import logging

logger = logging.getLogger(__name__)


ENCODING='base64'

def verify(release):
    vk = ed25519.VerifyingKey(settings.OML_UPDATE_KEY, encoding=ENCODING)
    value = []
    for module in sorted(release['modules']):
        value += [str('%s/%s' % (release['modules'][module]['version'], release['modules'][module]['sha1']))]
    value = '\n'.join(value)
    value = value.encode()
    sig = release['signature'].encode()
    try:
        vk.verify(sig, value, encoding=ENCODING)
    except ed25519.BadSignatureError:
        return False
    return True

def get(url, filename=None):
    request = urllib.request.Request(url, headers={
        'User-Agent': settings.USER_AGENT
    })
    with closing(urllib.request.urlopen(request)) as u:
        if not filename:
            data = u.read()
            return data
        else:
            dirname = os.path.dirname(filename)
            if dirname and not os.path.exists(dirname):
                os.makedirs(dirname)
            with open(filename, 'wb') as fd:
                data = u.read(4096)
                while data:
                    fd.write(data)
                    data = u.read(4096)

def check():
    if settings.release:
        release_data = get(settings.server.get('release_url',
            'http://downloads.openmedialibrary.com/release.json'))
        release = json.loads(release_data.decode('utf-8'))
        old = current_version('openmedialibrary')
        new = release['modules']['openmedialibrary']['version']
        return verify(release) and old < new
    return False

def current_version(module):
    if 'modules' in settings.release \
        and module in settings.release['modules'] \
        and 'version' in settings.release['modules'][module]:
        version = settings.release['modules'][module]['version']
    else:
        version = ''
    return version

def get_latest_release():
    release_data = get(settings.server.get('release_url'))
    release = json.loads(release_data.decode('utf-8'))
    if verify(release):
        with open(os.path.join(settings.updates_path, 'release.json'), 'wb') as fd:
            fd.write(release_data)
        return release

def download():
    if not os.path.exists(os.path.join(settings.config_path, 'release.json')):
        return True
    release = get_latest_release()
    if release:
        ox.makedirs(settings.updates_path)
        os.chdir(os.path.dirname(settings.base_dir))
        current_files = {'release.json'}
        for module in release['modules']:
            if release['modules'][module]['version'] > current_version(module):
                module_tar = os.path.join(settings.updates_path, release['modules'][module]['name'])
                base_url = settings.server.get('release_url').rsplit('/', 1)[0]
                url = '/'.join([base_url, release['modules'][module]['name']])
                if not os.path.exists(module_tar):
                    logger.debug('download', os.path.basename(module_tar))
                    get(url, module_tar)
                    if ox.sha1sum(module_tar) != release['modules'][module]['sha1']:
                        logger.debug('invalid checksum', os.path.basename(module_tar))
                        os.unlink(module_tar)
                        return False
                current_files.add(os.path.basename(module_tar))
        for f in set(next(os.walk(settings.updates_path))[2])-current_files:
            os.unlink(os.path.join(settings.updates_path, f))
        return True
    return True

def install():
    if not os.path.exists(os.path.join(settings.updates_path, 'release.json')):
        return True
    if not os.path.exists(os.path.join(settings.config_path, 'release.json')):
        return True
    with open(os.path.join(settings.updates_path, 'release.json')) as fd:
        release = json.load(fd)
    old_version = current_version('openmedialibrary')
    new_version = release['modules']['openmedialibrary']['version']
    if verify(release) and old_version < new_version:
        os.chdir(os.path.dirname(settings.base_dir))
        for module in release['modules']:
            if release['modules'][module]['version'] > current_version(module):
                module_tar = os.path.join(settings.updates_path, release['modules'][module]['name'])
                if os.path.exists(module_tar) and ox.sha1sum(module_tar) == release['modules'][module]['sha1']:
                    #tar fails if old platform is moved before extract
                    new = '%s_new' % module
                    ox.makedirs(new)
                    os.chdir(new)
                    tar = tarfile.open(module_tar)
                    tar.extractall()
                    tar.close()
                    os.chdir(os.path.dirname(settings.base_dir))
                    module_old = '%s_old' % module
                    if os.path.exists(module):
                        shutil.move(module, module_old)
                    shutil.move(os.path.join(new, module), module)
                    if os.path.exists(module_old):
                        shutil.rmtree(module_old)
                    shutil.rmtree(new)
                else:
                    os.unlink(module_tar)
                    return False
        shutil.copy(os.path.join(settings.updates_path, 'release.json'), os.path.join(settings.config_path, 'release.json'))
        for cmd in [
                ['./ctl', 'stop'],
                ['./ctl', 'postupdate', '-o', old_version, '-n', new_version]
            ]:
            subprocess.call(cmd)
        upgrade_app()
        return True
    return True

def update_available():
    db_version = settings.server.get('db_version', 0)
    if db_version < settings.DB_VERSION:
        return True
    if not os.path.exists(os.path.join(settings.updates_path, 'release.json')):
        return False
    if not os.path.exists(os.path.join(settings.config_path, 'release.json')):
        return False
    with open(os.path.join(settings.updates_path, 'release.json')) as fd:
        release = json.load(fd)
    old_version = current_version('openmedialibrary')
    new_version = release['modules']['openmedialibrary']['version']
    return verify(release) and old_version < new_version

def restart_oml(update=False):
    if update:
        get_latest_release()
    subprocess.Popen([os.path.join(settings.base_dir, 'ctl'), 'restart'],
        close_fds=True, start_new_session=True)

def get_app_version(app):
    plist = app + '/Contents/Info.plist'
    if os.path.exists(plist):
        cmd = ['defaults', 'read', plist, 'CFBundleShortVersionString']
        return subprocess.check_output(cmd).strip()

def upgrade_app():
    if sys.platform == 'darwin':
        base = os.path.dirname(settings.base_dir)
        bundled_app = os.path.join(base, 'platform/Darwin/Applications/Open Media Library.app')
        app = '/Applications/Open Media Library.app'
        version = get_app_version(app)
        current_version = get_app_version(bundled_app)
        if version and current_version and version != current_version:
            try:
                shutil.rmtree(app)
                shutil.copytree(bundled_app, app)
            except:
                logger.debug('Failed to update Application', exc_info=1)

def getVersion(data):
    '''
        check if new version is available
    '''
    response = {
        'current': settings.MINOR_VERSION,
        'upgrade': False,
    }
    if settings.MINOR_VERSION == 'git':
        '''
        cmd = ['git', 'rev-parse', '@']
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, close_fds=True)
        stdout, stderr = p.communicate()
        current = stdout.strip()
        cmd = ['git', 'ls-remote', 'origin', '-h', 'refs/heads/master']
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, close_fds=True)
        stdout, stderr = p.communicate()
        new = stdout.strip()[:40]
        response['update'] = len(new) == 40 and current != new
        '''
        response['update'] = False
    else:
        get_latest_release()
        if not os.path.exists(os.path.join(settings.updates_path, 'release.json')):
            return response
        if not os.path.exists(os.path.join(settings.config_path, 'release.json')):
            return response
        with open(os.path.join(settings.updates_path, 'release.json')) as fd:
            release = json.load(fd)
        current = current_version('openmedialibrary')
        response['current'] = current
        new = release['modules']['openmedialibrary']['version']
        response['new'] = new
        response['update'] = current < new
    return response
actions.register(getVersion, cache=False)

def restart(data):
    '''
        restart (and upgrade if upgrades are available)
    '''
    restart_oml(data.get('update'))
    return {}
actions.register(restart, cache=False)


class Update(Thread):

    def __init__(self):
        Thread.__init__(self)
        self.daemon = True
        self.start()

    def status(self, status, reload=False):
        from websocket import trigger_event
        trigger_event('updatestatus', {
            'reload': reload,
            'status': status,
        })

    def install(self):
        while update_available():
            self.status('Downloading new version...')
            while not download():
                self.status('Download failed... (try again in 10 seconds)')
                time.sleep(10)
                self.status('Downloading new version...')
            self.status('Installing new version...')
            # install right now calls stop!
            '''
            if not install():
                self.status('Installation failed...')
            '''
            restart_oml()

    def update_database(self):
        db_version = settings.server.get('db_version', 0)
        if db_version < settings.DB_VERSION:
            self.status('Migrating database...')
            time.sleep(1)
            settings.server['db_version'] = settings.DB_VERSION

    def run(self):
        self.status('Checking for Updates...')
        self.update_database()
        self.install()
        self.status('Restarting...', True)
        restart_oml()