#!/usr/bin/env python
import argparse
import logging
import logging.config
import sys

import trio
import trio_asyncio

from syncrypt import __version__
from syncrypt.app import SyncryptCLIApp
from syncrypt.config import AppConfig
from syncrypt.exceptions import SyncryptBaseException
from syncrypt.utils.logging import setup_logging

try:
    import win_unicode_console; win_unicode_console.enable()
except ImportError:
    pass


COPYRIGHT = '''Syncrypt v{version}
(c) 2016-2019 Syncrypt UG | https://syncrypt.space
Licensed under the GPLv3 | Source: https://github.com/syncrypt/client'''\
    .format(version=__version__)


LOGLEVELS = ['CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG']

logger = logging.getLogger('syncrypt')


class SyncryptCmd():
    command = None
    description = None

    def configure_parser(self, parser):
        parser.add_argument('-l', metavar='LOGLEVEL', type=str, default='INFO',
                dest='loglevel', choices=LOGLEVELS,
                help='log level: ' + ', '.join(LOGLEVELS)+ ' (default: INFO)')
        parser.add_argument('-c', metavar='CONFIG', type=str, default=None,
                dest='config', help='work with another config file')

    def __init__(self):
        self.parser = argparse.ArgumentParser(
                description=self.description,
                prog='%s %s' % ('syncrypt', self.command)
            )
        self.configure_parser(self.parser)
        self.config = None
        self.vault_dirs = []

    def parse(self, args):
        self.config = self.parser.parse_args(args)

    async def setup(self, nursery):
        self.app_config = AppConfig(self.config.config)
        setup_logging(self.config.loglevel)
        self.app = SyncryptCLIApp(self.app_config, vault_dirs=self.vault_dirs,
                nursery=nursery)
        await self.app.initialize()

    async def shutdown(self):
        await self.app.close()

    async def run(self):
        raise NotImplementedError()


class SingleVaultCmd(SyncryptCmd):
    '''command that supports only a single vault directory'''

    def __init__(self):
        super(SingleVaultCmd, self).__init__()
        # By default, we operate in the current directory
        self.vault_dirs = ['.']

    def configure_parser(self, parser):
        super(SingleVaultCmd, self).configure_parser(parser)
        parser.add_argument('-d', metavar='DIRECTORY', type=str,
                default='.', dest='directory', help='directory (default: .)')

    async def setup(self, nursery):
        self.vault_dirs = [self.config.directory]
        await super(SingleVaultCmd, self).setup(nursery)


class MultipleVaultCmd(SyncryptCmd):
    '''command that supports multiple vault directories'''

    def __init__(self):
        super(MultipleVaultCmd, self).__init__()
        # By default, we operate in the current directory
        self.vault_dirs = ['.']

    def configure_parser(self, parser):
        super(MultipleVaultCmd, self).configure_parser(parser)
        parser.add_argument('-d', metavar='DIRECTORY', type=str,
                action='append', dest='directory', help='directory (default: .)')

    async def setup(self, nursery):
        # For now, each command will work on a default AppConfig object. When
        # you want app-level configuration, use ``syncrypt_daemon``.
        if self.config.directory: # not None and not empty
            self.vault_dirs = self.config.directory
        await super(MultipleVaultCmd, self).setup(nursery)


class NoVaultCmd(SyncryptCmd):
    '''command that operates without a vault'''


class Clone(NoVaultCmd):
    command = 'clone'
    description = 'clone a remote vault to a local directory'

    def configure_parser(self, parser):
        super(Clone, self).configure_parser(parser)
        parser.add_argument('vault_id', help='vault id or name')
        parser.add_argument('directory', nargs='?', help='local directory')

    async def run(self):
        try:
            from uuid import UUID
            vault_id = str(UUID(self.config.vault_id))
            vault_name = None
        except ValueError:
            vault_id = None
            vault_name = self.config.vault_id

        if vault_id:
            vault = await self.app.clone(vault_id, self.config.directory or vault_id)
        else:
            vault = await self.app.clone_by_name(vault_name, self.config.directory or vault_name)


class Pull(MultipleVaultCmd):
    command = 'pull'
    description = 'pull all files from the latest revision'

    def configure_parser(self, parser):
        super(Pull, self).configure_parser(parser)
        parser.add_argument('-f', '--full', action='store_true',
                help='retrieve complete history instead of changes')

    async def run(self):
        await self.app.pull(full=self.config.full)


class AddUser(SingleVaultCmd):
    command = 'add-user'
    description = 'add another user to this vault'

    def configure_parser(self, parser):
        super(AddUser, self).configure_parser(parser)
        parser.add_argument('email', help='the user\'s email')

    async def run(self):
        await self.app.add_user(self.config.email)


class RemoveFile(SingleVaultCmd):
    command = 'remove-file'
    description = 'remove a file from the vault'

    def configure_parser(self, parser):
        super(RemoveFile, self).configure_parser(parser)
        parser.add_argument('path', nargs='+', help='path to the file(s)')

    async def run(self):
        await self.app.remove_files(self.config.path)


class DeleteVault(MultipleVaultCmd):
    command = 'delete-vault'
    description = 'permanently delete this vault from server (use with care)'

    async def run(self):
        await self.app.delete_vaults()


class UploadVaultKey(SingleVaultCmd):
    command = 'upload-vault-key'
    description = 'upload the vault key encrypted with my user key'

    async def run(self):
        await self.app.upload_vault_key()


class ListVaults(NoVaultCmd):
    command = 'list-vaults'
    description = 'list vaults'

    def configure_parser(self, parser):
        super(ListVaults, self).configure_parser(parser)
        parser.add_argument('-a', '--all', dest='all', action='store_true',
                help='also list vaults without key')

    async def run(self):
        if self.config.all:
            await self.app.print_list_of_all_vaults()
        else:
            await self.app.print_list_of_vaults()


class ListKeys(NoVaultCmd):
    command = 'list-keys'
    description = 'list keys'

    def configure_parser(self, parser):
        super(ListKeys, self).configure_parser(parser)
        parser.add_argument('-u', '--user', action='store', dest='user',
                help='user email to list keys for')
        parser.add_argument('--upload', action='store_true',
                help='upload your public keys to the server')
        parser.add_argument('--art', action='store_true', dest='art',
                help='show ascii art for each key')

    async def run(self):
        if self.config.upload:
            await self.app.upload_identity()
        else:
            await self.app.list_keys(self.config.user, with_art=self.config.art)

class Push(MultipleVaultCmd):
    command = 'push'
    description = 'push local changes to the server'

    def configure_parser(self, parser):
        super(Push, self).configure_parser(parser)

    async def run(self):
        await self.app.push()


class Init(SingleVaultCmd):
    command = 'init'
    description = 'register the directory as a Syncrypt vault'

    def configure_parser(self, parser):
        super(Init, self).configure_parser(parser)
        parser.add_argument('--host', help='remote host (default: storage.syncrypt.space)')
        parser.add_argument('-k', '--upload-vault-key', action='store_true',
                dest='upload_vault_key', help='upload encrypted vault key')

    async def run(self):
        await self.app.init(
                host=self.config.host,
                upload_vault_key=self.config.upload_vault_key
            )


class Info(MultipleVaultCmd):
    command = 'info'
    description = 'show vault information'

    async def run(self):
        await self.app.info()


class ConfigSet(SingleVaultCmd):
    command = 'set'
    description = 'set a vault config parameter'

    def configure_parser(self, parser):
        super(ConfigSet, self).configure_parser(parser)
        parser.add_argument('setting', help='the thing to set')
        parser.add_argument('value', help='the value to set it to')

    async def run(self):
        await self.app.set(self.config.setting, self.config.value)


class ConfigUnset(SingleVaultCmd):
    command = 'unset'
    description = 'unset a vault config parameter'

    def configure_parser(self, parser):
        super(ConfigUnset, self).configure_parser(parser)
        parser.add_argument('setting', help='the thing to unset')

    async def run(self):
        await self.app.unset(self.config.setting)


class Log(MultipleVaultCmd):
    command = 'log'
    description = 'show recent changes (file uploads, deletions, etc)'

    def configure_parser(self, parser):
        super(Log, self).configure_parser(parser)
        parser.add_argument('-v', '--verbose', action='store_true',
                help='print more information')

    async def run(self):
        await self.app.print_log(verbose=self.config.verbose)


class Login(NoVaultCmd):
    command = 'login'
    description = 'login to server and store auth token'

    async def run(self):
        await self.app.login()


class UploadKey(NoVaultCmd):
    command = 'upload-key'
    description = 'uploads your current public user key'

    async def run(self):
        await self.app.upload_identity()


class Register(NoVaultCmd):
    command = 'register'
    description = 'register a new user'

    async def run(self):
        await self.app.register()


class Logout(NoVaultCmd):
    command = 'logout'
    description = 'logout and remove auth token'

    async def run(self):
        await self.app.logout()


class ExportVault(SingleVaultCmd):
    command = 'export-vault'
    description = 'export vault config and keys to backup or share'

    def configure_parser(self, parser):
        super(ExportVault, self).configure_parser(parser)
        parser.add_argument('-o', '--output', dest='filename', help='export filename')

    async def run(self):
        await self.app.export_package(self.config.filename)


class ImportVault(SingleVaultCmd):
    command = 'import-vault'
    description = 'import vault package that has previously been exported'

    def configure_parser(self, parser):
        super(ImportVault, self).configure_parser(parser)
        parser.add_argument(dest='filename', help='filename of the package')
        parser.add_argument(dest='target', help='vault folder to create')

    async def run(self):
        await self.app.import_package(self.config.filename, self.config.target)


class CheckUpdate(NoVaultCmd):
    command = 'check-update'
    description = 'compare the installed version to the latest one'

    async def run(self):
        await self.app.check_update()


class GenerateKey(NoVaultCmd):
    command = 'generate-key'
    description = 'generate your user key'

    async def run(self):
        await self.app.identity.generate_keys()


class ExportKey(NoVaultCmd):
    command = "export-key"
    description = "export your user-key"

    def configure_parser(self, parser):
        super(ExportKey, self).configure_parser(parser)
        parser.add_argument(dest="filename", help="filename of the package")

    async def run(self):
        await self.app.export_user_key(self.config.filename)


class ImportKey(NoVaultCmd):
    command = "import-key"
    description = "import your user-key"

    def configure_parser(self, parser):
        super(ImportKey, self).configure_parser(parser)
        parser.add_argument(dest="filename", help="filename of the package")

    async def run(self):
        await self.app.import_user_key(self.config.filename)


COMMANDS = [
    # Administrative
    Register(),
    Login(),
    Logout(),
    CheckUpdate(),

    # Vault
    Init(),
    Pull(),
    Push(),
    ListVaults(),
    ExportVault(),
    ImportVault(),
    UploadVaultKey(),
    DeleteVault(),
    RemoveFile(),
    Clone(),
    AddUser(),
    Info(),
    Log(),
    ConfigSet(),
    ConfigUnset(),

    # Key Management
    UploadKey(),
    ListKeys(),
    GenerateKey(),
    ExportKey(),
    ImportKey(),
]
COMMAND_NAMES = [c.command for c in COMMANDS]

global_parser = argparse.ArgumentParser(
    formatter_class=argparse.RawDescriptionHelpFormatter,
    usage='''syncrypt <command> [<args>]

''' + COPYRIGHT  + '''

available commands:
''' + '\n'.join([
    '  {0.command:11s} {0.description}'.format(c) for c in COMMANDS
    ]))

global_parser.add_argument('command', help='Subcommand to run', choices=COMMAND_NAMES, nargs='?')
global_parser.add_argument('-v', '--version', action='store_true', help='Show version information')
global_parser.add_argument('--short-version', action='store_true', help='Show short version information')

async def run_command(cmd: SyncryptCmd):
    async with trio.open_nursery() as nursery:
        await cmd.setup(nursery)
        try:
            await cmd.run()
        except SyncryptBaseException as e:
            # For SyncryptBaseExceptions we will output the abbreviated form. Full stack
            # backtrace can be printed by running the command with -l DEBUG
            logger.debug('Command failed because of the following exception:', exc_info=e)
            logger.error(str(e))
        await cmd.shutdown()

if __name__ == '__main__':
    # setlocale() is called here so that strftime will use the correct user
    # locale when formatting datetime objects.
    import locale
    locale.setlocale(locale.LC_TIME, '')

    args = global_parser.parse_args(sys.argv[1:2])

    if args.version:
        print(COPYRIGHT)
    elif args.short_version:
        print(__version__)
    elif args.command:
        for c in COMMANDS:
            if c.command == args.command:
                c.parse(sys.argv[2:])
                trio_asyncio.run(run_command, c)
    else:
        global_parser.print_help()
