#!/usr/bin/env python
'''
This file is part of the Python EJTP library.

The Python EJTP library is free software: you can redistribute it 
and/or modify it under the terms of the GNU Lesser Public License as
published by the Free Software Foundation, either version 3 of the 
License, or (at your option) any later version.

the Python EJTP library is distributed in the hope that it will be 
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser Public License for more details.

You should have received a copy of the GNU Lesser Public License
along with the Python EJTP library.  If not, see 
<http://www.gnu.org/licenses/>.
'''

from __future__ import print_function

__doc__ = '''ejtp-identity

A simple little script for EJTP identity management from the command line.

Usage:
    ejtp-identity ( list ) [--by-file|--cache-source=<cache-source>]
    ejtp-identity ( details ) [-ep] <names>... [--cache-source=<cache-source>]
    ejtp-identity ( rm ) [-A] <names>... [--cache-source=<cache-source>]
    ejtp-identity ( new ) --name=<name> --location=<location> --encryptor=<encryptor>
    ejtp-identity ( new-interactive )
    ejtp-identity ( merge ) <filename>
    ejtp-identity ( dl | download ) <names>... [--server=<server>]
    ejtp-identity ( set ) <name> --args=<args> [--cache-source=<cache-source>]
    ejtp-identity -h | --help
    ejtp-identity --version

Options:
    -h --help       Show this help message
    -e --export     Wrap identity in the cache structure [default: False]
    -p --public     Convert the encryptor to its public counterpart [default: False]
'''

import os
import sys
import json
import traceback

import requests
from persei import JSONBytesEncoder

from ejtp.config import test_filenames
from ejtp.util.hasher import strict
from ejtp.util.scripts import confirm, retry, get_identity
from ejtp.crypto.encryptor import make
from ejtp.vendor.docopt import docopt
from ejtp.identity import Identity, IdentityCache

try:
    input = raw_input
except:
    pass

class JSONEncoder(json.JSONEncoder):

    def default(self, obj):
        try:
            return json.JSONEncoder.default(self, obj)
        except TypeError:
            return JSONBytesEncoder(obj)


def data_per_file(cache_source, env_var='EJTP_IDENTITY_CACHE_PATH'):
    files = []
    if cache_source:
        files = test_filenames([cache_source])
    if not files:
        files = test_filenames([], env_var=env_var)

    for fname in files:
        with open(fname) as f:
            yield fname, json.load(f)

def list_identities(cache_source, by_file=False, name=None):
    for fname, data in data_per_file(cache_source):
        fname_printed = False
        for identity in data.values():
            if by_file and not fname_printed:
                print(os.path.relpath(fname))
                fname_printed = True
            if not name or name == identity['name']:
                print('%s (%s)' % (identity['name'], identity['encryptor'][0]))

def identity_details(cache_source, names, export=False, public=False):
    identities = {}
    for fname, data in data_per_file(cache_source):
        for identity in data.values():
            if identity['name'] in names:
                if public:
                    encryptor = make(identity.get('encryptor'))
                    identity['encryptor'] = encryptor.public()
                identities[strict(identity.get('location')).export()] = identity
    if export:
        print(json.dumps(identities, indent=2, cls=JSONEncoder))
    else:
        for identity in sorted(identities.values(), key=lambda x: x.get('name')):
            print(json.dumps(identity, indent=2, cls=JSONEncoder))

def new_identity(name, location, encryptor, **kwargs):
    data = {
        'name': name,
        'location': location,
        'encryptor': encryptor
    }
    data.update(kwargs)
    print(json.dumps({strict(data['location']).export(): data}, indent=2, cls=JSONEncoder))

def new_interactive():
    def show_ident(ident):
        print("\n\nYour full identity is:\n")
        print(json.dumps(
            ident.serialize(),
            indent=2,
            cls=JSONEncoder,
            sort_keys = True
        ))

    ident = confirm(get_identity, show=show_ident)
    cache = IdentityCache()
    cache.update_ident(ident)

    def write(fname):
        json.dump(
            cache.serialize(),
            open(fname, 'w'),
            indent=2,
            cls=JSONEncoder,
            sort_keys = True
        )

    retry("File location to save your new cache? ", write)
    print("Congratulations!")

def merge(filename, data_to_merge):
    with open(filename, 'r') as f:
        data = json.load(f)

    data.update(**data_to_merge)

    with open(filename, 'w') as f:
        json.dump(data, f)

def download_idents(names, server):
    cache = IdentityCache()
    def die(short, e):
        longmsg = "".join(
            traceback.format_exception_only(type(e), e)
        )
        allmsg = " FAIL! (%s)\n%s" % (short, longmsg)
        print(allmsg, file=sys.stderr)
        quit(1)
    
    print("Downloading via %s..." % server, file=sys.stderr)
    for name in names:
        url = "%sidents/%s" % (server, name)
        print(" * %s..." % url, end='', file=sys.stderr)
        try:
            req = requests.get(url)
        except IOError as e:
            die('conn', e)

        try:
            req.raise_for_status()
        except Exception as e:
            die("http %d" % req.status_code, e)

        try:
            cache_json = req.json()
        except Exception as e:
            die("json parsing", e)

        try:
            cache.deserialize(cache_json)
        except Exception as e:
            die("accum cache", e)
        print(" done", file=sys.stderr)
    json.dump(
        cache.serialize(),
        sys.stdout,
        indent = 2,
        cls = JSONEncoder
    )
    #print() # Final newline

def set_attribute(cache_source, name, **kwargs):
    found = False
    for fname, data in data_per_file(cache_source):
        with open(fname, 'r') as f:
            for identity in data.values():
                if identity['name'] == name:
                    identity.update(**kwargs)
                    found = True
        if found:
            with open(fname, 'w') as f:
                json.dump(data, f)

def rm_identities(cache_source, rm_all=False, *names):
    names_found = {}
    keys_per_file = {}
    for fname, data in list(data_per_file(cache_source)):
        for key, identity in data.items():
            name = identity['name']
            if name in names:
                if not rm_all and name in names_found:
                    print('Identity %s found in multiple files:\n' % name)
                    list_identities(cache_source, by_file=True, name=name)
                    print('\nUse --cache-source to specify which file to delete ' +
                        'from or use -A to delete from all sources.')
                    quit(1)
                else:
                    names_found[name] = []
                names_found[name].append(fname)
                if fname not in keys_per_file:
                    keys_per_file[fname] = []
                keys_per_file[fname].append(key)

    for fname, keys in keys_per_file.items():
        with open(fname) as f:
            data = json.load(f)
        for key in keys:
            identity = data.pop(key)
            print('%s removed from file %s' % (identity['name'], fname))
        with open(fname, 'w') as f:
            json.dump(data, f)

    for name in set(names) - set(names_found.keys()):
        print('%s not found in any cache file' % name)

def main(argv):
    arguments = docopt(__doc__, argv=argv[1:],
        version='ejtp-identity 0.9.7')

    cache_source = arguments.get('--cache-source')

    if arguments.get('list'):
        list_identities(cache_source, by_file=arguments.get('--by-file'))

    if arguments.get('details'):
        identity_details(cache_source, arguments['<names>'], arguments['--export'], arguments['--public'])

    if arguments.get('new'):
        name = arguments.get('--name')
        location = json.loads(arguments.get('--location'))
        encryptor = json.loads(arguments.get('--encryptor'))
        new_identity(name, location, encryptor)

    if arguments.get('new-interactive'):
        new_interactive()

    if arguments.get('merge'):
        filename = arguments.get('<filename>')
        data_to_merge = json.load(sys.stdin)
        merge(filename, data_to_merge)

    if arguments.get('dl') or arguments.get('download'):
        default_server = 'http://localhost:16232'
        names  = arguments.get('<names>')
        server = arguments.get('--server') or default_server
        if not server.endswith('/'):
            server = server + '/'
        download_idents(names, server)

    if arguments.get('set'):
        args = json.loads(arguments.get('--args'))
        set_attribute(cache_source, arguments.get('<name>'), **args)

    if arguments.get('rm'):
        rm_identities(cache_source, arguments.get('-A'), *arguments.get('<names>'))

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