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

import os
from functools import partial

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.health
import defence360agent.simple_rpc.hosting_panel
import defence360agent.simple_rpc.kcarectl
import defence360agent.simple_rpc.thirdparty
from defence360agent import server
from defence360agent.contracts.config import (
    Core as Config,
    LicenseCLN,
    get_product_name,
)
from defence360agent.contracts import sentry
from defence360agent.internals import geo
from defence360agent.internals import strategy
from defence360agent.simple_rpc import (
    SUCCESS,
    RpcClient,
    SocketError,
)
from defence360agent.simple_rpc.utils import prepare_schema
from defence360agent.subsys.panels import hosting_panel
from defence360agent.utils import (
    antivirus_mode,
    get_external_ip,
    is_root_user,
    stub_unexpected_error,
)
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
    import defence360agent.simple_rpc.remote_proxy

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 _fill_sentry_tags() -> None:
    @stub_unexpected_error
    def _get_hosting_panel():
        return hosting_panel.HostingPanel().NAME

    def _is_firewalld_running() -> bool:
        try:
            subprocess.check_output(['firewall-cmd', '--state'],
                                    stderr=subprocess.DEVNULL)
            return True
        except (IOError, subprocess.CalledProcessError):
            return False

    def _is_csf_running() -> bool:
        try:
            out = subprocess.check_output(['/usr/sbin/csf', '--status'],
                                          stderr=subprocess.DEVNULL)
        except (FileNotFoundError, subprocess.CalledProcessError):
            return False
        return (b'have been disabled' not in out) and \
            (b'You have an unresolved error when starting csf:' not in out)

    @stub_unexpected_error
    def _get_current_firewall():
        if _is_csf_running():
            return 'csf'
        if _is_firewalld_running():
            return 'firewalld'
        return 'iptables'

    sentry.set_version(Config.VERSION)
    sentry.set_product_name(get_product_name())
    if not is_root_user():
        return
    sentry.set_server_id(LicenseCLN.get_server_id())
    fw = _get_current_firewall()
    sentry.set_firewall_type(fw)
    if fw == 'csf':
        sentry.set_strategy(strategy.Strategy.CSF_COOP_STRATEGY)
    else:
        sentry.set_strategy(strategy.Strategy.PRIMARY_IDS_STRATEGY)
    sentry.set_ip(stub_unexpected_error(get_external_ip)())
    sentry.set_hosting_panel(_get_hosting_panel())


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

    _fill_sentry_tags()
    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()
