#!/usr/bin/python

"""A Python command-line wrapper with github3.py library to access GitHub Gist.
"""

from github3 import authorize
from getpass import getuser, getpass
from github3 import login
from github3.models import GitHubError

import argparse
from functools import wraps
import os
import requests
import sys
try:
    from urlparse import urlparse
except ImportError:
    # py3k
    from urllib.parse import urlparse

def auth_check(func):
    """Decorator to note which object methods require authorization"""
    @wraps(func)
    def check_wrapper(self, *args, **kwargs):
        """A wrapper to check if a token exists"""
        if not kwargs.get('anonymous'):
            try:
                with open(os.path.expanduser('~/.pgist'), 'r') as tkf:
                    self.token = tkf.readline()
                self.github = login(token=self.token)
            except IOError:
                raise SystemExit('Please use `pgist --login` authenticate ' \
                        'gist on this computer')
        try:
            return func(self, *args, **kwargs)
        except GitHubError as ghe:
            if ghe.code in (401, 403):
                raise SystemExit('Your current gist authorize is bad, ' \
                        'please use `pgist --login` to authenticate it again!')
            raise SystemExit(ghe + '\nPlease report this bug to the author!')
    return check_wrapper

def token_request():
    """Request app token from GitHub to operate gists"""
    user = raw_input('GitHub username(default is {0}): '.format(getuser())) \
            or getuser()
    password = ''

    while not password:
        password = getpass('GitHub password for {0}: '.format(user))

    note = 'pgist'
    note_url = 'https://github.com/douglarek/pgist'
    scopes = ['user', 'gist']

    try:
        auth = authorize(user, password, scopes, note, note_url)
    except GitHubError:
        raise SystemExit('Gist authorize failed, please check your username'\
                'or password!')

    with open(os.path.expanduser('~/.pgist'), 'w') as tkf:
        tkf.write(auth.token)

    print('Done ...')

def url_shorten(long_url):
    """Shorten a long url with git.io service"""
    req = requests.post('http://git.io', data={'url' : long_url})
    return req.headers['location']

def upload_files(files):
    """Build up uploaded or updated files' structure"""
    files = [f for f in files if os.path.exists(f)]
    _upload_files = {}
    for _ in files:
        with open(_, 'r') as _fd:
            content = _fd.readlines()
            if not content:
                continue
            _upload_files[os.path.basename(_)] = {'content' : \
                    ''.join(content)}

    if not _upload_files:
        raise SystemExit('All of your files are empty, WTF?')

    return _upload_files

def find_gist_by_id(github, _id):
    """Find a gist by _id"""
    dest = None
    for gist in github.iter_gists():
        if _id == gist.id or _id == gist.html_url:
            dest = gist
            break

    if dest is None:
        raise SystemExit('The gist ID or URL is not found, is it right?')

    return dest

def get_id(_id):
    """Convert _id(ID or URL) to ID"""
    result = urlparse(_id)
    if result.path:
        return result.path.split('/')[-1]
    raise SystemExit('Your url(id): {0} is invalid !'.format(_id))

class Gist(object):
    """Define known gist operations"""

    def __init__(self):
        self.token, self.github = None, None

    @auth_check
    def list_gists(self, _all=False):
        """List all gists or public only ones"""
        print('List of {0} gists: \n'.format(['public','all'][_all]))
        if _all:
            for gist in self.github.iter_gists():
                print('{0}{1}'.format(\
                        [g.name for g in gist.iter_files()][0].ljust(30), \
                        gist.html_url))
        else:
            for gist in self.github.iter_gists():
                if gist.is_public():
                    print('{0}{1}'.format(\
                            [g.name for g in gist.iter_files()][0].ljust(30), \
                            gist.html_url))

    @auth_check
    def create_gist(self,
                    description=None,
                    files=(),
                    public=True,
                    anonymous=False,
                    short_url=False):
        """Create public, private or anonymous gists"""
        if description is None:
            description = ''

        if anonymous:
            from github3 import create_gist
            gist = create_gist(description, upload_files(files))
        else:
            gist = self.github.create_gist(description, upload_files(files), \
                    public)

        print(url_shorten(gist.html_url) if short_url else gist.html_url)

    @auth_check
    def update_gist(self,
                    _id,
                    description=None,
                    files=()):
        """Update a gist"""
        if description is None:
            description = ''

        dest = find_gist_by_id(self.github, get_id( _id))

        if dest.edit(description, upload_files(files)):
            print('<{0}> has been update successfully.'.format(dest.html_url))

    @auth_check
    def delete_gist(self, _id):
        """Delete a gist by _id"""
        dest = find_gist_by_id(self.github, get_id(_id))
        if dest.delete():
            print('<{0}> has been deleted successfully.'.format(dest.html_url))

    @auth_check
    def fork_gist(self, _id):
        """Fork a gist by ID or URL"""
        try:
            new = self.github.gist(get_id(_id)).fork()
        except AttributeError:
            raise SystemExit('The gist {0} is not found !'.format(_id))
        if new is None:
            raise SystemExit('Enha, maybe you are forking yourself ?')
        print('{0} is forked to {1}'.format(_id, new.html_url))

def main(argv):
    """The main body"""
    description = 'A Python command-line wrapper with github3.py library '\
            'to access GitHub gists'
    parser = argparse.ArgumentParser(description=description)

    parser.add_argument('-l', '--list', help='List public gists, with `-A` ' \
            'list all ones', action='store_true')

    parser.add_argument('-A', '--all', action='store_true')
    parser.add_argument('-s', '--shorten', action='store_true', help=\
            'Shorten the gist URL using git.io')
    parser.add_argument('-u', '--update', metavar='[ID | URL]', \
            help='Update an existing gist')

    parser.add_argument('files', nargs='*', metavar='FILE*', \
            type=os.path.abspath, help=\
            'Files which will be uploaded, separate multiple ones with space')
    parser.add_argument('-d', '--desc', help='Adds a description to your gist')
    group1 = parser.add_mutually_exclusive_group()
    group1.add_argument('-D', '--delete', metavar='[ID | URL]', \
            help='Detele an existing gist')
    group1.add_argument('-f', '--fork', metavar='[ID | URL]', \
            help='Fork an existing gist')
    group2 = parser.add_mutually_exclusive_group()
    group2.add_argument('-p', '--private', help='Makes your gist private', \
            action='store_true')
    group2.add_argument('-a', '--anonymous', help='Create an anonymous gist', \
            action='store_true')

    parser.add_argument('--login', help='Authenticate gist on this computer', \
            action='store_true')

    args = parser.parse_args(argv)

    gist = Gist()

    if args.list:
        gist.list_gists(_all=[False, True][args.all])
    elif args.update and args.files:
        gist.update_gist(args.update, args.desc, args.files)
    elif args.delete:
        gist.delete_gist(args.delete)
    elif args.fork:
        gist.fork_gist(args.fork)
    elif args.files:
        gist.create_gist(args.desc, args.files, \
                [True, False][args.private], anonymous=args.anonymous, \
                short_url=args.shorten)
    elif args.login:
        token_request()
    else:
        parser.print_help()

if __name__ == '__main__':
    try:
        sys.exit(main(sys.argv[1:]))
    except KeyboardInterrupt:
        raise SystemExit('\nOk, Goodbye.')
