#!/usr/bin/env python3

import os
import yaml
import time
try:
    yaml.warnings({'YAMLLoadWarning': False})
except:
    pass
import tempfile
import logging
from neotermcolor import set_style, cprint

set_style('robogerctl:error', 'red', attrs='bold')
set_style('robogerctl:title', 'blue')
set_style('robogerctl:separator', 'grey')
set_style('robogerctl:attr')
set_style('robogerctl:value', 'yellow')

from pathlib import Path

dir_me = Path(__file__).absolute().parents[1]

default_timeout = 5

import sys
import argparse

logger = logging.getLogger('roboger')

ap = argparse.ArgumentParser()

ap.add_argument('--debug', help=f'client debug mode', action='store_true')
ap.add_argument(
    '-T',
    '--timeout',
    help=f'API request timeout (in seconds, default: {default_timeout})',
    type=float,
    default=default_timeout,
    metavar='SEC')
ap.add_argument('-F',
                '--config-file',
                help='alternative server/client config file',
                metavar='FILE')
ap.add_argument('-o',
                '--output',
                help='output',
                choices=['default', 'raw', 'json', 'yaml'],
                default='default')

sp = ap.add_subparsers(dest='command', help='command to execute')
sp.required = True

sp_test = sp.add_parser('test', help='test server')
sp_deploy = sp.add_parser('deploy', help='deploy configuration')
sp_deploy.add_argument('--delete-everything',
                       help='DELETE ALL RESOURCES on server before deployment',
                       action='store_true')
reqn = sp_deploy.add_argument_group('required named arguments')
ifile = reqn.add_argument('-f',
                          '--input-file',
                          help='input file ("stdin" for stdin)')
ifile.required = True
sp.add_parser('reset-addr-limits', help='reset address limits (if used)')
sp.add_parser('core-cleanup', help='core cleanup')

iap = sp.add_parser('plugin', help='plugin management', aliases=['plug', 'pl'])
isp = iap.add_subparsers(dest='func', help='plugin commands')
isp.required = True
isp.add_parser('list', help='list_plugins', aliases=['ls', 'l'])

for prs in [('addr', 'a'), ('endpoint', 'e', 'ep', 'end'),
            ('subscription', 's', 'sub')]:
    i = prs[0]
    iap = sp.add_parser(i, help=f'{i} management', aliases=prs[1:])
    funcs = [('list resources', 'list', 'ls', 'l'),
             ('create new sub-resource', 'create', 'cr'),
             ('describe resource', 'describe', 'des', 'desc', 'ds'),
             ('edit resource', 'edit', 'ed', 'e'),
             ('delete resource', 'delete', 'del'),
             ('disable resource', 'disable', 'dis'),
             ('enable resource', 'enable', 'en'),
             ('apply resource properties from JSON or YAML file', 'apply',
              'apl')]
    if i == 'addr':
        funcs.append(('re-generate address', 'change', 'ch'))
    elif i == 'endpoint':
        funcs.append((
            'copy subscriptions',
            'copysub',
        ))
    isp = iap.add_subparsers(dest='func', help=f'{i} commands')
    isp.required = True
    for fs in funcs:
        f = fs[1]
        x = isp.add_parser(f, help=fs[0], aliases=fs[2:])
        reqn = x.add_argument_group('required named arguments')
        if i != 'addr' or f not in ['list', 'create']:
            x.add_argument('resource', help='current/parent resource')
        if i == 'endpoint' and f == 'create':
            x.add_argument('plugin_name', help='plugin name')
        if i == 'addr' and f == 'change':
            x.add_argument('--to', help='change addres to the specified value')
        if i == 'endpoint' and f == 'copysub':
            x.add_argument('target_resource', help='current/parent resource')
            x.add_argument(
                '--replace',
                help='Replace (remove existing) target subscriptions',
                action='store_true')
        if f == 'apply':
            ifile = reqn.add_argument('-f',
                                      '--input-file',
                                      help='input file ("stdin" for stdin)')
            ifile.required = True

a = ap.parse_args()

if a.debug:
    logger.setLevel(level=logging.DEBUG)
    logging.basicConfig(level=logging.DEBUG)

if a.output == 'raw':
    import neotermcolor
    neotermcolor._isatty = False

import roboger_manager

if a.config_file:
    fname = a.config_file
else:
    fname = os.path.expanduser('~/.robogerctl.yml')
    if not Path(fname).exists():
        fname = dir_me / 'etc/roboger.yml'
    if not Path(fname).exists():
        fname = '/usr/local/etc/roboger.yml'
logger.debug(f'Client using config file {fname}')
with open(fname) as fh:
    config = yaml.load(fh.read())

format_uri = lambda uri: 'http://{}'.format(uri.replace('0.0.0.0', '127.0.0.1'))

if config.get('roboger'):
    api = roboger_manager.ManagementAPI(
        api_uri=format_uri(config['roboger']['gunicorn']['listen']),
        api_key=os.getenv('ROBOGER_MASTERKEY',
                          config['roboger']['master']['key']),
        timeout=a.timeout)
    limits = config['roboger'].get('limits')
else:
    api = roboger_manager.ManagementAPI(api_uri=config['uri'],
                                        api_key=os.getenv(
                                            'ROBOGER_MASTERKEY', config['key']),
                                        timeout=a.timeout)
    limits = config.get('limits')

if limits:
    roboger_manager.use_limits = True

roboger_manager.default_api = api

result = None

editor = os.environ.get('EDITOR', 'vi')


def format_dict(d, fields, xtra={}):
    from collections import OrderedDict
    d = dict(d)
    result = OrderedDict()
    for f in fields:
        if f in d.keys():
            result[f] = d[f]
        elif f in xtra.keys():
            result[f] = xtra[f]
    return result


def edit_obj(obj):

    def getch():
        import termios
        import tty
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        except:
            time.sleep(2)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch

    obj.load()
    fd, fname = tempfile.mkstemp('.yaml')
    try:
        with os.fdopen(fd, 'w') as tmp:
            fc = yaml.dump(obj.serialize(include_protected_fields=False),
                           default_flow_style=False)
            tmp.write(fc)
        from hashlib import sha256
        oldhash = sha256(fc.encode()).digest()
        while True:
            try:
                os.system(f'{editor} {fname}')
                with open(fname) as tmp:
                    fc = tmp.read()
                    newhash = sha256(fc.encode()).digest()
                if oldhash == newhash:
                    return ''
                else:
                    obj.load(data=yaml.load(fc), load_protected_fields=False)
                    obj.save()
                    return None
            except Exception as e:
                cprint(f'ERROR: {e}', '@robogerctl:error', file=sys.stderr)
                if a.debug:
                    import traceback
                    traceback.print_exc()
                getch()
    finally:
        os.unlink(fname)


def process_obj_func(obj, format_func):
    if a.func in ['delete', 'del']:
        obj.delete()
    elif a.func in ['describe', 'des', 'desc', 'ds']:
        obj.load()
        return format_func(obj)
    elif a.func in ['edit', 'ed']:
        return edit_obj(obj)
    elif a.func in ['disable', 'dis']:
        obj.disable()
    elif a.func in ['enable', 'en']:
        obj.enable()
    elif a.func in ['apply', 'apl']:
        if a.input_file == 'stdin':
            fc = sys.stdin.read()
        else:
            with open(a.input_file) as fh:
                fc = fh.read()
        obj.load(data=yaml.load(fc), load_protected_fields=False)
        obj.save()


def format_addr(obj):
    d = dict(obj)
    d['res'] = d['id']
    del d['id']
    return d


def format_endpoint(obj):
    d = dict(obj)
    d['res'] = f'{d["addr_id"]}.{d["id"]}'
    del d['id']
    del d['addr_id']
    return d


def format_subscription(obj):
    d = dict(obj)
    d['res'] = (f'{d["addr_id"]}.' f'{d["endpoint_id"]}.{d["id"]}')
    del d['id']
    del d['addr_id']
    return d


def split_res(res, amnt):
    if res.count('.') != amnt:
        raise ValueError
    result = res.split('.', amnt)
    for x in result:
        if not x:
            raise ValueError
    return result


try:
    if a.command == 'test':
        result = api.test()
    elif a.command == 'reset-addr-limits':
        result = api.reset_addr_limits()
    elif a.command == 'core-cleanup':
        result = api.core_cleanup()
    elif a.command == 'deploy':
        if a.input_file == 'stdin':
            fc = sys.stdin.read()
        else:
            with open(a.input_file) as fh:
                fc = fh.read()
        data = yaml.load(fc)
        result = []
        if a.delete_everything:
            roboger_manager.delete_everything(confirm='YES')
        try:
            for acfg in data.get('addrs', []):
                addr = roboger_manager.create_addr()
                result.append(addr)
                addr_config = acfg.copy()
                try:
                    del addr_config['endpoints']
                except:
                    pass
                addr.load(data=addr_config, load_protected_fields=False)
                addr.save()
                if 'a' in acfg:
                    addr.change(to=acfg['a'])
                for ecfg in acfg.get('endpoints', []):
                    ep = addr.create_endpoint(plugin_name=ecfg['plugin_name'])
                    ep_config = ecfg.copy()
                    del ep_config['plugin_name']
                    try:
                        del ep_config['subscriptions']
                    except:
                        pass
                    ep.load(data=ep_config, load_protected_fields=False)
                    ep.save()
                    for scfg in ecfg.get('subscriptions', []):
                        s = ep.create_subscription()
                        s.load(data=scfg, load_protected_fields=False)
                        s.save()
            result = {'deployed': [(a.id) for a in result]}
        except Exception as e:
            for addr in result:
                try:
                    addr.delete()
                except:
                    pass
            result = None
            raise
    elif a.command in ['plugin', 'plug', 'pl']:
        if a.func in ['list', 'ls', 'l']:
            result = [
                format_dict(d, ['plugin_name', 'description', 'version', 'url'])
                for d in api.list_plugins()
            ]
    elif a.command in ['addr', 'a']:
        if a.func in ['list', 'ls', 'l']:
            result = [
                format_dict(d, ['res', 'a', 'active', 'lim_c', 'lim_s'],
                            {'res': d['id']})
                for d in roboger_manager.list_addr()
            ]
        elif a.func in ['create', 'cr']:
            result = format_addr(roboger_manager.create_addr())
        else:
            addr = roboger_manager.Addr(id=a.resource)
            if a.func in ['change', 'ch']:
                addr.change(to=a.to)
                result = addr.a
            else:
                result = process_obj_func(addr, format_addr)
    elif a.command in ['endpoint', 'e', 'ep', 'end']:
        if a.func in ['list', 'ls', 'l']:
            addr = roboger_manager.Addr(id=a.resource)
            result = [
                format_dict(
                    ep,
                    ['res', 'active', 'plugin_name', 'config', 'description'],
                    {'res': f'{ep.addr_id}.{ep.id}'})
                for ep in addr.list_endpoints()
            ]
        elif a.func in ['create', 'cr']:
            addr = roboger_manager.Addr(id=a.resource)
            result = format_endpoint(
                addr.create_endpoint(plugin_name=a.plugin_name))
        else:
            try:
                addr_id, ep_id = split_res(a.resource, 1)
            except ValueError:
                raise Exception(
                    'Please specify correct resource path: addr_id.endpoint_id')
            ep = roboger_manager.Endpoint(id=ep_id, addr_id=addr_id)
            if a.func in ['copysub']:
                try:
                    addr2_id, ep2_id = split_res(a.target_resource, 1)
                except ValueError:
                    raise Exception('Please specify correct target '
                                    'resource path: addr_id.endpoint_id')
                ep2 = roboger_manager.Endpoint(id=ep2_id, addr_id=addr2_id)
                ep.copysub(target=ep2, replace=a.replace)
            else:
                result = process_obj_func(ep, format_endpoint)
    elif a.command in ['subscription', 's', 'sub']:
        if a.func in ['list', 'create', 'ls', 'l', 'cr']:
            try:
                addr_id, ep_id = split_res(a.resource, 1)
            except ValueError:
                raise Exception(
                    'Please specify correct resource path: addr_id.endpoint_id')
            ep = roboger_manager.Endpoint(id=ep_id, addr_id=addr_id)
            if a.func in ['list', 'ls', 'l']:
                result = [
                    format_dict(s, [
                        'res', 'active', 'location', 'tag', 'sender', 'level',
                        'level_match'
                    ], {'res': f'{s.addr_id}.{s.endpoint_id}.{s.id}'})
                    for s in ep.list_subscriptions()
                ]
            elif a.func in ['create', 'cr']:
                result = format_subscription(ep.create_subscription())
        else:
            try:
                addr_id, ep_id, s_id = split_res(a.resource, 2)
            except ValueError:
                raise Exception('Please specify correct resource path: '
                                'addr_id.endpoint_id.subscription_id')
            s = roboger_manager.Subscription(id=s_id,
                                             addr_id=addr_id,
                                             endpoint_id=ep_id)
            result = process_obj_func(s, format_subscription)
except Exception as e:
    cprint(f'ERROR: {e}', '@robogerctl:error', file=sys.stderr)
    if a.debug:
        raise
    sys.exit(1)

if result:
    if a.output == 'yaml':
        print(yaml.dump(result, default_flow_style=False))
    elif a.output == 'json':
        import json
        print(json.dumps(result, indent=True, sort_keys=True))
    else:
        if isinstance(result, dict):
            from collections import OrderedDict
            import rapidtables
            tbl = [
                OrderedDict(attr=k, value=result[k])
                for k in sorted(result.keys())
                if k != 'res'
            ]
            if 'res' in result.keys():
                tbl.insert(0, OrderedDict(attr='res', value=result['res']))
            import rapidtables
            header, rows = rapidtables.format_table(
                tbl, fmt=rapidtables.FORMAT_GENERATOR_COLS)
            spacer = '  '
            cprint(spacer.join(header), '@robogerctl:title')
            cprint('-' * sum([(len(x) + 2) for x in header]),
                   '@robogerctl:separator')
            for r in rows:
                cprint(r[0] + '  ', '@robogerctl:attr', end='')
                cprint(r[1], '@robogerctl:value')
        elif isinstance(result, list):
            import rapidtables
            header, tbl = rapidtables.format_table(
                result, fmt=rapidtables.FORMAT_GENERATOR)
            cprint(header, '@robogerctl:title')
            cprint('-' * len(header), '@robogerctl:separator')
            for t in tbl:
                print(t)
        else:
            print(result)
elif result != '' and result != []:
    print('OK')
