#!/usr/bin/env python
# TODO: nuke json dependency
import json
import logging
import optparse
import os
import re
import stripe
import sys
import textwrap
import urllib
import urllib2
import urlparse

import ConfigParser

logging.basicConfig(level=logging.WARN)
logger = logging.getLogger(__name__)

# This key isn't sensitive, but people's code will break if I ever
# roll it.  TODO: retrieve the key on demand from the domaincli
# server.
stripe.api_key = 'pk_7n4q8sAaOWgUHTK2Opt14l2TbIWXw'

class Error(Exception):
    pass

class DomainCLIError(Exception):
    pass

class RPC(object):
    # Yeah yeah yeah, I know.  I'll buy an SSL cert for domaincli.com soon.
    URL = 'https://getpokebot.com:7000/v1'
    # URL = 'http://localhost:8000/v1'

    def __init__(self, config=None):
        self.config = config

    # Stolen from Stripe
    def _encodeInner(self, d):
        """
        We want post vars of form:
        {'foo': 'bar', 'nested': {'a': 'b', 'c': 'd'}}
        to become:
        foo=bar&nested[a]=b&nested[c]=d
        """
        stk = []    
        for key, value in d.items():
            if isinstance(value, dict):
                n = {}
                for k, v in value.items():
                    n["%s[%s]" % (key, k)] = v
                    stk.extend(self._encodeInner(n))
            else:
                stk.append((key, value))
        return stk

    # Stolen from Stripe
    def _encode(self, d):
        """
        Internal: encode a string for url representation
        """
        return urllib.urlencode(self._encodeInner(d))

    def call(self, name, args={}):
        args['method'] = name
        args['username'] = os.environ.get('USER')
        if self.config:
            args['user_id'] = self.config.get('DomainCLI', 'user_id')
        serialized = self._encode(args)
        logger.debug('About to call %s with params %r' % (self.URL, args))
        try:
            c = urllib2.urlopen(self.URL, serialized)
        except urllib2.URLError, e:
            raise DomainCLIError("Unexpected error while talking to server: %s" % (e, ))
        resp = c.read()
        resp = urlparse.parse_qs(resp, strict_parsing=True)
        for k, v in resp.items():
            resp[k] = v[0]
        logging.debug('Result: %r' % resp)
        if resp['object'] == 'error':
            raise DomainCLIError(resp['message'])
        return resp

### Valid subprograms
def check(parser, opts, args):
    if len(args) == 0:
        parser.print_help()
        return 1
    check_backend(parser, opts, args)

def check_backend(parser, opts, args):
    results = {}
    config = load_config(opts)
    rpc = RPC(config)
    for arg in args:
        before_rpc('Checking status of %s... ' % (arg, ))
        sys.stdout.flush()
        res = rpc.call('check_availability', { 'domain' : arg })
        if res['available'] == 'True':
            after_rpc_success('available!')
            results[arg] = True
        else:
            after_rpc_failure('not available.  :(')
            results[arg] = False
    return results

def register(parser, opts, args):
    if len(args) == 0:
        parser.print_help()
        return 1
    results = check_backend(parser, opts, args)
    available = [domain for domain, avail in results.iteritems() if avail]
    not_available = [domain for domain, avail in results.iteritems() if not avail]
    if not_available:
      print
      if len(not_available) == 1:
        print "Sorry, I can't register %s for you.  You should try again" % (not_available[0], )
        print "with an even better domain (say, get%s)!" % (not_available[0], )
      else:
        print "Sorry, all of %s are taken, so I can't register them for you." % (', '.join(not_available), )
      if available:
        print
        if len(available) == 1:
          print "I can, however, register %s for you!" % (available[0], )
        elif len(available) > 1:
          print "I can, however, register all of %s for you!" % (', '.join(available), )
        print "Just run 'domaincli register %s'" % (' '.join(available), )
      return
    config = load_config(opts)
    rpc = RPC(config)
    if opts.years == 1:
        years = '1 year'
    else:
        years = '%d years' % opts.years
    if len(args) == 1:
        domains = '1 domain'
    else:
        domains = '%d domains' % len(args)
    total = 12 * opts.years * len(args)
    total_msg = '* Your total comes to $%d (%s x %s) *' % (total, years, domains)
    print '*' * len(total_msg)
    print total_msg
    print '*' * len(total_msg)
    card = get_card(config, rpc, opts)
    ok = raw_input('Are you cool with this purchase? [Yn] ')
    if ok.lower()[0:1] == 'n':
        print 'As you wish.  Aborting without making a purchase!'
        return
    for arg in args:
        before_rpc('Trying to register %s... ' % arg)
        res = rpc.call('register_domain', { 'domain' : arg,
                                            'years' : opts.years })
        if res['success'] == 'True':
            after_rpc_success('success!')
            print '\nYou are now the proud owner of'
            print '  %s' % arg
            print
            nameservers = get_nameservers(config, opts, args)
            rpc.call('set_nameservers', { 'nameservers' : nameservers, 'domain' : arg })
        else:
            after_rpc_failure('failure!')
        print ' \-->', res['message']

### Output
def before_rpc(msg, print_method=None):
    if not print_method:
        print_method = sys.stdout.write
    print_method(msg)
    sys.stdout.flush()

def after_rpc_success(msg, print_method=None):
    if not print_method:
        print_method = sys.stdout.write
    print_method(msg + '\n')
    sys.stdout.flush()

def after_rpc_failure(msg, print_method=None):
    if not print_method:
        print_method = sys.stdout.write
    print_method(msg + '\n')
    sys.stdout.flush()

### Config
re_ns = re.compile('^([a-z0-9]([-a-z0-9]*[a-z0-9])?\\.)+((a[cdefgilmnoqrstuwxz]|aero|arpa)|(b[abdefghijmnorstvwyz]|biz)|(c[acdfghiklmnorsuvxyz]|cat|com|coop)|d[ejkmoz]|(e[ceghrstu]|edu)|f[ijkmor]|(g[abdefghilmnpqrstuwy]|gov)|h[kmnrtu]|(i[delmnoqrst]|info|int)|(j[emop]|jobs)|k[eghimnprwyz]|l[abcikrstuvy]|(m[acdghklmnopqrstuvwxyz]|mil|mobi|museum)|(n[acefgilopruz]|name|net)|(om|org)|(p[aefghklmnrstwy]|pro)|qa|r[eouw]|s[abcdeghijklmnortvyz]|(t[cdfghjklmnoprtvwz]|travel)|u[agkmsyz]|v[aceginu]|w[fs]|y[etu]|z[amw])$', re.IGNORECASE)
re_ip = re.compile('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')
def get_nameservers(config, opts, args):
    nameservers = opts.nameservers
    if not nameservers:
        try:
            nameservers = config.get('DomainCLI', 'nameservers')
        except ConfigParser.NoOptionError:
            pass
    while True:
        if nameservers:
            # Ignore trailing periods, because internet.bs doesn't like them
            for ns in [ns.strip().rstrip('.') for ns in nameservers.split(',')]:
                if all([re_ns.search(n) or re_ip.search(n) for n in ns.split(' ')]):
                    continue
                print "Uh-oh: There are invalid characters in one of your nameservers (%r)" % ns
                print "Nameservers can only be domain names and IP addresses."
                print "Mind helping me out?"
                nameservers = None
                break
        if nameservers:
            return nameservers
        if len(args) == 1:
            dom = 'this domain'
        else:
            dom = 'these domains'
        print 'What nameservers would you like to use for %s?' % dom
        print "(HINT: If you're not sure, run 'dig NS <yourotherdomain>'"
        print " to find what nameservers you are using elsewhere)"
        print 'Please give me a comma-separated list of nameservers; then press enter.'
        inp = raw_input('Nameservers: ')
        nameservers = ', '.join([ns.strip().rstrip('.') for ns in inp.split(',')])
        config.set('DomainCLI', 'nameservers', nameservers)
        save_config(config, opts)

def get_card(config, rpc, opts):
    card = None
    if not opts.new_card:
        res = rpc.call('domaincli_get_card')
        if res['success'] == 'True':
            card = {
                'type' : res['card[type]'],
                'last4' : res['card[last4]'],
                'exp_month' : res['card[exp_month]'],
                'exp_year' : res['card[exp_year]']
                }
            print "Using %s ending in %s and expiring on %s/%s" % (card['type'], card['last4'], card['exp_month'], card['exp_year'])
    while not card:
        print "Please enter your credit card details below"
        print "(Your credit card information is stored with Stripe,"
        print " a PCI level-one compliant payment processor, and never"
        print " touches our servers.)"
        number = raw_input('Card number: ')
        exp_month = raw_input('Expiry month: ')
        exp_year = raw_input('Expiry year: ')
        if len(exp_year) == 2:
            exp_year = '20%s' % exp_year
        real_card = { 'number' : number, 'exp_month' : exp_month, 'exp_year' : exp_year }
        # TODO: catch these exceptions
        res = stripe.Token.create(card=real_card, amount=1200)
        res = rpc.call('domaincli_add_card', { 'card_token' : res.id })
        card = res
    return card

def save_config(config, opts):
    path = os.path.expanduser(opts.config_file)
    # TODO: atomic write?  Getting that right is always kind of tricky.
    config.write(open(path, 'w'))

def load_config(opts):
    config = ConfigParser.ConfigParser()
    path = os.path.expanduser(opts.config_file)
    if os.path.exists(path):
        loaded = True
        f = open(path)
        config.readfp(f)
    else:
        loaded = False

    try:
        config.get('DomainCLI', 'user_id')
    except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        pass
    else:
        return config

    if loaded:
        msg = ("It appears your config file\n"
               "%s\n"
               "does not contain a valid user ID.\n"
               "I'm going to generate a new ID for\n"
               "you.  Feel free to replace this new\n"
               "user ID with an existing one, if you\n"
               "have one." % opts.config_file)
    else:
        msg = ("Looks like this is your first time running DomainCLI.\n"
               "If you'll pardon the brief interruption, I'm going to\n"
               "create a user ID for you and automatically save it to\n"
               "%s" % opts.config_file)
    logger.info('->' + '\n-> '.join(textwrap.wrap(msg)))
    before_rpc('-> Generating user ID... ', print_method=logger.info)
    res = RPC().call('domaincli_create_account')
    after_rpc_success('done!', print_method=logger.info)
    try:
        config.add_section('DomainCLI')
    except ConfigParser.DuplicateSectionError:
        pass
    config.set('DomainCLI', 'user_id', res['id'])
    save_config(config, opts)
    logger.info("-> For your information, your user ID is\n"
                "->  %s\n" % res['id'])
    return config


### Stripe
class StripeResponse(object):
  def __init__(self, d):
    self.dict = d
  
  def __getattr__(self, name):
    return self.dict[name]

class StripeException(Exception):
  def __init__(self, msg):
    self.message = msg
    super(StripeException, self).__init__(msg)
  
  def message(self):
    self.message

class StripeCardException(StripeException):
  def __init__(self, resp):
    self.code = resp.get('code', '')
    self.param = resp.get('param', '')
    super(StripeCardException, self).__init__(resp['message'])

class StripeInvalidRequestException(StripeException):
  def __init__(self, resp):
    self.param = resp.get('param', '')
    super(StripeInvalidRequestException, self).__init__(resp['message'])

class StripeAPIException(StripeException):
  pass

class StripeAPIConnectionException(StripeException):
  pass

def main():
    programs = {'check' : check,
                'register' : register}
    idx = prog = None
    proper_args = sys.argv[1:]
    for i, arg in enumerate(proper_args):
        if len(arg) == 0 or arg[0] != '-':
            idx = i
            prog = arg
            break
    if idx is not None:
        args = proper_args[:idx] + proper_args[idx+1:]
    else:
        args = sys.argv

    if prog == 'check':
        parser = optparse.OptionParser("""%prog [options] check domain [domain...]""")
    elif prog == 'register':
        parser = optparse.OptionParser("""%prog [options] register domain [domain...]

Supported domain names are com, info, net, org, us.  Please let us
know if you'd like to see additional supported domains (gdb@gregbrockman.com)""")
        parser.add_option('-s', '--name-server', help='Nameservers for registered domain [can pass multiple times. Defaults to list in config file]',
                          dest='nameservers', action='append', default=[])
        parser.add_option('-y', '--years', help='Number of years to register for',
                          dest='years', type=int, default=1)
        parser.add_option('-c', '--new-card', help="Use a new credit card rather than the one on file (if one exists)",
                          dest='new_card', action='store_true', default=False)

    else:
        prog = None
        parser = optparse.OptionParser("""%%prog [options] %s""" % '|'.join(programs.iterkeys()))
    parser.add_option('-v', '--verbosity', help='Verbosity of debugging output.',
                      dest='verbosity', action='count', default=0)
    parser.add_option('-f', '--config-file',
                      help='Location of config file (default: ~/.domaincli)',
                      dest='config_file', default='~/.domaincli')
    opts, args = parser.parse_args(args)

    if opts.verbosity == 1:
        logging.getLogger('').setLevel(logging.INFO)
    elif opts.verbosity >= 2:
        logging.getLogger('').setLevel(logging.DEBUG)
    try:
        method = programs[prog]
    except KeyError:
        parser.print_help()
        return 1
    else:
        logger.debug('About to call %s with %r / %r' % (prog, opts, args))
        return method(parser, opts, args)

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