"""
Copyright (c) 2017 SONATA-NFV and Paderborn University
ALL RIGHTS RESERVED.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Neither the name of the SONATA-NFV, Paderborn University
nor the names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.

This work has been performed in the framework of the SONATA project,
funded by the European Commission under Grant number 671517 through
the Horizon 2020 and 5G-PPP programmes. The authors would like to
acknowledge the contributions of their colleagues of the SONATA
partner consortium (www.sonata-nfv.eu).
"""
from __future__ import print_function  # TODO remove when print is no longer needed for debugging
from resources import *
from datetime import datetime
import re
import sys
import uuid
import logging
import ip_handler as IP


LOG = logging.getLogger("api.openstack.heat.parser")


class HeatParser:
    """
    The HeatParser will parse a heat dictionary and create a stack and its components, to instantiate it within son-emu.
    """

    def __init__(self, compute):
        self.description = None
        self.parameter_groups = None
        self.parameters = None
        self.resources = None
        self.outputs = None
        self.compute = compute
        self.bufferResource = list()

    def parse_input(self, input_dict, stack, dc_label, stack_update=False):
        """
        It will parse the input dictionary into the corresponding classes, which are then stored within the stack.

        :param input_dict: Dictionary with the template version and resources.
        :type input_dict: ``dict``
        :param stack: Reference of the stack that should finally contain all created classes.
        :type stack: :class:`heat.resources.stack`
        :param dc_label: String that contains the label of the used data center.
        :type dc_label: ``str``
        :param stack_update: Specifies if a new stack will be created or a older one will be updated
        :type stack_update: ``bool``
        :return: * *True*: If the template version is supported and all resources could be created.
                 * *False*: Else
        :rtype: ``bool``
        """
        if not self.check_template_version(str(input_dict['heat_template_version'])):
            print('Unsupported template version: ' + input_dict['heat_template_version'], file=sys.stderr)
            return False

        self.description = input_dict.get('description', None)
        self.parameter_groups = input_dict.get('parameter_groups', None)
        self.parameters = input_dict.get('parameters', None)
        self.resources = input_dict.get('resources', None)
        self.outputs = input_dict.get('outputs', None)
        # clear bufferResources
        self.bufferResource = list()

        for resource in self.resources.values():
            self.handle_resource(resource, stack, dc_label, stack_update=stack_update)

        # This loop tries to create all classes which had unresolved dependencies.
        unresolved_resources_last_round = len(self.bufferResource) + 1
        while len(self.bufferResource) > 0 and unresolved_resources_last_round > len(self.bufferResource):
            unresolved_resources_last_round = len(self.bufferResource)
            number_of_items = len(self.bufferResource)
            while number_of_items > 0:
                self.handle_resource(self.bufferResource.pop(0), stack, dc_label, stack_update=stack_update)
                number_of_items -= 1

        if len(self.bufferResource) > 0:
            print(str(len(self.bufferResource)) +
                  ' classes of the HOT could not be created, because the dependencies could not be found.')
            print("the problem classes are:")
            for br in self.bufferResource:
                print("class: %s" % str(br))
            return False
        return True

    def handle_resource(self, resource, stack, dc_label, stack_update=False):
        """
        This function will take a resource (from a heat template) and determines which type it is and creates
        the corresponding class, with its required parameters, for further calculations (like deploying the stack).
        If it is not possible to create the class, because of unresolved dependencies, it will buffer the resource
        within the 'self.bufferResource' list.

        :param resource: Dict which contains all important informations about the type and parameters.
        :type resource: ``dict``
        :param stack: Reference of the stack that should finally contain the created class.
        :type stack: :class:`heat.resources.stack`
        :param dc_label: String that contains the label of the used data center
        :type dc_label: ``str``
        :param stack_update: Specifies if a new stack will be created or a older one will be updated
        :type stack_update: ``bool``
        :return: void
        :rtype: ``None``
        """
        if "OS::Neutron::Net" in resource['type']:
            try:
                net_name = resource['properties']['name']
                if net_name not in stack.nets:
                    stack.nets[net_name] = self.compute.create_network(net_name, True)

            except Exception as e:
                LOG.warning('Could not create Net: ' + e.message)
            return

        if 'OS::Neutron::Subnet' in resource['type'] and "Net" not in resource['type']:
            try:
                net_name = resource['properties']['network']['get_resource']
                if net_name not in stack.nets:
                    net = self.compute.create_network(net_name, stack_update)
                    stack.nets[net_name] = net
                else:
                    net = stack.nets[net_name]

                net.subnet_name = resource['properties']['name']
                if 'gateway_ip' in resource['properties']:
                    net.gateway_ip = resource['properties']['gateway_ip']
                net.subnet_id = resource['properties'].get('id', str(uuid.uuid4()))
                net.subnet_creation_time = str(datetime.now())
                if not stack_update:
                    net.set_cidr(IP.get_new_cidr(net.subnet_id))
            except Exception as e:
                LOG.warning('Could not create Subnet: ' + e.message)
            return

        if 'OS::Neutron::Port' in resource['type']:
            try:
                port_name = resource['properties']['name']
                if port_name not in stack.ports:
                    port = self.compute.create_port(port_name, stack_update)
                    stack.ports[port_name] = port
                else:
                    port = stack.ports[port_name]

                if str(resource['properties']['network']['get_resource']) in stack.nets:
                    net = stack.nets[resource['properties']['network']['get_resource']]
                    if net.subnet_id is not None:
                        port.net_name = net.name
                        port.ip_address = net.get_new_ip_address(port.name)
                        return
            except Exception as e:
                LOG.warning('Could not create Port: ' + e.message)
            self.bufferResource.append(resource)
            return

        if 'OS::Nova::Server' in resource['type']:
            try:
                compute_name = str(dc_label) + '_' + str(stack.stack_name) + '_' + str(resource['properties']['name'])
                shortened_name = str(dc_label) + '_' + str(stack.stack_name) + '_' + \
                                 self.shorten_server_name(str(resource['properties']['name']), stack)
                nw_list = resource['properties']['networks']

                if shortened_name not in stack.servers:
                    server = self.compute.create_server(shortened_name, stack_update)
                    stack.servers[shortened_name] = server
                else:
                    server = stack.servers[shortened_name]

                server.full_name = compute_name
                server.template_name = str(resource['properties']['name'])
                server.command = resource['properties'].get('command', '/bin/sh')
                server.image = resource['properties']['image']
                server.flavor = resource['properties']['flavor']

                for port in nw_list:
                    port_name = port['port']['get_resource']
                    # just create a port
                    # we don't know which network it belongs to yet, but the resource will appear later in a valid
                    # template
                    if port_name not in stack.ports:
                        stack.ports[port_name] = self.compute.create_port(port_name, stack_update)
                    server.port_names.append(port_name)
                return
            except Exception as e:
                LOG.warning('Could not create Server: ' + e.message)
            return

        if 'OS::Neutron::RouterInterface' in resource['type']:
            try:
                router_name = None
                subnet_name = resource['properties']['subnet']['get_resource']

                if 'get_resource' in resource['properties']['router']:
                    router_name = resource['properties']['router']['get_resource']
                else:
                    router_name = resource['properties']['router']

                if router_name not in stack.routers:
                    stack.routers[router_name] = Router(router_name)

                for tmp_net in stack.nets.values():
                    if tmp_net.subnet_name == subnet_name:
                        stack.routers[router_name].add_subnet(subnet_name)
                        return
            except Exception as e:
                LOG.warning('Could not create RouterInterface: ' + e.__repr__())
            self.bufferResource.append(resource)
            return

        if 'OS::Neutron::FloatingIP' in resource['type']:
            try:
                port_name = resource['properties']['port_id']['get_resource']
                floating_network_id = resource['properties']['floating_network_id']
                if port_name not in stack.ports:
                    stack.ports[port_name] = self.compute.create_port(port_name, stack_update)

                stack.ports[port_name].floating_ip = floating_network_id
            except Exception as e:
                LOG.warning('Could not create FloatingIP: ' + e.message)
            return

        if 'OS::Neutron::Router' in resource['type']:
            try:
                name = resource['properties']['name']
                if name not in stack.routers:
                    stack.routers[name] = Router(name)
            except Exception as e:
                print('Could not create Router: ' + e.message)
            return

        if 'OS::Heat::ResourceGroup' in resource['type']:
            try:
                embedded_resource = resource['properties']['resource_def']
                LOG.debug("Found resource in resource group: {}".format(embedded_resource))
                # recursively parse embedded resource
                self.handle_resource(embedded_resource, stack, dc_label, stack_update)
            except Exception as e:
                print('Could not create Router: ' + e.message)
            return

        LOG.warning('Could not determine resource type: {}'.format(resource['type']))
        return

    def shorten_server_name(self, server_name, stack):
        """
        Shortens the server name to a maximum of 12 characters plus the iterator string, if the original name was
        used before.

        :param server_name: The original server name.
        :type server_name: ``str``
        :param stack: A reference to the used stack.
        :type stack: :class:`heat.resources.stack`
        :return: A string with max. 12 characters plus iterator string.
        :rtype: ``str``
        """
        server_name = self.shorten_name(server_name, 12)
        iterator = 0
        while server_name in stack.servers:
            server_name = server_name[0:12] + str(iterator)
            iterator += 1
        return server_name

    def shorten_name(self, name, max_size):
        """
        Shortens the name to max_size characters and replaces all '-' with '_'.

        :param name: The original string.
        :type name: ``str``
        :param max_size: The number of allowed characters.
        :type max_size: ``int``
        :return: String with at most max_size characters and without '-'.
        :rtype: ``str``
        """
        shortened_name = name.split(':', 1)[0]
        shortened_name = shortened_name.replace("-", "_")
        shortened_name = shortened_name[0:max_size]
        return shortened_name

    def check_template_version(self, version_string):
        """
        Checks if a version string is equal or later than 30-04-2015

        :param version_string: String with the version.
        :type version_string: ``str``
        :return: * *True*: if the version is equal or later 30-04-2015.
         * *False*: else
        :rtype: ``bool``
        """
        r = re.compile('\d{4}-\d{2}-\d{2}')
        if not r.match(version_string):
            return False

        year, month, day = map(int, version_string.split('-', 2))
        if year < 2015:
            return False
        if year == 2015:
            if month < 04:
                return False
            if month == 04 and day < 30:
                return False
        return True
