#!/usr/bin/env python


__license__   = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import glob
import hashlib
import json
import os
import shutil
import stat
import subprocess
import sys
import time
from subprocess import check_call
from tempfile import NamedTemporaryFile, mkdtemp
from urllib.request import Request, urlopen
from zipfile import ZipFile

if __name__ == '__main__':
    d = os.path.dirname
    sys.path.insert(0, d(d(os.path.abspath(__file__))))

from setup import Command, __appname__, __version__, installer_names, manual_build_dir

DOWNLOADS = '/srv/main/downloads'
HTML2LRF = 'calibre/ebooks/lrf/html/demo'
TXT2LRF = 'src/calibre/ebooks/lrf/txt/demo'
STAGING_HOST = 'download.calibre-ebook.com'
BACKUP_HOST = 'code.calibre-ebook.com'
STAGING_USER = BACKUP_USER = 'root'
STAGING_DIR = '/root/staging'
BACKUP_DIR = '/binaries'


def installer_description(fname):
    if fname.endswith('.tar.xz'):
        return 'Source code'
    if fname.endswith('.txz'):
        return ('ARM' if 'arm64' in fname else 'AMD') + ' 64-bit Linux binary'
    if fname.endswith('.msi'):
        return 'Windows installer'
    if fname.endswith('.dmg'):
        return 'macOS dmg'
    if fname.endswith('.exe'):
        return 'Calibre Portable'
    return 'Unknown file'


def upload_signatures():
    tdir = mkdtemp()
    scp = ['scp']
    try:
        for installer in installer_names():
            if not os.path.exists(installer):
                continue
            sig = os.path.join(tdir, os.path.basename(installer + '.sig'))
            scp.append(sig)
            check_call([
                os.environ['PENV'] + '/gpg-as-kovid', '--output', sig,
                '--detach-sig', installer
            ])
            with open(installer, 'rb') as f:
                raw = f.read()
            fingerprint = hashlib.sha512(raw).hexdigest()
            sha512 = os.path.join(tdir, os.path.basename(installer + '.sha512'))
            with open(sha512, 'w') as f:
                f.write(fingerprint)
            scp.append(sha512)
        for srv in 'code main'.split():
            check_call(scp + [f'{srv}:/srv/{srv}/signatures/'])
            check_call(
                ['ssh', srv, 'chown', '-R', 'http:http', f'/srv/{srv}/signatures']
            )
    finally:
        shutil.rmtree(tdir)


class ReUpload(Command):  # {{{

    description = 'Re-upload any installers present in dist/'

    sub_commands = ['upload_installers']

    def pre_sub_commands(self, opts):
        opts.replace = True
        exists = {x for x in installer_names() if os.path.exists(x)}
        if not exists:
            print('There appear to be no installers!')
            raise SystemExit(1)

    def run(self, opts):
        for x in installer_names():
            if os.path.exists(x):
                os.remove(x)

# }}}


# Data {{{
def get_github_data():
    with open(os.environ['PENV'] + '/github-token', 'rb') as f:
        un, pw = f.read().decode('utf-8').strip().split(':')
    return {'username': un, 'password': pw}


def get_sourceforge_data():
    return {'username': 'kovidgoyal', 'project': 'calibre'}


def get_fosshub_data():
    with open(os.environ['PENV'] + '/fosshub', 'rb') as f:
        return f.read().decode('utf-8').strip()


def send_data(loc):
    subprocess.check_call([
        'rsync', '--inplace', '--delete', '-r', '-zz', '-h', '--info=progress2', '-e',
        'ssh -x', loc + '/', f'{STAGING_USER}@{STAGING_HOST}:{STAGING_DIR}'
    ])


def send_to_backup(loc):
    host = f'{BACKUP_USER}@{BACKUP_HOST}'
    dest = f'{BACKUP_DIR}/{__version__}'
    subprocess.check_call(['ssh', '-x', host, 'mkdir', '-p', dest])
    subprocess.check_call([
        'rsync', '--inplace', '--delete', '-r', '-zz', '-h', '--info=progress2', '-e',
        'ssh -x', loc + '/', f'{host}:{dest}/'
    ])


def gh_cmdline(ver, data):
    safe = [
        __appname__, ver, 'fmap', 'github', __appname__, data['username'],
    ]
    return safe + [data['password']], safe + ['PASSWORD_REDACTED']


def sf_cmdline(ver, sdata):
    return [
        __appname__, ver, 'fmap', 'sourceforge', sdata['project'], sdata['username']
    ]


def calibre_cmdline(ver):
    return [__appname__, ver, 'fmap', 'calibre']


def run_remote_upload(args, safe=None):
    print('Running remotely:', ' '.join(safe or args))
    subprocess.check_call([
        'ssh', '-x', f'{STAGING_USER}@{STAGING_HOST}', 'cd', STAGING_DIR, '&&',
        'python', 'hosting.py'
    ] + args)

# }}}


def upload_to_fosshub():
    # https://devzone.fosshub.com/dashboard/restApi
    # fosshub has no API to do partial uploads, so we always upload all files.
    api_key = get_fosshub_data()

    def request(path, data=None):
        r = Request('https://api.fosshub.com/rest/' + path.lstrip('/'),
                headers={
                    'Content-Type': 'application/json',
                    'X-auth-key': api_key,
                    'User-Agent': 'calibre'
        })
        res = urlopen(r, data=data)
        ans = json.loads(res.read())
        if ans.get('error'):
            raise SystemExit(ans['error'])
        if res.getcode() != 200:
            raise SystemExit(f'Request to {path} failed with response code: {res.getcode()}')
        # from pprint import pprint
        # pprint(ans)
        return ans['status'] if 'status' in ans else ans['data']

    print('Sending upload request to fosshub...')
    project_id = None

    for project in request('projects'):
        if project['name'].lower() == 'calibre':
            project_id = project['id']
            break
    else:
        raise SystemExit('No calibre project found')

    files = set(installer_names())
    entries = []
    for fname in files:
        desc = installer_description(fname)
        url = f'https://download.calibre-ebook.com/{__version__}/{os.path.basename(fname)}'
        entries.append({
            'fileUrl': url,
            'type': desc,
            'version': __version__,
        })
    jq = {
        'version': __version__,
        'files': entries,
        'publish': True,
        'isOldRelease': False,
    }
    data = json.dumps(jq)
    # print(data)
    data = data.encode('utf-8')
    if not request(f'projects/{project_id}/releases/', data=data):
        raise SystemExit('Failed to queue publish job with fosshub')


class UploadInstallers(Command):  # {{{

    def add_options(self, parser):
        parser.add_option(
            '--replace',
            default=False,
            action='store_true',
            help='Replace existing installers'
        )

    def run(self, opts):
        # return upload_to_fosshub()
        all_possible = set(installer_names())
        available = set(glob.glob('dist/*'))
        files = {
            x: installer_description(x)
            for x in all_possible.intersection(available)
        }
        for x in files:
            os.chmod(x, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
        sizes = {os.path.basename(x): os.path.getsize(x) for x in files}
        self.record_sizes(sizes)
        tdir = mkdtemp()
        try:
            self.upload_to_staging(tdir, files)
            self.upload_to_calibre()
            if opts.replace:
                upload_signatures()
                check_call('ssh code /apps/update-calibre-version.py'.split())
            # self.upload_to_sourceforge()
            # upload_to_fosshub()
            self.upload_to_github(opts.replace)
        finally:
            shutil.rmtree(tdir, ignore_errors=True)

    def record_sizes(self, sizes):
        print('\nRecording dist sizes')
        args = [
            f'{__version__}:{fname}:{size}'
            for fname, size in sizes.items()
        ]
        check_call(['ssh', 'code', '/usr/local/bin/dist_sizes'] + args)

    def upload_to_staging(self, tdir, files):
        os.mkdir(tdir + '/dist')
        hosting = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), 'hosting.py'
        )
        shutil.copyfile(hosting, os.path.join(tdir, 'hosting.py'))

        for f in files:
            for x in (tdir + '/dist',):
                dest = os.path.join(x, os.path.basename(f))
                shutil.copy2(f, x)
                os.chmod(
                    dest, stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH
                )

        with open(os.path.join(tdir, 'fmap'), 'wb') as fo:
            for f, desc in files.items():
                fo.write((f'{f}: {desc}\n').encode())

        while True:
            try:
                send_data(tdir)
            except Exception:
                print('\nUpload to staging failed, retrying in a minute')
                time.sleep(60)
            else:
                break

        while True:
            try:
                send_to_backup(tdir)
            except Exception:
                print('\nUpload to backup failed, retrying in a minute')
                time.sleep(60)
            else:
                break

    def upload_to_github(self, replace):
        data = get_github_data()
        args, safe = gh_cmdline(__version__, data)
        if replace:
            args = ['--replace'] + args
            safe = ['--replace'] + safe
        run_remote_upload(args, safe)

    def upload_to_sourceforge(self):
        sdata = get_sourceforge_data()
        args = sf_cmdline(__version__, sdata)
        run_remote_upload(args)

    def upload_to_calibre(self):
        run_remote_upload(calibre_cmdline(__version__))

# }}}


class UploadUserManual(Command):  # {{{
    description = 'Build and upload the User Manual'
    sub_commands = ['manual']

    def build_plugin_example(self, path):
        from calibre import CurrentDir
        with NamedTemporaryFile(suffix='.zip') as f:
            os.fchmod(
                f.fileno(), stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH |
                stat.S_IWRITE
            )
            with CurrentDir(path):
                with ZipFile(f, 'w') as zf:
                    for x in os.listdir('.'):
                        if x.endswith('.swp'):
                            continue
                        zf.write(x)
                        if os.path.isdir(x):
                            for y in os.listdir(x):
                                zf.write(os.path.join(x, y))
            bname = self.b(path) + '_plugin.zip'
            dest = f'{DOWNLOADS}/{bname}'
            subprocess.check_call(['scp', f.name, 'main:' + dest])

    def run(self, opts):
        path = self.j(self.SRC, '..', 'manual', 'plugin_examples')
        for x in glob.glob(self.j(path, '*')):
            self.build_plugin_example(x)

        srcdir = self.j(manual_build_dir(), 'en', 'html') + '/'
        check_call(
            ' '.join(
                ['rsync', '-zz', '-rl', '--info=progress2', srcdir, 'main:/srv/manual/']
            ),
            shell=True
        )
        check_call('ssh main chown -R http:http /srv/manual'.split())

# }}}


class UploadDemo(Command):  # {{{

    description = 'Rebuild and upload various demos'

    def run(self, opts):
        check_call(
            f'''ebook-convert {self.j(self.SRC, HTML2LRF)}/demo.html /tmp/html2lrf.lrf '''
            '''--title='Demonstration of html2lrf' --authors='Kovid Goyal' '''
            '''--header '''
            '''--serif-family "/usr/share/fonts/corefonts, Times New Roman" '''
            '''--mono-family  "/usr/share/fonts/corefonts, Andale Mono" ''',
            shell=True
        )

        lrf = self.j(self.SRC, 'calibre', 'ebooks', 'lrf', 'html', 'demo')
        check_call(
            f'cd {lrf} && zip -j /tmp/html-demo.zip * /tmp/html2lrf.lrf',
            shell=True
        )

        check_call(f'scp /tmp/html-demo.zip main:{DOWNLOADS}/', shell=True)

# }}}


class UploadToServer(Command):  # {{{

    description = 'Upload miscellaneous data to calibre server'

    def run(self, opts):
        check_call('scp translations/website/locales.zip main:/srv/main/'.split())
        check_call('scp translations/changelog/locales.zip main:/srv/main/changelog-locales.zip'.split())
        check_call('ssh main /apps/static/generate.py'.split())
        src_file = glob.glob('dist/calibre-*.tar.xz')[0]
        upload_signatures()
        check_call(['git', 'push'])
        check_call([
            os.environ['PENV'] + '/gpg-as-kovid', '--armor', '--yes',
            '--detach-sign', src_file
        ])
        check_call(['scp', src_file + '.asc', 'code:/srv/code/signatures/'])
        check_call('ssh code /usr/local/bin/update-calibre-code.py'.split())
        check_call(
            ('ssh code /apps/update-calibre-version.py ' + __version__).split()
        )
        check_call((
            f'ssh main /usr/local/bin/update-calibre-version.py {__version__} && /usr/local/bin/update-calibre-code.py && /apps/static/generate.py'
        ).split())

# }}}
