##############################################################################
# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Apache License, Version 2.0
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################


from django.shortcuts import render
from django.forms import formset_factory

import json
import re
from xml.dom import minidom

from workflow.models import WorkflowStep
from workflow.forms import *
from resource_inventory.models import *
from dashboard.exceptions import *

import logging
logger = logging.getLogger(__name__)


class Define_Hardware(WorkflowStep):

    template = 'resource/steps/define_hardware.html'
    title = "Define Hardware"
    description = "Choose the type and amount of machines you want"
    short_title = "hosts"
    def get_context(self):
        context = super(Define_Hardware, self).get_context()
        selection_data = {"hosts": {}, "labs": {}}
        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
        hosts = models.get("hosts", [])
        for host in hosts:
            profile_id = "host_" + str(host.profile.id)
            if profile_id not in selection_data['hosts']:
                selection_data['hosts'][profile_id] = []
            selection_data['hosts'][profile_id].append({"host_name": host.resource.name, "class": profile_id})

        if models.get("bundle", GenericResourceBundle()).lab:
            selection_data['labs'] = {"lab_" + str(models.get("bundle").lab.lab_user.id): "true"}

        form = HardwareDefinitionForm(
                selection_data=selection_data
                )
        context['form'] = form
        return context

    def render(self, request):
        self.context = self.get_context()
        return render(request, self.template, self.context)


    def update_models(self, data):
        data = json.loads(data['filter_field'])
        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
        models['hosts'] = []  # This will always clear existing data when this step changes
        models['interfaces'] = {}
        if "bundle" not in models:
            models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))
        host_data = data['hosts']
        names = {}
        for host_dict in host_data:
            id = host_dict['class']
            # bit of formatting
            id = int(id.split("_")[-1])
            profile = HostProfile.objects.get(id=id)
            # instantiate genericHost and store in repo
            name = host_dict['host_name']
            if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
                raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
            if name in names:
                raise NonUniqueHostnameException("All hosts must have unique names")
            names[name] = True
            genericResource = GenericResource(bundle=models['bundle'], name=name)
            genericHost = GenericHost(profile=profile, resource=genericResource)
            models['hosts'].append(genericHost)
            for interface_profile in profile.interfaceprofile.all():
                genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
                if genericHost.resource.name not in models['interfaces']:
                    models['interfaces'][genericHost.resource.name] = []
                models['interfaces'][genericHost.resource.name].append(genericInterface)

        # add selected lab to models
        for lab_dict in data['labs']:
            if list(lab_dict.values())[0]:  # True for lab the user selected
                lab_user_id = int(list(lab_dict.keys())[0].split("_")[-1])
                models['bundle'].lab = Lab.objects.get(lab_user__id=lab_user_id)
                break # if somehow we get two 'true' labs, we only use one

        # return to repo
        self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)


    def update_confirmation(self):
        confirm = self.repo_get(self.repo.CONFIRMATION, {})
        if "resource" not in confirm:
            confirm['resource'] = {}
        confirm['resource']['hosts'] = []
        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {"hosts": []})
        for host in models['hosts']:
            host_dict = {"name": host.resource.name, "profile": host.profile.name}
            confirm['resource']['hosts'].append(host_dict)
        if "lab" in models:
            confirm['resource']['lab'] = models['lab'].lab_user.username
        self.repo_put(self.repo.CONFIRMATION, confirm)


    def post_render(self, request):
        try:
            self.form = HardwareDefinitionForm(request.POST)
            if self.form.is_valid():
                self.update_models(self.form.cleaned_data)
                self.update_confirmation()
                self.metastep.set_valid("Step Completed")
            else:
                self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
                pass
        except Exception as e:
            self.metastep.set_invalid(str(e))
        self.context = self.get_context()
        return render(request, self.template, self.context)

class Define_Nets(WorkflowStep):
    template = 'resource/steps/pod_definition.html'
    title = "Define Networks"
    description = "Use the tool below to draw the network topology of your POD"
    short_title = "networking"
    form = NetworkDefinitionForm

    def get_vlans(self):
        vlans = self.repo_get(self.repo.VLANS)
        if vlans:
            return vlans
        # try to grab some vlans from lab
        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
        if "bundle" not in models:
            return None
        lab = models['bundle'].lab
        if lab is None or lab.vlan_manager is None:
            return None
        try:
            vlans = lab.vlan_manager.get_vlan(count=lab.vlan_manager.block_size)
            self.repo_put(self.repo.VLANS, vlans)
            return vlans
        except Exception as e:
            return None

    def get_context(self):
        # TODO: render *primarily* on hosts in repo models
        context = super(Define_Nets, self).get_context()
        context['form'] = NetworkDefinitionForm()
        try:
            context['hosts'] = []
            models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
            vlans = self.get_vlans()
            if vlans:
                context['vlans'] = vlans
            hosts = models.get("hosts", [])
            hostlist = self.repo_get(self.repo.GRB_LAST_HOSTLIST, None)
            added_list = []
            added_dict = {}
            context['added_hosts'] = []
            if not hostlist is None:
                new_hostlist = []
                for host in models['hosts']:
                    intcount = host.profile.interfaceprofile.count()
                    new_hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
                context['removed_hosts'] = list(set(hostlist) - set(new_hostlist))
                added_list = list(set(new_hostlist) - set(hostlist))
                for hoststr in added_list:
                    key = hoststr.split("*")[0]
                    added_dict[key] = hoststr
            for generic_host in hosts:
                host_profile = generic_host.profile
                host = {}
                host['id'] = generic_host.resource.name
                host['interfaces'] = []
                for iface in host_profile.interfaceprofile.all():
                    host['interfaces'].append({
                            "name": iface.name,
                            "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type})
                host['value'] = {"name": generic_host.resource.name}
                host['value']['description'] = generic_host.profile.description
                context['hosts'].append(json.dumps(host))
                if host['id'] in added_dict:
                    context['added_hosts'].append(json.dumps(host))
            bundle = models.get("bundle", False)
            if bundle and bundle.xml:
                context['xml'] = bundle.xml
            else:
                context['xml'] = False

        except Exception as e:
            pass
        return context

    def post_render(self, request):
        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
        if 'hosts' in models:
            hostlist = []
            for host in models['hosts']:
                intcount = host.profile.interfaceprofile.count()
                hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
            self.repo_put(self.repo.GRB_LAST_HOSTLIST, hostlist)
        try:
            xmlData = request.POST.get("xml")
            self.updateModels(xmlData)
            # update model with xml
            self.metastep.set_valid("Networks applied successfully")
        except Exception as e:
            self.metastep.set_invalid("An error occurred when applying networks")
        return self.render(request)

    def updateModels(self, xmlData):
        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
        models["vlans"] = {}
        given_hosts, interfaces = self.parseXml(xmlData)
        vlan_manager = models['bundle'].lab.vlan_manager
        existing_host_list = models.get("hosts", [])
        existing_hosts = {} # maps id to host
        for host in existing_host_list:
            existing_hosts[host.resource.name] = host

        bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))

        for hostid, given_host in given_hosts.items():
            existing_host = existing_hosts[hostid[5:]]

            for ifaceId in given_host['interfaces']:
                iface = interfaces[ifaceId]
                iface_profile = existing_host.profile.interfaceprofile.get(name=iface['profile_name'])
                if existing_host.resource.name not in models['vlans']:
                    models['vlans'][existing_host.resource.name] = {}
                models['vlans'][existing_host.resource.name][iface['profile_name']] = []
                for network in iface['networks']:
                    vlan_id = network['network']['vlan']
                    is_public = network['network']['public']
                    if is_public:
                        vlan_id = vlan_manager.get_public_vlan().vlan
                    vlan = Vlan(vlan_id=vlan_id, tagged=network['tagged'], public=is_public)
                    models['vlans'][existing_host.resource.name][iface['profile_name']].append(vlan)
        bundle.xml = xmlData
        self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)

    # serialize and deserialize xml from mxGraph
    def parseXml(self, xmlString):
        parent_nets = {}  # map network ports to networks
        networks = {}  # maps net id to network object
        hosts = {}  # cotains id -> hosts, each containing interfaces, referencing networks
        interfaces = {}  # maps id -> interface
        xmlDom = minidom.parseString(xmlString)
        root = xmlDom.documentElement.firstChild
        connections = []
        netids = {}
        untagged_ints = {}
        for cell in root.childNodes:
            cellId = cell.getAttribute('id')

            if cell.getAttribute("edge"):
                #cell is a network connection
                escaped_json_str = cell.getAttribute("value")
                json_str = escaped_json_str.replace('"', '"')
                attributes = json.loads(json_str)
                tagged = attributes['tagged']
                interface = None
                network = None
                src = cell.getAttribute("source")
                tgt = cell.getAttribute("target")
                if src in parent_nets:
                    #src is a network port
                    network = networks[parent_nets[src]]
                    if tgt in untagged_ints and tagged==False:
                        raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
                    interface = interfaces[tgt]
                    untagged_ints[tgt] = True
                else:
                    network = networks[parent_nets[tgt]]
                    if src in untagged_ints and tagged==False:
                        raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
                    interface = interfaces[src]
                    untagged_ints[src] = True
                interface['networks'].append({"network": network, "tagged": tagged})

            elif "network" in cellId:  # cell is a network
                escaped_json_str = cell.getAttribute("value")
                json_str = escaped_json_str.replace('"', '"')
                net_info = json.loads(json_str)
                nid = net_info['vlan_id']
                public = net_info['public']
                try:
                    int_netid = int(nid)
                    assert public or int_netid > 1, "Net id is 1 or lower"
                    assert int_netid < 4095, "Net id is 4095 or greater"
                except Exception as e:
                    raise InvalidVlanConfigurationException("VLAN ID is not an integer more than 1 and less than 4095")
                if nid in netids:
                    raise NetworkExistsException("Non unique network id found")
                else:
                    pass
                network = {"name": net_info['name'], "vlan": net_info['vlan_id'], "public": public}
                netids[net_info['vlan_id']] = True
                networks[cellId] = network

            elif "host" in cellId:  # cell is a host/machine
                #TODO gather host info
                cell_json_str = cell.getAttribute("value")
                cell_json = json.loads(cell_json_str)
                host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']}
                hosts[cellId] = host

            elif cell.hasAttribute("parent"):
                parentId = cell.getAttribute('parent')
                if "network" in parentId:
                    parent_nets[cellId] = parentId
                elif "host" in parentId:
                    #TODO gather iface info
                    cell_json_str = cell.getAttribute("value")
                    cell_json = json.loads(cell_json_str)
                    iface = {"name": cellId, "networks": [], "profile_name": cell_json['name']}
                    hosts[parentId]['interfaces'].append(cellId)
                    interfaces[cellId] = iface
        return hosts, interfaces


class Resource_Meta_Info(WorkflowStep):
    template = 'resource/steps/meta_info.html'
    title = "Extra Info"
    description = "Please fill out the rest of the information about your resource"
    short_title = "pod info"

    def get_context(self):
        context = super(Resource_Meta_Info, self).get_context()
        name = ""
        desc = ""
        bundle = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("bundle", False)
        if bundle and bundle.name:
            name = bundle.name
            desc = bundle.description
        context['form'] = ResourceMetaForm(initial={"bundle_name": name, "bundle_description": desc})
        return context

    def post_render(self, request):
        form = ResourceMetaForm(request.POST)
        if form.is_valid():
            models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
            name = form.cleaned_data['bundle_name']
            desc = form.cleaned_data['bundle_description']
            bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
            bundle.name = name
            bundle.description = desc
            models['bundle'] = bundle
            self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
            confirm = self.repo_get(self.repo.CONFIRMATION)
            if "resource" not in confirm:
                confirm['resource'] = {}
            confirm_info = confirm['resource']
            confirm_info["name"] = name
            tmp = desc
            if len(tmp) > 60:
                tmp = tmp[:60] + "..."
            confirm_info["description"] = tmp
            self.repo_put(self.repo.CONFIRMATION, confirm)
            self.metastep.set_valid("Step Completed")

        else:
            self.metastep.set_invalid("Please correct the fields highlighted in red to continue")
            pass
        return self.render(request)


class Host_Meta_Info(WorkflowStep):
    template = "resource/steps/host_info.html"
    title = "Host Info"
    description = "We need a little bit of information about your chosen machines"
    short_title = "host info"

    def __init__(self, *args, **kwargs):
        super(Host_Meta_Info, self).__init__(*args, **kwargs)
        self.formset = formset_factory(GenericHostMetaForm, extra=0)

    def get_context(self):
        context = super(Host_Meta_Info, self).get_context()
        GenericHostFormset = self.formset
        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
        initial_data = []
        if "hosts" not in models:
            context['error'] = "Please go back and select your hosts"
        else:
            for host in models['hosts']:
                profile = host.profile.name
                name = host.resource.name
                if not name:
                    name = ""
                initial_data.append({"host_profile": profile, "host_name": name})
        context['formset'] = GenericHostFormset(initial=initial_data)
        return context

    def post_render(self, request):
        models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
        if 'hosts' not in models:
            models['hosts'] = []
        hosts = models['hosts']
        i = 0
        confirm_hosts = []
        GenericHostFormset = self.formset
        formset = GenericHostFormset(request.POST)
        if formset.is_valid():
            for form in formset:
                host = hosts[i]
                host.resource.name = form.cleaned_data['host_name']
                i += 1
                confirm_hosts.append({"name": host.resource.name, "profile": host.profile.name})
            models['hosts'] = hosts
            self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
            confirm = self.repo_get(self.repo.CONFIRMATION, {})
            if "resource" not in confirm:
                confirm['resource'] = {}
            confirm['resource']['hosts'] = confirm_hosts
            self.repo_put(self.repo.CONFIRMATION, confirm)
        else:
            pass
        return self.render(request)