import argparse
import json
import logging
import os
import sys
from vstorage.roles_cfg.enums import Roles, RESERVED_ROLE_NAMES, PREDEFINED_ROLE_NAMES
from vstorage.roles_cfg.roles_storage import RolesStorage, RolesCreator
from vstorage.roles_cfg.utils.constants import IPTABLES_PERSIST_CONFIG
from vstorage.roles_cfg.utils.iputils import PortRange, OutboundAllowRule


class ArgumentException(Exception):
    pass

class ArgumentParser(argparse.ArgumentParser):
    def error(self, message):
        logger.error(message)
        self.print_usage(sys.stdout)
        self.exit(1)

def check_interface(iface):
    if not iface:
        raise ArgumentException('Argument --interface is required')

def parse_and_create_roles(args_roles):
    if not args_roles:
        return []
    roles = []
    for r in args_roles.split(','):
        role_list = r.split(':', 1)
        name = role_list[0]
        port = role_list[1] if (len(role_list) > 1 and role_list[1] != '') else None

        if name in [role.name for role in roles]:
            logger.error('Role name "{}" is already defined'.format(name))
            sys.exit(1)

        if name in RESERVED_ROLE_NAMES:
            logger.error('Role name "{}" is reserved'.format(name))
            sys.exit(1)

        if name in PREDEFINED_ROLE_NAMES and port is not None:
            logger.error('Cannot set port for predefined role "{}"'.format(name))
            sys.exit(1)

        role = RolesCreator.create_role(name)
        if port is not None:
            role.add_rule(proto='tcp', dport=PortRange(port))
            role.add_rule(proto='udp', dport=PortRange(port))

        roles.append(role)
    return roles

def parse_and_create_outbound_allow_rules(args_outbound_allow_list):
    if args_outbound_allow_list is None:
        return None
    outbound_allow_rules = []
    for rule in args_outbound_allow_list.split(','):
        rule_obj = OutboundAllowRule.from_string(rule).as_rule
        # Remove duplicates
        if rule_obj not in outbound_allow_rules:
            outbound_allow_rules.append(rule_obj)
    return outbound_allow_rules

def parse_roles_names(args_roles):
    if not args_roles:
        return []
    roles_names = args_roles.split(',')
    return roles_names

def print_roles_hashes(iface_hashes, role_hashes):
    row_format ="{:<25}{:<10}"
    out = "Interfaces:\n"
    for entry in iface_hashes:
        out += (row_format.format(entry[0], entry[1]) + "\n")
    out += "\nRoles:\n"
    for entry in role_hashes:
        out += (row_format.format(entry[0], entry[1]) + "\n")
    sys.stdout.write(out)


usage_template = """
{script_name} command [-i,--interface DEV, -r,--roles ROLE,...]

Roles configuration tool for VStorage

Commands:
  set                apply new set of roles
  append             append roles to existing set
  sync               sync roles database and iptables
  delete             remove roles from existing set
  find               find devices with specific roles
  get                get roles of specific adapter
  cleanup            remove roles and all firewall rules
  backup             save roles.json and iptables rules to the file
  restore            restore roles.json and iptables rules
  show-hashes        show hashes of role names

Options:
  -i, --interface DEV                  name of network interface to configure
  -r, --roles ROLE,ROLE,...            network roles for interface delimited by comma
              for set and append operations for custom roles you can specify a port
              ROLE                     for predefined roles
              ROLE:PORT                for custom roles
  --outbound-allow-list RULE,RULE,...  allow rules
  Predefined roles: {roles_list}
  -d, --debug                      set debug loglevel
  -o, --offline                    do not modify runtime config, read and write changes to {ipcfg} only
"""

def main():
    os.putenv('LANG', 'C')
    usage = usage_template.format(script_name=os.path.basename(__file__),
        roles_list=[r.value for r in Roles],
        ipcfg=IPTABLES_PERSIST_CONFIG)
    parser = ArgumentParser(usage=usage, add_help=False)
    parser.add_argument('command', type=str,
                        choices=['set',
                                 'append',
                                 'sync',
                                 'delete',
                                 'find',
                                 'get',
                                 'cleanup',
                                 'backup',
                                 'restore',
                                 'show-hashes'])
    parser.add_argument('-i', '--interface', dest='interface',
                        type=str, required=False)
    parser.add_argument('-r', '--roles', dest='roles',
                        type=str, required=False)
    parser.add_argument('--outbound-allow-list', dest='outbound_allow_list',
                        type=str, required=False)
    parser.add_argument('-d', '--debug', dest='debug',
                        action='store_true', required=False)
    parser.add_argument('-o', '--offline', dest='offline',
                        action='store_true', required=False)

    args = parser.parse_args()
    if args.debug:
        logger.setLevel(logging.DEBUG)

    roles_storage = RolesStorage(logger, args.offline)
    try:
        if args.command == 'set':
            check_interface(args.interface)
            roles_storage.set(device=args.interface,
                              roles=parse_and_create_roles(args.roles),
                              outbound_allow_list=parse_and_create_outbound_allow_rules(
                                  args.outbound_allow_list)
                             )
        elif args.command == 'append':
            roles_storage.get(args.interface)
            check_interface(args.interface)
            roles_storage.append(device=args.interface,
                                 roles=parse_and_create_roles(args.roles),
                                 outbound_allow_list=parse_and_create_outbound_allow_rules(
                                     args.outbound_allow_list)
                                )
        elif args.command == 'sync':
            roles_storage.sync(args.interface)
        elif args.command == 'delete':
            check_interface(args.interface)
            roles_storage.remove(device=args.interface,
                                 roles_names=parse_roles_names(args.roles),
                                 outbound_allow_list=parse_and_create_outbound_allow_rules(
                                     args.outbound_allow_list)
                                )
        elif args.command == 'find':
            out = {}
            out['result'] = roles_storage.find_devices_by_roles_names(parse_roles_names(args.roles))
            sys.stdout.write(json.dumps(out))
        elif args.command == 'get':
            check_interface(args.interface)
            (roles, inbound_allow_list,
             inbound_deny_list, outbound_allow_list) = roles_storage.get(args.interface)
            out_roles = [r.to_dict() for r in roles]
            out_inbound_allow_list = [i.to_dict() for i in inbound_allow_list]
            out_inbound_deny_list = [i.to_dict() for i in inbound_deny_list]
            out_outbound_allow_list = [i.to_dict() for i in outbound_allow_list]

            out = {'result': dict()}
            out['result']['roles'] = out_roles
            out['result']['inbound_allow_list'] = out_inbound_allow_list
            out['result']['inbound_deny_list'] = out_inbound_deny_list
            out['result']['outbound_allow_list'] = out_outbound_allow_list
            sys.stdout.write(json.dumps(out))
        elif args.command == 'cleanup':
            roles_storage.cleanup_interface(args.interface)
        elif args.command == 'backup':
            roles_storage.backup()
        elif args.command == 'restore':
            roles_storage.restore()
        elif args.command == 'show-hashes':
            print_roles_hashes(*roles_storage.get_iface_and_roles_hashes())
        else:
            raise ArgumentException('Unknown command: {}'.format(args.command))

    except ArgumentException as err:
        logger.error('{}'.format(err))
        sys.exit(1)

    except Exception as err:
        logger.error(str(err), exc_info=True)
        sys.exit(1)

    sys.exit(0)

if __name__ == '__main__':
    logger = logging.getLogger("RolesCfgLogger")
    h = logging.StreamHandler(sys.stdout)
    h.setFormatter(logging.Formatter('%(asctime)s.%(msecs)03d %(levelname)s [roles_cfg] %(message)s',
                                     datefmt='%Y-%m-%d %H:%M:%S'))
    logger.addHandler(h)
    logger.setLevel(logging.INFO)
    main()
