#!/usr/bin/env python

import os
import sys
import yaml 
import json
import shutil
import filecmp
import subprocess
import argparse
import os.path
import base64
import hmac
import hashlib
import urllib
import urllib2

from sae.util import search_file_bottom_up

UPLOAD_SERVER = 'http://upload.sae.sina.com.cn'
DEPLOY_SERVER = 'http://deploy.sae.sina.com.cn'
SVN_SERVER = 'https://svn.sinaapp.com/'
LOCAL_CACHE_DIR = os.path.expanduser('~/.saecloud')

VERSION = '0.0.1'
verbose = False

def version(args):
    print "SAE command line v%s" % VERSION

def run(*args):
    # FIXME: Check the return code please
    if verbose:
        print '+', ' '.join(args)
        return subprocess.call(args)
    else:
        return subprocess.call(args, stdout=open(os.devnull, 'w'))

def _get_svn_opts(args):
    opts = []
    for opt in ['username', 'password']:
        value = getattr(args, opt)
        if value:
            opts.append('--' + opt)
            opts.append(value)
    return opts

def deploy(args):
    """Deploy local source to server

    Deploy code in source directory to sae server, by default source is current
    directory, version number is set in config.yaml. 

    """
    source = args.dir 
    opts = _get_svn_opts(args)
    cache = LOCAL_CACHE_DIR

    if source is None:
        source = search_file_bottom_up('config.yaml')
        if source is None:
            print >>sys.stderr,     \
                'error: Not an app directory(or any of the parent directories)'
            return

    conf_file = os.path.join(source, 'config.yaml')
    try:
        conf =  yaml.load(open(conf_file))
    except:
        print >>sys.stderr, 'error: Failed to load config.yaml'
        return

    name =  conf['name']
    version = conf['version']

    print 'Deploying http://%s.%s.sinaapp.com' % (version, name)
    print 'Updating cache'
    name = str(name)
    path = os.path.join(cache, name)
    if not os.path.exists(path):
        url = SVN_SERVER + name
        run('svn', 'checkout', url, path, *opts)
    else:
        run('svn', 'cleanup', path)
        run('svn', 'update', path, '-q')

    print 'Finding changes'
    modified = False
    vpath = os.path.join(path, str(version))
    if os.path.exists(vpath):
        q = ['',]
        while len(q):
            part = q.pop(0)
            s = os.path.join(source, part)
            t = os.path.join(vpath, part)
            dc = filecmp.dircmp(s, t, ['.svn'])

            # New files
            for f in dc.left_only:
                if f.startswith('.'): 
                    continue
                d1 = os.path.join(s, f)
                d2 = os.path.join(t, f)
                if os.path.isdir(d1):
                    shutil.copytree(d1, d2)
                else:
                    shutil.copy2(d1, d2)
                run('svn', 'add', d2, '-q')
                modified = True

            # Deleted files
            for f in dc.right_only:
                if f.startswith('.'): 
                    continue
                d = os.path.join(t, f)
                if os.path.isdir(d):
                    shutil.rmtree(d)
                else:
                    os.unlink(d)
                run('svn', 'delete', d, '-q')
                modified = True

            # Modified files
            for f in dc.diff_files:
                if f.startswith('.'): 
                    continue
                d1 = os.path.join(s, f)
                d2 = os.path.join(t, f)
                shutil.copy2(d1, d2)
                modified = True

            subdirs = filter(lambda x: not x.startswith('.'), dc.common_dirs)
            q.extend([os.path.join(part, d) for d in subdirs])
    else:
        # New version
        shutil.copytree(source, vpath, ignore=shutil.ignore_patterns('.*'))
        run('svn', 'add', vpath, '-q')
        modified = True

    if not modified:
        print 'No changes found'
    print 'Deploying to server... ',
    sys.stdout.flush()
    run('svn', 'commit', path, '-mx')
    print 'done'

def export(args):
    """Export source from sae server

    Export source currently deployed on the sae server to currently directory.
    Version 1 will be used unless you have specified a version number, also, 
    you can specify your svn username and password just as `saecloud depoly`

    """
    url = SVN_SERVER + args.app + '/' + args.version
    print 'Exporting to', args.app
    opts = _get_svn_opts(args)
    run('svn', 'export', url, args.app, *opts)

def install(args):
    # If we are in an app directory, try to install the package in the
    # standard directory.
    parent_dir = search_file_bottom_up('config.yaml') or os.getcwd()
    dest = os.path.join(parent_dir, 'site-packages')

    if not os.path.exists(dest):
        os.mkdir(dest)

    import tempfile
    tmpdir = tempfile.gettempdir()
    argv = ['install', '-I',
            '--install-option=--install-lib=%s' % dest,
            '--install-option=--install-data=%s' % dest,
            '--install-option=--install-scripts=%s' % tmpdir]
    # only compile if it is python2.7.3
    import imp
    magic = imp.get_magic()[:2]
    if magic != '\x03\xf3':
        argv.append('--install-option=--no-compile')
    if args.requirement:
        argv.extend(['-r', args.requirement[0]])
    argv.extend(args.package)

    # In virtualenv, the install command will remove the old installed
    # distribution, patch to skip it.
    try:
        def _(*arg, **kws):
            pass
        import pip.req
        pip.req.InstallRequirement.uninstall = _
        pip.req.InstallRequirement.commit_uninstall = _
    except:
        pass

    import pip
    pip.main(argv)

    for f in os.listdir(dest):
        pth = os.path.join(dest, f)
        if os.path.isfile(pth) and f.endswith('.egg'):
            print 'uncompress: %s' % f
            import zipfile
            zf = zipfile.ZipFile(pth)
            zf.extractall(dest)
            zf.close()
            os.unlink(pth)

HISTORY_PATH = os.path.join(LOCAL_CACHE_DIR, 'shell.hist')

def shell(args):
    app_name = args.app
    if app_name is None:
        app_root = search_file_bottom_up('config.yaml')
        try:
            path = os.path.join(app_root, 'config.yaml')
            app_name = yaml.load(open(path))['name']
        except Exception:
            pass
    if app_name is None:
        print >>sys.stderr, 'error: No app is specified'
        return

    api_url = 'http://%s.sinaapp.com/_sae/shell/rpc' % app_name

    import time
    session = None
    password = args.password and args.password[0]

    def _rpc(op, **kwargs):
        req = json.dumps({'op': op,
                          'session': session,
                          'password': password,
                          'kwargs': kwargs})
        body = urllib2.urlopen(api_url, req).read()
        return body

    import code

    try:
        import readline, atexit
        readline.parse_and_bind('tab: complete')
        atexit.register(lambda: readline.write_history_file(HISTORY_PATH))
        if os.path.exists(HISTORY_PATH):
            readline.read_history_file(HISTORY_PATH)
    except ImportError:
        pass

    try:
        session, python_version = _rpc('open').split(' ', 1)
    except Exception:
        print >>sys.stderr, \
            'error: connect with %s\'s ShellMiddleware' % app_name
        return

    banner = '''Python %s
Type "help", "copyright", "credits" or "license" for more information.
''' % python_version

    class _InteractiveConsole(code.InteractiveConsole):
        def runsource(self, source, filename="<input>", symbol="single"):
            try:
                if self.compile(source, filename, symbol) is None:
                    # Imcomplete code
                    return True
            except (OverflowError, SyntaxError, ValueError):
                # Syntax error
                self.showsyntaxerror(filename)
                return False

            try:
                self.write(_rpc('run',
                                source=source, filename=filename, mode=symbol))
            except Exception:
                self.write('error: execute code on server\n')
            return False

    _InteractiveConsole().interact(banner)

def main():

    parser = argparse.ArgumentParser(prog=os.path.basename(sys.argv[0]))
    parser.set_defaults(verbose=False)

    subparsers = parser.add_subparsers(help='sub commands')

    credentials = argparse.ArgumentParser(add_help=False)
    credentials.add_argument('-u', '--username', help='repo username')
    credentials.add_argument('-p', '--password', help='repo password')
    credentials.add_argument('-v', '--verbose', dest='verbose', action='store_true', 
                             help='show lowlevel repo operations')

    p = subparsers.add_parser('export', parents=[credentials],
                              help='export source code to local directory')
    p.add_argument('app', help='application name')
    p.add_argument('version', nargs='?', default='1', 
                   help='which version to export, default to 1')
    p.set_defaults(func=export)

    p = subparsers.add_parser('deploy', parents=[credentials], 
                              help='deploy source directory to SAE')
    p.add_argument('dir', nargs='?', default=None,
                   help='the source code directory to deploy, default to current dir')
    p.set_defaults(func=deploy)

    p = subparsers.add_parser('install',
                              help='helper to install packages for SAE application')
    p.add_argument('package', nargs='*', help='package name to install')
    p.add_argument('-r', '--requirement', nargs=1,
                   help='install all the packages listed in the given requirements file')
    p.set_defaults(func=install)

    p = subparsers.add_parser('version', help='show version info')
    p.set_defaults(func=version)

    p = subparsers.add_parser('shell', help='remote python shell runs on SAE')
    p.add_argument('app', nargs='?', default=None, help='which app\'s shell to run')
    p.add_argument('-p', '--password', nargs=1,
                   help='password you set in the ShellMiddleware')
    p.set_defaults(func=shell)

    args = parser.parse_args()
    global verbose
    if args.verbose: verbose = True

    args.func(args)

if __name__ == '__main__':
    main()
