#!/usr/bin/env python

"""Usage: deis <command> [--formation=<formation>] [<args>...]

Options:
  -h --help       Show this help screen
  -v --version    Show the CLI version

Auth commands:

  register      register a new user with a controller
  login         login to a controller
  logout        logout from the current controller

Common commands:

  create        create a new container formation
  scale         scale backends, proxies or container types
  rotate        rotate in fresh backends, proxies or containers
  balance       rebalance and converge the container formation
  destroy       destroy a container formation
  
Use `deis help [subcommand]` to learn about these subcommands:
  
  formations    manage container formations
  containers    manage the containers running on backends
  backends      manage the backend nodes hosting containers
  proxies       manage the proxy nodes routing to containers
  nodes         manage nodes of all types
  
  flavors       create and manage node flavors
  providers     create and manage cloud provider credentials

  config        list, set and unset environment variables for a formation
  builds        list and manage git-push builds for a formation
  releases      list and show a formation's release history

Use `git push deis master` to deploy to the container formation.

"""

from cookielib import MozillaCookieJar
from getpass import getpass
import json
import os.path
import glob
import re
import sys
import urlparse

from docopt import docopt
from docopt import DocoptExit
import requests
import subprocess
import yaml


__version__ = '0.0.1'


USAGE = {
    'auth:register': """Usage: deis auth:register <controller> [--username=<username> --password=<password> --email=<email>]
""",
    'backends:list': """Usage: deis backends:list
""",
    'balance': """Usage: deis balance
""",
    'builds:create': """Usage: deis builds:create - [--formation=<formation>]
""",
    'builds:list': """Usage: deis builds:list
""",
    'config:list': """Usage: deis config:list
""",
    'config:set': """Usage: deis config:set <var>=<value>...
""",
    'config:unset': """Usage: deis config:unset <key>...
""",
    'containers:list': """Usage: deis containers:list
""",
    'calculate': """Usage: deis calculate
""",
    'create': """Usage: deis create --flavor=<flavor> --image=<image> [--id=<id>]
""",
    'converge': """Usage: deis converge
""",
    'destroy': """Usage: deis destroy [<formation>] [--confirm=<confirm>]
""",
    'flavors:create': """Usage: deis flavors:create --id=<id> --provider=<provider> --params=<params> [options]

Options:

--params=PARAMS    provider-specific parameters (size, region, zone, etc.)
--init=INIT        override Ubuntu cloud-init with custom YAML

SSH Options:

--ssh_username=USERNAME         username for ssh connections [default: ubuntu]
--ssh_private_key=PRIVATE_KEY   private key for ssh comm (default: auto-gen)
--ssh_public_key=PUBLIC_KEY     public key for ssh comm (default: auto-gen)

""",
    'flavors:delete': """Usage: deis flavors:delete <id>
""",
    'flavors:info': """Usage: deis flavors:info <flavor>
""",
    'flavors:list': """Usage: deis flavors:list
""",
    'formations:create': """Usage: deis formations:create --flavor=<flavor> --image=<image> [--id=<id>]
""",
    'formations:list': """Usage: deis formations:list
""",
    'formations:destroy': """Usage: deis formations:destroy [<formation>] [--confirm=<confirm>]
""",
    'formations:scale': """Usage: deis formations:scale <type=num>...
""",
    'formations:converge': """Usage: deis formations:converge
""",
    'formations:calculate': """Usage: deis formations:calculate
""",
    'formations:balance': """Usage: deis formations:balance
""",
    'keys:add': """Usage: deis keys:add [<key>]
""",
    'keys:list': """Usage: deis keys:list
""",
    'keys:remove': """Usage: deis keys:remove <key>
""",
    'login': """Usage: deis login <controller> [--username=<username> --password=<password>]
""",
    'logout': """Usage: deis logout
""",
    'nodes:info': """Usage: deis nodes:info <node>
""",
    'nodes:list': """Usage: deis nodes:list
""",
    'nodes:delete': """Usage: deis nodes:delete --id=<id>
""",
    'providers:create': """Usage: deis providers:create --type=<type> [--id=<id> --creds=<creds>]
""",
    'providers:list': """Usage: deis providers:list
""",
    'providers:info': """Usage: deis providers:info <provider>
""",
    'proxies:list': """Usage: deis proxies:list
""",
    'register': """Usage: deis register <controller> [--username=<username> --password=<password> --email=<email>]
""",
    'scale': """Usage: deis scale <type=num>...
""",
}


class Session(requests.Session):

    def __init__(self):
        super(Session, self).__init__()
        self.trust_env = False
        cookie_file = os.path.expanduser('~/.deis/cookies.txt')
        cookie_dir = os.path.dirname(cookie_file)
        self.cookies = MozillaCookieJar(cookie_file)
        # Create the $HOME/.deis dir if it doesn't exist
        if not os.path.isdir(cookie_dir):
            os.mkdir(cookie_dir, 0700)
        # Load existing cookies if the cookies.txt exists
        if os.path.isfile(cookie_file):
            self.cookies.load()
            self.cookies.clear_expired_cookies()

    def _get_formation(self):
        # TODO: determine the user's current formation from files in .git
        # in the current working directory.
        try:
            git_root = subprocess.check_output(
                ['git', 'rev-parse', '--show-toplevel']).strip('\n')
        except subprocess.CalledProcessError as e:
            raise EnvironmentError('Current directory is not a git repository')
        # try to match a deis remote
        remotes = subprocess.check_output(['git', 'remote', '-v'],
                                          cwd=git_root)
        m = re.match(r'^deis\W+(?P<url>\S+)\W+\(', remotes, re.MULTILINE)
        if not m:
            raise EnvironmentError(
                'Could not find deis remote in `git remote -v`')
        url = m.groupdict()['url']
        m = re.match('\S+:(?P<formation>[a-z0-9-]+)(.git)?', url)
        if not m:
            raise EnvironmentError('Could not parse: {url}'.format(**locals()))
        return m.groupdict()['formation']

    formation = property(_get_formation)

    def request(self, *args, **kwargs):
        for cookie in self.cookies:
            if cookie.name == 'csrftoken':
                if 'headers' in kwargs:
                    kwargs['headers']['X-CSRFToken'] = cookie.value
                else:
                    kwargs['headers'] = {'X-CSRFToken': cookie.value}
                break
        response = super(Session, self).request(*args, **kwargs)
        self.cookies.save()
        return response


class Settings(dict):

    def __init__(self):
        path = os.path.expanduser('~/.deis')
        if not os.path.exists(path):
            os.mkdir(path)
        self._path = os.path.join(path, 'client.yaml')
        if not os.path.exists(self._path):
            with open(self._path, 'w') as f:
                f.write(yaml.safe_dump({}))
        # load initial settings
        self.load()

    def load(self):
        with open(self._path) as f:
            data = f.read()
        settings = yaml.safe_load(data)
        self.update(settings)
        return settings

    def save(self):
        data = yaml.safe_dump(dict(self))
        with open(self._path, 'w') as f:
            f.write(data)
        return data


def dictify(args):
    """Converts a list of key=val strings into a python dict.

    >>> dictify(['MONGODB_URL=http://mongolabs.com/test', 'scale=5'])
    {'MONGODB_URL': 'http://mongolabs.com/test', 'scale': 5}
    """
    data = {}
    for arg in args:
        try:
            var, val = arg.split('=')
        except ValueError:
            raise DocoptExit()
        # Try to coerce the value to an int since that's a common use case
        try:
            data[var] = int(val)
        except ValueError:
            data[var] = val
    return data


class DeisClient(object):

    """
    A client which interacts with a Deis server.
    """

    def __init__(self):
        self._session = Session()
        self._settings = Settings()

    def auth_register(self, args):
        """Register a user at a controller."""
        controller = args['<controller>']
        username = args.get('--username')
        if not username:
            username = raw_input('username: ')
        password = args.get('--password')
        if not password:
            password = getpass('password: ')
        email = args.get('--email')
        if not email:
            email = raw_input ('email: ')
        url = controller + 'api/register'
        payload = {'username': username, 'password': password, 'email': email}
        response = self._session.post(url, data=payload, allow_redirects=False)
        if response.status_code == requests.codes.created:  # @UndefinedVariable
            self._settings['controller'] = controller
            self._settings.save()
            print('Registered {}'.format(username))
            login_args = {'--username': username, '--password': password,
                          '<controller>': controller}
            # login after registering 
            if self.auth_login(login_args) is False:
                print('Login failed')
                return
            # add ssh keys before formations are created
            self.keys_add({})
        else:
            print('Registration failed', response.content)
            return False

    def auth_login(self, args):
        """Authenticate a user in the system."""
        controller = args['<controller>']
        username = args.get('--username')
        headers = {}
        if not username:
            username = raw_input('username: ')
        password = args.get('--password')
        if not password:
            password = getpass('password: ')
        url = controller + 'api/auth/login/'
        payload = {'username': username, 'password': password}
        # prime cookies for login
        self._session.get(url, headers=headers)
        # post credentials to the login URL
        response = self._session.post(url, data=payload, allow_redirects=False)
        if response.status_code == requests.codes.found:  # @UndefinedVariable
            self._settings['controller'] = controller
            self._settings.save()
            print('Logged in as {}'.format(username))
            return True
        else:
            print('Login failed')
            self._session.cookies.clear()
            self._session.cookies.save()
            return False

    def auth_logout(self, args):
        """Clear a user's session and unauthenticate her."""
        controller = self._settings.get('controller')
        if controller:
            self._session.get(controller + 'api/auth/logout/')
        self._session.cookies.clear()
        self._session.cookies.save()
        self._settings['controller'] = None
        self._settings.save()
        print('Logged out')
    
    def backends_list(self, args):
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.get(
            '{}api/formations/{}/backends'.format(
                self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            print('=== {0}'.format(formation))
            data = response.json()
            for item in data['results']:
                print('{0[uuid]:<23} {0[created]}'.format(item))
        else:
            print('Error!', response.text)

    def builds_create(self, args):
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        data = sys.stdin.read()
        # url / sha / slug_size / procfile / checksum
        j = json.loads(data)
        response = self._session.post(
            '{}api/formations/{}/builds'.format(
                self._settings['controller'], formation),
            data=json.dumps(j), headers={'content-type': 'application/json'})
        if response.status_code == requests.codes.created:  # @UndefinedVariable
            print('Build created.')
        else:
            print('Error!', response.text)

    def builds_list(self, args):
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.get('{}api/formations/{}/builds'.format(
            self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            print('=== {0}'.format(formation))
            data = response.json()
            for item in data['results']:
                print('{0[uuid]:<23} {0[created]}'.format(item))
        else:
            print('Error!', response.text)

    def config_list(self, args):
        """List configuration values for an application."""
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.get(
            '{}api/formations/{}/config'.format(
                self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            config = response.json()
            values = json.loads(config['values'])
            print('=== {0}'.format(formation))
            items = values.items()
            if len(items) == 0:
                print('No configuration')
                return
            for k, v in values.items():
                print('{k}: {v}'.format(**locals()))
        else:
            print('Error!', response.text)

    def config_set(self, args):
        """Set a configuration value for a formation."""
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        body = {'values': json.dumps(dictify(args['<var>=<value>']))}
        response = self._session.post(
            '{}api/formations/{}/config'.format(
                self._settings['controller'], formation),
            data=json.dumps(body),
            headers={'content-type': 'application/json'})
        if response.status_code == requests.codes.created:  # @UndefinedVariable
            config = response.json()
            values = json.loads(config['values'])
            print('=== {0}'.format(formation))
            items = values.items()
            if len(items) == 0:
                print('No configuration')
                return
            for k, v in values.items():
                print('{k}: {v}'.format(**locals()))
        else:
            print('Error!', response.text)

    def config_unset(self, args):
        """Unset a configuration value for a formation."""
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        values = {}
        for k in args.get('<key>'):
            values[k] = None
        body = {'values': json.dumps(values)}
        response = self._session.post(
            '{}api/formations/{}/config'.format(
                self._settings['controller'], formation),
            data=json.dumps(body),
            headers={'content-type': 'application/json'})
        if response.status_code == requests.codes.created:  # @UndefinedVariable
            config = response.json()
            values = json.loads(config['values'])
            print('=== {0}'.format(formation))
            items = values.items()
            if len(items) == 0:
                print('No configuration')
                return
            for k, v in values.items():
                print('{k}: {v}'.format(**locals()))
        else:
            print('Error!', response.text)

    def containers_list(self, args):
        """List containers for this formation."""
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.get('{}api/formations/{}/containers'.format(
            self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            print('=== {0}'.format(formation))
            data = response.json()
            for item in data['results']:
                print('{0[uuid]}'.format(item))
        else:
            print('Error!', response.text)

    def keys_add(self, args):
        """Add SSH keys for the logged in user."""
        path = args.get('<key>')
        if not path:
            ssh_dir = os.path.expanduser('~/.ssh')
            pubkeys = glob.glob(os.path.join(ssh_dir, '*.pub'))
            print('Found the following SSH public keys:')
            for i, k in enumerate(pubkeys):
                key = k.split(os.path.sep)[-1]
                print('{0}) {1}'.format(i+1, key))
            inp = raw_input('Which would you like to use with Deis? ')
            try:
                path = pubkeys[int(inp)-1]
                key_id = path.split(os.path.sep)[-1].replace('.pub', '')
            except:
                print 'Aborting'
                return
        with open(path) as f:
            data = f.read()
        match = re.match(r'^(ssh-...) ([^ ]+) (.+)', data)
        if not match:
            print 'Could not parse public key material'
            return
        key_type, key_str, _key_comment = match.groups()
        body = {'id': key_id, 'public': '{0} {1}'.format(key_type, key_str)}
        sys.stdout.write('Uploading {0} to Deis... '.format(path))
        sys.stdout.flush()
        response = self._session.post('{}api/keys'.format(
            self._settings['controller']), json.dumps(body),
            headers={'content-type': 'application/json'})
        if response.status_code == requests.codes.created:  # @UndefinedVariable
            print('done')
        else:
            print('Error!', response.text)

    def keys_list(self, args):
        """List SSH keys for the logged in user."""
        response = self._session.get('{}api/keys'.format(
            self._settings['controller']))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            data = response.json()
            if data['count'] == 0:
                print 'No keys found'
                return
            print('=== {0} Keys'.format(data['results'][0]['owner']))
            for key in data['results']:
                public = key['public']
                print('{0} {1}...{2}'.format(
                  key['id'], public[0:16], public[-10:]))
        else:
            print('Error!', response.text)

    def keys_remove(self, args):
        """Remove a specific SSH key for the logged in user."""
        key = args.get('<key>')
        sys.stdout.write('Removing {0} SSH Key... '.format(key))
        sys.stdout.flush()
        response = self._session.delete('{}api/keys/{}'.format(
            self._settings['controller'], key))
        if response.status_code == requests.codes.no_content:  # @UndefinedVariable
            print('done')
        else:
            print('Error!', response.text)
    
    def flavors_create(self, args):
        """Create a flavor using a provider"""
        body = {'id': args.get('--id'), 'provider': args.get('--provider')}
        fields = ('params', 'init', 'ssh_username', 'ssh_private_key',
                  'ssh_public_key')
        for fld in fields:
            opt = args.get('--'+fld)
            if opt:
                body.update({fld: opt})
        controller = self._settings['controller']
        response = self._session.post(
            controller + 'api/flavors/', data=body)
        if response.status_code == requests.codes.created:  # @UndefinedVariable
            print('{0[id]}'.format(response.json()))
        else:
            print('Error!', response.text)

    def flavors_delete(self, args):
        """Delete a node flavor by ID."""
        node = args.get('<id>')
        response = self._session.delete(
            '{}api/flavors/{}'.format(
                self._settings['controller'], node))
        if response.status_code == requests.codes.no_content:  # @UndefinedVariable
            pass
        else:
            print('Error!', response.status_code, response.text)

    def flavors_info(self, args):
        """Show detail of a node flavor."""
        flavor = args.get('<flavor>')
        response = self._session.get(
                '{}api/flavors/{}'.format(
                    self._settings['controller'], flavor))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            print(json.dumps(response.json(), indent=2))
        else:
            print('Error!', response.text)
    
    def flavors_list(self, args):
        """List flavors for a user."""
        response = self._session.get(
            self._settings['controller'] + 'api/flavors/')
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            data = response.json()
            for item in data['results']:
                print('{0[id]:<23}'.format(item))
        else:
            print('Error!', response.text)

    def formations_create(self, args):
        """Create a formation."""
        body = {'image': args['--image'], 'flavor': args['--flavor']}
        if '--id' in args:
            body.update({'id': args['--id']})
        controller = self._settings['controller']
        response = self._session.post(
            controller + 'api/formations/', data=body)
        if response.status_code == requests.codes.created:  # @UndefinedVariable
            data = response.json()
            formation = data['id']
            print('Created ' + formation)
            hostname = urlparse.urlparse(controller).netloc
            try:
                subprocess.check_call(
                    ['git', 'remote', 'add', '-f', 'deis',
                     'git@{hostname}:{formation}.git'.format(**locals())])
            except subprocess.CalledProcessError:
                sys.exit(1)
            print('Git remote added')
        else:
            print('Error!', response.text)

    def formations_list(self, args):
        """List formations for a user."""
        response = self._session.get(
            self._settings['controller'] + 'api/formations/')
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            data = response.json()
            for item in data['results']:
                print('{0[id]:<23}'.format(item))
        else:
            print('Error!', response.text)

    def formations_destroy(self, args):
        formation = args.get('<formation>')
        if not formation:
            formation = self._session.formation
        confirm = args.get('--confirm')
        if confirm == formation:
            pass
        else:
            print """
 !    WARNING: Potentially Destructive Action
 !    This command will destroy: {formation}
 !    To proceed, type "{formation}" or re-run this command with --confirm={formation}
""".format(**locals())
            confirm = raw_input('> ').strip('\n')
            if confirm != formation:
                print('Destroy aborted')
                return
        print 'Destroying {0}...'.format(formation),
        response = self._session.delete(
                '{}api/formations/{}'.format(
                    self._settings['controller'], formation))
        if response.status_code in (requests.codes.no_content, requests.codes.not_found):  # @UndefinedVariable
            print('done')
            try:
                subprocess.check_call(
                    ['git', 'remote', 'rm', 'deis'],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                print('Git remote removed')
            except subprocess.CalledProcessError:
                pass  # ignore error
        else:
            print('Error!', response.text)

    def formations_scale(self, args):
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        body = {}
        for type_num in args.get('<type=num>'):
            typ, count = type_num.split('=')
            body.update({typ: int(count)})
        response = self._session.post(
            '{}api/formations/{}/scale'.format(
                self._settings['controller'], formation),
            data=json.dumps(body),
            headers={'content-type': 'application/json'})
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            databag = json.loads(response.content)
            print(json.dumps(databag, indent=2))
        else:
            print('Error!', response.text)

    def formations_calculate(self, args):
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.post(
            '{}api/formations/{}/calculate'.format(
                self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            databag = json.loads(response.content)
            print(json.dumps(databag, indent=2))
        else:
            print('Error!', response.text)

    def formations_balance(self, args):
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.post(
            '{}api/formations/{}/balance'.format(
                self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            databag = json.loads(response.content)
            print(json.dumps(databag, indent=2))
        else:
            print('Error!', response.text)
    
    def formations_converge(self, args):
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.post(
            '{}api/formations/{}/converge'.format(
                self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            databag = json.loads(response.content)
            print(json.dumps(databag, indent=2))
        else:
            print('Error!', response.text)

    def nodes_info(self, args):
        """Show detail of a provider."""
        node = args.get('<node>')
        response = self._session.get(
                '{}api/nodes/{}'.format(
                    self._settings['controller'], node))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            print(json.dumps(response.json(), indent=2))
        else:
            print('Error!', response.text)
    
    def nodes_list(self, args):
        """List nodes for this formation."""
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.get(
            '{}api/formations/{}/nodes'.format(
                self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            print('=== {0}'.format(formation))
            data = response.json()
            format_str = '{0[id]:<23} {0[type]} {0[provider_id]} {0[fqdn]}'
            for item in data['results']:
                print(format_str.format(item))
        else:
            print('Error!', response.text)

    def nodes_delete(self, args):
        """Delete a node by ID."""
        node = args.get('--id')
        response = self._session.delete(
            '{}api/nodes/{}'.format(self._settings['controller'], node))
        if response.status_code == requests.codes.no_content:  # @UndefinedVariable
            print('Node deleted successfully')
        else:
            print('Error!', response.status_code, response.text)

    def providers_create(self, args):
        """Create a provider for use by Deis"""
        type = args.get('--type')  # @ReservedAssignment
        if type == 'ec2':
            # read creds from envvars
            for k in ('AWS_ACCESS_KEY', 'AWS_SECRET_KEY'):
                if not k in os.environ:
                    msg = 'Missing environment variable: {}'.format(k)
                    raise EnvironmentError(msg)
            creds = {'access_key': os.environ['AWS_ACCESS_KEY'],
                     'secret_key': os.environ['AWS_SECRET_KEY']}
        id = args.get('--id')  # @ReservedAssignment
        if not id:
            id = type  # @ReservedAssignment
        body = {'id': id, 'type': type, 'creds': json.dumps(creds)}
        response = self._session.post(
            '{}api/providers'.format(self._settings['controller']),
            data=json.dumps(body),
            headers={'content-type': 'application/json'})
        if response.status_code == requests.codes.created:  # @UndefinedVariable
            print('{0[id]}'.format(response.json()))
        else:
            print('Error!', response.text)

    def providers_list(self, args):
        """List providers."""
        response = self._session.get(
            '{}api/providers'.format(self._settings['controller']))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            data = response.json()
            for item in data['results']:
                print(item['id'])
        else:
            print('Error!', response.text)

    def providers_info(self, args):
        """Show detail of a provider."""
        provider = args.get('<provider>')
        response = self._session.get(
                '{}api/providers/{}'.format(
                    self._settings['controller'], provider))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            print(json.dumps(response.json(), indent=2))
        else:
            print('Error!', response.text)
    
    def proxies_list(self, args):
        """List proxies for this formation."""
        formation = args.get('--formation')
        if not formation:
            formation = self._session.formation
        response = self._session.get(
            '{}api/formations/{}/proxies'.format(
                self._settings['controller'], formation))
        if response.status_code == requests.codes.ok:  # @UndefinedVariable
            print('=== {0}'.format(formation))
            data = response.json()
            for item in data['results']:
                print('{0[uuid]} {0[created]}'.format(item))
        else:
            print('Error!', response.text)


def main():
    """
    Create a client, parse the arguments received on the command line, and
    call the appropriate method on the client.
    """
    # create a client instance
    cli = DeisClient()
    # parse base command-line arguments
    args = docopt(__doc__, version='Deis CLI {}'.format(__version__),
                  options_first=True)
    cmd = args['<command>']
    # re-parse for any cmd-specific arguments
    if cmd in USAGE:
        args.update(docopt(USAGE[cmd]))
    # split cmd with _ if it contains a :
    if ':' in cmd:
        cmd = '_'.join(cmd.split(':'))
    # prepare a map of shortcut commands
    dispatch = {'register': cli.auth_register,
                'login': cli.auth_login,
                'logout': cli.auth_logout,
                'create': cli.formations_create,
                'destroy': cli.formations_destroy,
                'scale': cli.formations_scale,
                'calculate': cli.formations_calculate,
                'balance': cli.formations_balance,
                'converge': cli.formations_converge}
    # dispatch based on primary command 
    if cmd == 'help':
        docopt(__doc__, argv=['--help'])
    elif hasattr(cli, cmd):
        method = getattr(cli, cmd)
        return method(args)
    elif hasattr(cli, cmd+'_list'):
        method = getattr(cli, cmd+'_list')
        return method(args)
    elif cmd in dispatch:
        method = dispatch[cmd]
        return method(args)
    else:
        print 'Found no matching command'
        raise DocoptExit()


if __name__ == '__main__':
    main()
    sys.exit(0)
