#!/opt/alt/python35/bin/python3
import argparse
import asyncio
import logging
import sys
from contextlib import suppress

import os
from functools import partial

from defence360agent.contracts.config import Core as Config

import defence360agent.internals.logger
import defence360agent.malwarelib.rpc.endpoints
import defence360agent.feature_management.rpc.endpoints
import defence360agent.simple_rpc.configuration_management
import defence360agent.simple_rpc.dashboard
import defence360agent.simple_rpc.endpoints
import defence360agent.simple_rpc.feature
import defence360agent.simple_rpc.hosting_panel
import defence360agent.simple_rpc.kcarectl
import defence360agent.simple_rpc.remote_proxy
import defence360agent.simple_rpc.thirdparty
from defence360agent import server
from defence360agent.internals import geo
from defence360agent.simple_rpc import (
    SUCCESS,
    RpcClient,
    SocketError,
)
from defence360agent.simple_rpc.utils import prepare_schema
from defence360agent.utils import antivirus_mode, is_root_user
from defence360agent.utils.cli import (
    EXITCODE_GENERAL_ERROR,
    EXIT_CODES,
    print_response,
    print_error,
)
from defence360agent.utils.validate import is_valid_ipv4_addr

if antivirus_mode.disabled:
    # TODO: move more Im360 related RPC entries here
    import defence360agent.simple_rpc.rpc360.proactive

logger = logging.getLogger(__name__)


def _filter_user(schema, user):
    for key, values in schema.items():
        if user in values.get('cli', {}).get('users', []):
            yield key, values


def generate_params(arguments, _parser):
    kwargs = {}
    for argument in arguments:
        arg_parser_argument = argument.replace('-', '_')
        value = getattr(_parser, arg_parser_argument, None)
        if value is not None:
            kwargs[argument] = value

    return kwargs


def generate_entrypoint(command, arguments, require_rpc, _parser):
    params = generate_params(arguments, _parser)
    result, data = RpcClient(require_svc_is_running=require_rpc) \
        .cmd(*command)(**params)
    if result == SUCCESS:
        print_response(command, data, _parser.json, _parser.verbose)
    else:
        print_error(result, data, _parser.json, _parser.verbose)
        sys.exit(EXIT_CODES[result])


def schema_to_argparser(parser, argument, options):
    argparse_options = {}

    if options.get('type') == 'boolean':
        bool_parser = parser.add_mutually_exclusive_group(
            required=options.get('required')
        )
        bool_parser.add_argument(
            '--' + argument.replace('_', '-'),
            dest=argument,
            action='store_true'
        )
        bool_parser.add_argument(
            '--no-' + argument.replace('_', '-'),
            dest=argument,
            action='store_false'
        )
        bool_parser.set_defaults(**{argument: options.get('default')})
        return

    if options.get('positional', False):
        argname = argument
        # 'required' is an invalid argument for positionals
        options.pop('required', None)
    else:
        argname = '--' + argument.replace('_', '-')

    if options.get('type') == 'list':
        argparse_options['nargs'] = '+' if options.get('required') else '*'
        argparse_options['metavar'] = argument.upper()
    else:
        if options.get('required'):
            argparse_options['required'] = options['required']

    if options.get('default') is not None:
        argparse_options['default'] = options['default']

    if options.get('help'):
        argparse_options['help'] = options['help']

    if options.get('allowed'):
        argparse_options['choices'] = options['allowed']

    parser.add_argument(argname, **argparse_options)


def apply_parser(subparsers, schema):
    _subparsers = {}
    commands = sorted(schema.keys())
    for methods in commands:
        values = schema[methods]
        assert isinstance(methods, (tuple, list))
        parser = None

        # generate subparsers
        subparser = subparsers
        for i, command in enumerate(methods):
            # last element
            if i == len(methods) - 1:
                parser = subparser.add_parser(
                    name=command,
                    help=values.get('help'))
                if any((c != methods and methods == c[:len(methods)])
                       for c in commands):
                    _subparsers[methods] = parser.add_subparsers(
                        help='Available commands'
                    )
            else:
                # does't need to create new subparser for each command
                # argparser doesn't work, need to reuse created subparsers
                hashable = tuple(methods[:i + 1])
                exists_subparser = _subparsers.get(hashable)
                if not exists_subparser:
                    subparser = _subparsers[hashable] = subparser.add_parser(
                        command
                    ).add_subparsers(
                        help='Available commands'
                    )
                else:
                    subparser = exists_subparser

        assert parser, 'parser is not defined'

        # generate arguments
        for argument, options in values.get('schema', {}).items():
            schema_to_argparser(parser, argument, options)

        parser.add_argument('--json', action='store_true',
                            help="return data in JSON format")
        parser.add_argument('--verbose', '-v', action='count')

        require_rpc = values.get('cli', {}).get('require_rpc', 'running')
        parser.set_defaults(call=partial(
            generate_entrypoint,
            methods,
            values.get('schema', {}).keys(),
            require_rpc
        ))


def _apply_subparsers(subparsers, user):
    server.create_parsers(subparsers)

    schema = dict(_filter_user(prepare_schema(), user))
    apply_parser(subparsers, schema)


def main(cli_args):
    # get ready to start: set conservative umask
    os.umask(Config.FILE_UMASK)

    defence360agent.internals.logger.reconfigure()

    parser = argparse.ArgumentParser(
        description='CLI for %s.' % Config.NAME)

    parser.add_argument('--console-log-level',
                        choices=['ERROR', 'WARNING', 'INFO', 'DEBUG'],
                        help="Level of logging input to the console")
    parser.add_argument(
        '--remote-addr',
        type=lambda ip: ip if is_valid_ipv4_addr(ip) else None,
        help="Client's IP address for adding it to the whitelist"
    )
    subparsers = parser.add_subparsers(help='Available commands')
    _apply_subparsers(subparsers, 'root')

    args = parser.parse_args(args=cli_args)

    # DEPRECATED: after full migration to socket integration need to remove
    if getattr(args, 'remote_addr', False):
        if is_root_user():
            with suppress(SocketError), geo.reader() as geo_reader:
                RpcClient().whitelist_current_user(
                    ip=args.remote_addr,
                    country=geo_reader.get_id(args.remote_addr))
    # <--

    if args.console_log_level:
        defence360agent.internals.logger.setConsoleLogLevel(
            args.console_log_level
        )
    if hasattr(args, "call"):
        try:
            args.call(args)
        except SocketError as e:
            print_response(
                None, {"items": "ERROR: {}".format(e)},
                args.json, args.verbose)
            exit(EXITCODE_GENERAL_ERROR)
    else:
        parser.print_help()


def entrypoint():
    if not is_root_user():
        logger.info('%s could be used by the root user only!', Config.NAME)
        sys.exit(EXITCODE_GENERAL_ERROR)
    try:
        main(sys.argv[1:])
        # ensure loop is closed to prevent asyncio warning
        # (https://bugs.python.org/issue23548)
        asyncio.get_event_loop().close()
    except Exception as e:
        logger.info('Unknown error happened. See logs for more information')
        logger.exception(e)
        sys.exit(EXITCODE_GENERAL_ERROR)


if __name__ == '__main__':
    entrypoint()
