import os
import re
import sys

import yaml
import socket
import ipaddress

import service_utils
import logger_utils
from servicelab import settings
from servicelab.stack import Context

slab_logger = logger_utils.setup_logger(settings.verbosity, 'stack.utils.tc_vm_yaml_create')
ctx = Context()


def open_yaml(filename):
    """Opens yaml file into dictionary

    Args:
        filename {str}: File path and name to open

    Returns:
        {dict}: PyYaml extracted dict of data from the opened file

    Example Usage:
        file_data = open_yaml('/some/path/to/my/environment.yaml')
    """
    try:
        with open(filename, 'r') as stream:
            return yaml.load(stream)
    except IOError:
        slab_logger.error('Unable to open %s' % filename)
        return 1


def write_file(yaml_data, output_file):
    """Write the host.yaml file to the tenant cloud hosts.d directory.  Checks first to make
       sure the file does not already exist.

       **** NOTE ****
       The dictionary keys and subkeys will be written in alphabetical order.

    Args:
        yaml_data {dict}: Data generated by build_yaml_data subroutine
        output_file {str}: Full path and name of output file

    Returns:
        Nothing.  Writes file in tenant cloud hosts.d directory

    Example Usage:
        write_file(yaml_data, output_file)
    """
    slab_logger.log(15, 'Writing %s as yaml from provided dictionary' % output_file)
    if os.path.isfile(output_file):
        slab_logger.error(
            '%s already exists.  Aborting host create.' %
            output_file)
        return 1
    # default_flow_style=False breaks lists into individual lines with leading '-'
    with open(output_file, 'w') as outfile:
        outfile.write(yaml.dump(yaml_data, default_flow_style=False))
    slab_logger.log(25, output_file)
    slab_logger.log(25, 'File created successfully')
    slab_logger.debug('%s created succesfully' % output_file)


def find_vlan(source_data):
    """Determines which vlan the IP belongs to from the env data.  Generates a list of all
       IPs for each vlan found within the dictionary provided, then checks if the supplied
       IP is in the list.

    Args:
       source_data {dict}: Keys needed:
          ip: IP address in standard four octect format, no mask
          <vlan-ids>: Number of the vlan for the keys, subnet with mask for the value
          tc_name: Name of the tenant cloud being worked on.  Used for error message.

    Returns:
        vlan {str}: VLAN ID as extracted from the environment.yaml or '1' if no valid match

    Example Usage:
        vlan_id = find_vlan(source_data)
    """
    my_ip = ipaddress.IPv4Address(unicode(source_data['ip']))
    slab_logger.log(15, 'Determining the vlan id for %s' % source_data['ip'])
    for key in source_data:
        # Regex search for all keys that are only numbers
        if re.search('^\d+$', key):
            subnet = ipaddress.IPv4Network(unicode(source_data[key]))
            subnet_ips = list(subnet.hosts())
            if my_ip in subnet_ips:
                return key

    slab_logger.error('Unable to find the vlan for %s within %s'
                      % (source_data['ip'], source_data['tc_name']))
    return 1


def input_vlan(source_data):
    """Manual input of vlan subnet data for vlans that are not found in the ccs-data
       environment.yaml for the env

    Args:
        source_data {dict}: Keys needed:
            vlan_id {int}: Vlan id number
            sc_name {str}: Name of the service cloud

    Returns:
        subnet {str}: Vlan subnet with /mask IE 10.20.30.0/24

    Example Usage:
        my_dict = {'vlan_id': 7,
                   'sc_name': 'rtp10-svc-1',}
        subnet = input_vlan(my_dict)
        >>> Unable to find data needed for vlan 7.  Please supply the subnet
        >>> information from a "neutron net-list" run from rtp10-svc-1 infra node
        >>> Input vlan subnet information: <user_input>
    """
    done = False
    slab_logger.log(25, 'Unable to find data needed for vlan %s.  Please supply the subnet\n'
                    'information from a "neutron net-list" run from %s infra node'
                    % (source_data['vlan_id'], source_data['sc_name']))
    while not done:
        subnet_input = raw_input('Input vlan subnet information: ')
        try:
            subnet = ipaddress.IPv4Network(unicode(subnet_input))
            if subnet.prefixlen > 28:
                slab_logger.log(25, '%i is an invalid mask' % subnet.prefixlen)
            else:
                done = True
        except ValueError:
            slab_logger.log(25, '%s is not a valid subnet.  Please try again' % subnet_input)
    return str(subnet.with_prefixlen)


def find_ip(env_path, vlan):
    """Finds the first unassigned IP in the selected vlan.  Searches all host.yamls in the
       service cloud environments subdirs, and does a host lookup on the remaining IPs

    Args:
       env_path {str}: path to service cloud env - ccs-data/sites/sc/environments
       vlan {obj}: ipaddress object of vlan subnet data

    Returns:
       returncode {int}: 0 for success, 1 for failure
       ip {str}: First unused / unassigned IP from vlan

    Example Usage:
        find_ip('<environments path>, ipaddress.IPv4Network(unicode(10.11.12.0/24))
    """
    slab_logger.log(15, 'Finding next available IP in vlan %s' % vlan)
    # Create list of ipaddress module objects of all valid IPs in the subnet
    all_ips = list(vlan.hosts())
    # Remove the first 4 IPs.  They *should* be reserved in AM anyway.
    del all_ips[0:4]

    # check if path exists if not exist
    if not os.path.exists(env_path):
        slab_logger.info('The ccs-data repo was not found.  Cloning it now.')
        returncode = service_utils.sync_service(ctx.path, 'master', ctx.username, 'ccs-data')
        if not returncode:
            return(1, '')
    # Find all the envs within the site
    for env in os.listdir(env_path):
        hosts_path = os.path.join(env_path, env, 'hosts.d')
        # Find all the hosts within the env
        files = os.listdir(hosts_path)
        for f in files:
            hostfile = os.path.join(hosts_path, f)
            host_data = open_yaml(hostfile)
            if 'interfaces' in host_data:
                # Not all interface names are created equally
                for interface in host_data['interfaces']:
                    try:
                        # Not all interfaces have an ip_address
                        if 'ip_address' in host_data['interfaces'][interface]:
                            addy = unicode(
                                host_data['interfaces'][interface]['ip_address'])
                            # Turn IP into ipaddress module object for list search
                            ipaddy = ipaddress.IPv4Address(addy)
                            if ipaddy in all_ips:
                                all_ips.remove(ipaddy)
                    except TypeError:
                        slab_logger.debug('%s did not contain any data for interface %s'
                                          % (hostfile, interface))
                    except ipaddress.AddressValueError:
                        tcvm_logger.info('Bad address found in %s' % hostfile)
    for ip in all_ips:
        try:
            # Host lookup by ip address
            socket.gethostbyaddr(str(ip))
        # socket.herror means there was no DNS reservation found
        except socket.herror:
            slab_logger.debug('Using IP %s' % str(ip))
            return(0, str(ip))
    return(1, '')


def create_vm(
        repo_path,
        hostname,
        sc_name,
        tc_name,
        flavor,
        vlan_id,
        role,
        groups,
        sec_groups,
        ip_address):
    """Combine the user inputs from 'stack create host' to create a host.yaml file in the
       appropriate tenant cloud hosts.d directory

    Args:
        repo_path {str}: Patch to the ccs-data repo
        hostname {str}: User supplied name of vm to create
        sc_name {str}: Service cloud name
        tc_name {str}: Tenant cloud name
        flavor {str}: Flavor (hardware requirements) of the vm
        vlan_id {str}: Vlan number to assign IP from.  This must be present in the tenant
                       environment.yaml file
        role {str}: Role of the vm
        groups {list}: List of groups the vm should be in
        sec_groups {str}: Comma delimited str of security groups
        ip_address {str}: Either a specific IP address, or 'False'

    Returns:
        Status code.  Calls various subroutines to gather data, build the host data, and
        write the host file.

    Example usage:
        tc_vm_yaml_create.create_vm(path=<path to ccs-data repo>, hostname='my-service-001',
                                    sc_name=<service cloud>, tc_name=<tenant cloud>,
                                    flavor=<some vm flavor>, vlan_id=<vlan number(66 or 67)>,
                                    role=<none, typically>, groups=<['default', 'other']>,
                                    sec_groups='default,something,somethingelse,maybe')
    """
    slab_logger.log(15, 'Creating host yaml file for %s' % hostname)
    if sc_name == tc_name:
        slab_logger.error('Please select a tenant cloud within %s' % sc_name)
        return 1
    source_data = {'repo_path': repo_path,
                   'hostname': str(hostname),
                   'sc_name': sc_name,
                   'tc_name': tc_name,
                   'flavor': flavor,
                   'role': role,
                   'vlan_id': vlan_id,
                   'groups': groups,
                   'sec_groups': sec_groups,
                   }
    source_data = extract_env_data(source_data)
    if source_data == 1:
        return 1
    if not re.search(source_data['tc_region'], source_data['hostname']):
        source_data['hostname'] = source_data['tc_region'] + '-' + source_data['hostname']
    source_data['az'] = source_data['sc_region'] + determine_az(hostname)
    source_data['vlan_id'] = str(source_data['vlan_prefix']) + source_data['vlan_id']
    if vlan_id not in source_data:
        source_data[source_data['vlan_id']] = input_vlan(source_data)
        slab_logger.error(
            ('Vlan%s was not found within %s.  Please try a different vlan' %
             (vlan_id, source_data['tc_name'])))
        return 1
    if not ip_address:
        vlan = ipaddress.IPv4Network(unicode(source_data[str(vlan_id)]))
        returncode, source_data['ip'] = find_ip(source_data['env_path'], vlan)
        if not returncode == 0:
            if str(vlan_id + '-sup') in source_data:
                vlan = ipaddress.IPv4Network(unicode(source_data[str(vlan_id + '-sup')]))
                returncode, source_data['ip'] = find_ip(source_data['env_path'], vlan)
                source_data['sup'] = True
    else:
        source_data['ip'] = ip_address
        vlan_id = find_vlan(source_data)
        if vlan_id == 1:
            return 1
        vlan = ipaddress.IPv4Network(unicode(source_data[str(vlan_id)]))
    if not source_data['ip']:
        slab_logger.error(
            'Vlan%s does not have any IP addresses available' %
            vlan_id)
        return 1
    yaml_data = build_yaml_data(source_data, vlan)
    if 'sup' in source_data:
        yaml_data['deploy_args']['subnet_name'] += '2'
    output_file = os.path.join(source_data['tc_path'], 'hosts.d',
                               str(source_data['hostname'] + '.yaml'))
    write_file(yaml_data, output_file)
    return 0


def determine_az(hostname):
    """Used to determine the availability zone for the vm.

    Args:
        hostname {str}: hostname as input by user from 'stack create host' command

    Returns:
        {str}: -<letter> [a-c] based on the input hostname trailing number, or -a if the
               hostname does not include a number

    Example Usage:
        my_availability_zone = determine_az('my-hostname-003')
    """
    slab_logger.log(15, 'Determining availablity zone for %s' % hostname)
    match = re.search('(\d+)$', hostname)
    if match:
        num_switch = {1: '-a', 2: '-b', 3: '-c'}
        num = int(match.group(1))
        while num > 3:
            num -= 3
        return num_switch[num]
    else:
        return '-a'


def build_yaml_data(source_data, vlan):
    """Combines the gathered data into a dict used to write to host.yaml file

    Args:
        source_data {dict}: Should have all the data needed when built with extract_env_data
            auth_ip {str}: IP address for tenant cloud keystone
            az {str}: Tenant cloud availability zone, typically csl-a
            domain {str}: Domain name of the service cloud
            flavor {str}: Compute flavor of the VM
            groups {list}: List of groups the VM belongs to
            hostname {str}: Name of the VM to build
            ip {str}: IP address of the VM
            role {str}: VM role, typically 'none'
            sec_groups {str}: Comma delimited string of security groups
            tc_name {str}: Name of the tenant cloud
            vlan_id {str}: Number of the vlan used by the VM
        vlan {object}: ipaddress module object used to extract gateway and netmask

    Returns:
        yaml_data {dict}: Structured data for host.yaml file creation

    Example Usage:
        my_vlan = ipaddress.IPv4Network(unicode(10.11.12.0/24))
        host_data = build_yaml_data(source_data, my_vlan)
    """
    slab_logger.log(15, 'Building data dictionary to write to host yaml file')
    fqdn = str(source_data['hostname'] + '.' + source_data['tc_name'] + '.' +
               source_data['domain'])
    yaml_data = {
        'deploy_args': {
            'auth_url': 'http://' + source_data['auth_ip'] + ':5000/v2.0',
            'availability_zone': source_data['az'],
            'flavor': source_data['flavor'],
            'image': 'RHEL-7',
            'network_name': 'Nimbus-Management-iv%s' % source_data['vlan_id'],
            'security_groups': source_data['sec_groups'],
            'subnet_name': 'Nimbus-Management-iv%s-subnet' % source_data['vlan_id'],
            'tenant': source_data['tc_name'],
        },
        'groups': source_data['groups'],
        'hostname': fqdn,
        'interfaces': {
            'eth0': {
                'gateway': str(vlan.network_address + 1),
                'ip_address': source_data['ip'],
                'netmask': str(vlan.netmask),
            },
        },
        'role': source_data['role'],
        'type': 'virtual',
    }
    return yaml_data


def extract_env_data(source_data):
    """Extract needed data from SC and TC environment.yaml files.

    Args:
        source_data {dict}: Includes the following keys and values
            repo_path: Path too ccs-data repo
            sc_name: Name of the service cloud
            tc_name: Name of the tenant cloud

    Returns:
        source_data {dict}: Includes the original data plus the following
            tc_path: Path to the sites/sc/env/tc/ directory
            domain: domain name for the SC
            sc_region: SC region value i.e. csm/csx
            vlan66/67: subnet with /mask for both vlans
            vlan_prefix: prefix of the real 66/67 id

    Example Usage:
        source_data = extract_env_data(source_data)
    """
    slab_logger.log(15, 'Extracting data from service and tenant cloud environment yamls')
    # Open the service cloud environment.yaml
    env_path = os.path.join(
        source_data['repo_path'],
        'sites',
        source_data['sc_name'],
        'environments')
    source_data['env_path'] = env_path
    sc_path = os.path.join(env_path, source_data['sc_name'])
    source_data['tc_path'] = os.path.join(env_path, source_data['tc_name'])
    env_file = os.path.join(sc_path, 'data.d', 'environment.yaml')
    env_data = open_yaml(env_file)
    if env_data == 1:
        return 1
    if 'controller_internal_vip' in env_data:
        source_data['auth_ip'] = env_data['controller_internal_vip']
    if 'domain_name' in env_data:
        source_data['domain'] = env_data['domain_name']
    if 'region' in env_data:
        source_data['sc_region'] = env_data['region']
    if 'site_name' in env_data:
        regex = re.escape(env_data['site_name']) + '\.([\w.]+)'
        match = re.search(regex, source_data['domain'])
        if match:
            source_data['domain'] = match.group(1)
    if 'domain' in source_data and 'auth_ip' in source_data:
        pass
    else:
        return 1

    # Open the tenant cloud environment.yaml
    env_file = os.path.join(
        source_data['tc_path'],
        'data.d',
        'environment.yaml')
    env_data = open_yaml(env_file)
    if env_data == 1:
        return 1
    if 'tc_region' in env_data:
        source_data['tc_region'] = env_data['tc_region']
    else:
        source_data[source_data['vlan_id']] = input_vlan(source_data)
        match = re.search('([\w-]+)-keystonectl-001', env_data['keystone_hostnames'][0])
        source_data['tc_region'] = match.group(1)
        if not len(source_data[source_data['vlan_id']]):
            slab_logger.error('Unable to find ServiceLab data in %s' % env_file)
            return 1
    for vlan_key in env_data:
        match = re.search('^vlan(\d{0,2})(6[367])(-sup)?$', vlan_key)
        if match:
            if match.group(3):
                source_data[
                    match.group(2) +
                    match.group(3)] = env_data[vlan_key]
            else:
                source_data[match.group(2)] = env_data[vlan_key]
            source_data['vlan_prefix'] = match.group(1)
        else:
            source_data['vlan_prefix'] = ''

    return source_data
