# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/)
#
# 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.

 
import base64, json, logging #, re
from contextlib import contextmanager
import json
import grpc
from collections import defaultdict
from flask import current_app, redirect, render_template, Blueprint, flash, session, url_for, request
from common.proto.context_pb2 import (
    IsolationLevelEnum, Service, ServiceId, ServiceTypeEnum, ServiceStatusEnum, Connection, Empty, DeviceDriverEnum,
    ConfigActionEnum, Device, DeviceList)
from common.tools.context_queries.Context import get_context
from common.tools.context_queries.Topology import get_topology
from common.tools.context_queries.EndPoint import get_endpoint_names
from wtforms.validators import ValidationError
from context.client.ContextClient import ContextClient
from service.client.ServiceClient import ServiceClient
from device.client.DeviceClient import DeviceClient
from common.tools.object_factory.Service import (
    json_service_l2nm_planned, json_service_l3nm_planned)
from common.tools.object_factory.Constraint import (
    json_constraint_sla_availability, json_constraint_sla_capacity, json_constraint_sla_isolation,
    json_constraint_sla_latency)
from common.tools.descriptor.Loader import DescriptorLoader, compose_notifications
from common.tools.object_factory.ConfigRule import json_config_rule_set
from common.tools.object_factory.Device import json_device_id
from common.tools.object_factory.EndPoint import json_endpoint_id
from webui.service.service.forms import AddServiceForm_1, AddServiceForm_ACL_L2, AddServiceForm_ACL_IPV4, AddServiceForm_ACL_IPV6, AddServiceForm_L2VPN, AddServiceForm_L3VPN
from common.tools.context_queries.Service import get_service_by_uuid
from common.tools.object_factory.Context import json_context_id
from common.tools.object_factory.Topology import json_topology_id
from typing import Optional, Set

LOGGER = logging.getLogger(__name__)
service = Blueprint('service', __name__, url_prefix='/service')                     #Define a flask Blueprint called "service" behind the url "/service"

context_client = ContextClient()                                                    #Create an instance of ContextClient class as defined in /src/service/client/ContextClient.py
service_client = ServiceClient()                                                    #Create an instance of ServiceClient class as defined in /src/service/client/ServiceClient.py
device_client = DeviceClient()

type     = ["ACL_UNDEFINED", "ACL_IPV4","ACL_IPV6","ACL_L2","ACL_MPLS","ACL_MIXED"]
f_action = ["UNDEFINED", "DROP","ACCEPT","REJECT"]
l_action = ["UNDEFINED", "LOG_NONE","LOG_SYSLOG"]

#@service.get('/')                                                                   #Route for the homepage of the created "service" blueprint 
#@contextmanager
#def connected_client(c):
#    try:
#        c.connect()
#        yield c
#    finally:
#        c.close()

# Context client must be in connected state when calling this function
def get_device_drivers_in_use(topology_uuid: str, context_uuid: str) -> Set[str]:
    active_drivers = set()
    grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)
    topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}
    grpc_devices: DeviceList = context_client.ListDevices(Empty())
    for device in grpc_devices.devices:
        if device.device_id.device_uuid.uuid in topo_device_uuids:
            for driver in device.device_drivers:
                active_drivers.add(DeviceDriverEnum.Name(driver))
    return active_drivers

@service.get('/')
def home():
    if 'context_uuid' not in session or 'topology_uuid' not in session:             #Check if context_uuid and topology_uuid are defined in the current seession
        flash("Please select a context!", "warning")                                #If they are not defined in the current session [Return to main page - STOP]                           
        return redirect(url_for("main.home"))
    context_uuid = session['context_uuid']
    topology_uuid = session['topology_uuid']

    context_client.connect()                                                        #Creates a connection, as specified in the connect method of the ContextClient() class

    context_obj = get_context(context_client, context_uuid, rw_copy=False)          #Using the get_context function, defined in /src/common/ 
    if context_obj is None:
        flash('Context({:s}) not found'.format(str(context_uuid)), 'danger')
        services, device_names, endpoints_data = list(), list(), list()
    else:
        try:
            services = context_client.ListServices(context_obj.context_id)
            services = services.services
            active_drivers = get_device_drivers_in_use(topology_uuid, context_uuid)
        except grpc.RpcError as e:
            if e.code() != grpc.StatusCode.NOT_FOUND: raise
            if e.details() != 'Context({:s}) not found'.format(context_uuid): raise
            services, device_names, endpoints_data = list(), dict(), dict()
            active_drivers = set()
        else:
            endpoint_ids = list()
            for service_ in services:
                endpoint_ids.extend(service_.service_endpoint_ids)
            device_names, endpoints_data = get_endpoint_names(context_client, endpoint_ids)

    context_client.close()
    return render_template(
        'service/home.html', services=services, device_names=device_names, endpoints_data=endpoints_data,
        ste=ServiceTypeEnum, sse=ServiceStatusEnum, active_drivers=active_drivers)


def get_hub_module_name(dev: Device) -> Optional[str]:
    for cr in dev.device_config.config_rules:
        if cr.action == ConfigActionEnum.CONFIGACTION_SET and cr.custom and cr.custom.resource_key == "_connect/settings":
            try:
                cr_dict = json.loads(cr.custom.resource_value)
                if "hub_module_name" in cr_dict:
                    return cr_dict["hub_module_name"]
            except json.JSONDecodeError:
                pass
    return None

#@service.route('add-xr', methods=['GET', 'POST'])
#def add_xr():
#    ### FIXME: copypaste
#    if 'context_uuid' not in session or 'topology_uuid' not in session:
#        flash("Please select a context!", "warning")
#        return redirect(url_for("main.home"))
#
#    context_uuid = session['context_uuid']
#    topology_uuid = session['topology_uuid']
#
#    context_client.connect()
#    grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)
#    if grpc_topology is None:
#        flash('Context({:s})/Topology({:s}) not found'.format(str(context_uuid), str(topology_uuid)), 'danger')
#        return redirect(url_for("main.home"))
#    else:
#        topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}
#        grpc_devices= context_client.ListDevices(Empty())
#        devices = [
#            device for device in grpc_devices.devices
#            if device.device_id.device_uuid.uuid in topo_device_uuids and DeviceDriverEnum.DEVICEDRIVER_XR in device.device_drivers
#        ]
#        devices.sort(key=lambda dev: dev.name)
#
#        hub_interfaces_by_device = defaultdict(list)
#        leaf_interfaces_by_device = defaultdict(list)
#        constellation_name_to_uuid = {}
#        dev_ep_to_uuid = {}
#        ep_uuid_to_name = {}
#        for d in devices:
#            constellation_name_to_uuid[d.name] = d.device_id.device_uuid.uuid
#            hm_name = get_hub_module_name(d)
#            if hm_name is not None:
#                hm_if_prefix= hm_name + "|"
#                for ep in d.device_endpoints:
#                    dev_ep_to_uuid[(d.name, ep.name)] = ep.endpoint_id.endpoint_uuid.uuid
#                    if ep.name.startswith(hm_if_prefix):
#                        hub_interfaces_by_device[d.name].append(ep.name)
#                    else:
#                        leaf_interfaces_by_device[d.name].append(ep.name)
#                    ep_uuid_to_name[ep.endpoint_id.endpoint_uuid.uuid] = (d.name, ep.name)
#                hub_interfaces_by_device[d.name].sort()
#                leaf_interfaces_by_device[d.name].sort()
#
#        # Find out what endpoints are already used so that they can be disabled
#        # in the create screen
#        context_obj = get_context(context_client, context_uuid, rw_copy=False)
#        if context_obj is None:
#            flash('Context({:s}) not found'.format(str(context_uuid)), 'danger')
#            return redirect(request.url)
#        
#        services = context_client.ListServices(context_obj.context_id)
#        ep_used_by={}
#        for service in services.services:
#            if  service.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE:
#                for ep in service.service_endpoint_ids:
#                    ep_uuid = ep.endpoint_uuid.uuid
#                    if ep_uuid in ep_uuid_to_name:
#                        dev_name, ep_name = ep_uuid_to_name[ep_uuid]
#                        ep_used_by[f"{ep_name}@{dev_name}"] = service.name
#
#    context_client.close()
#
#    if request.method != 'POST':
#        return render_template('service/add-xr.html', devices=devices, hub_if=hub_interfaces_by_device, leaf_if=leaf_interfaces_by_device, ep_used_by=ep_used_by)
#    else:
#        service_name = request.form["service_name"]
#        if service_name == "":
#            flash(f"Service name must be specified", 'danger')
#
#        constellation = request.form["constellation"]
#        constellation_uuid = constellation_name_to_uuid.get(constellation, None)
#        if constellation_uuid is None:
#            flash(f"Invalid constellation \"{constellation}\"", 'danger')
#
#        hub_if = request.form["hubif"]
#        hub_if_uuid = dev_ep_to_uuid.get((constellation, hub_if), None)
#        if hub_if_uuid is None:
#            flash(f"Invalid hub interface \"{hub_if}\"", 'danger')
#
#        leaf_if = request.form["leafif"]
#        leaf_if_uuid = dev_ep_to_uuid.get((constellation, leaf_if), None)
#        if leaf_if_uuid is None:
#            flash(f"Invalid leaf interface \"{leaf_if}\"", 'danger')
#        
#        if service_name == "" or constellation_uuid is None or hub_if_uuid is None or leaf_if_uuid is None:
#            return redirect(request.url)
#        
#        
#        json_context_uuid=json_context_id(context_uuid)
#        sr = {
#            "name": service_name,
#            "service_id": {
#                 "context_id": {"context_uuid": {"uuid": context_uuid}},
#                 "service_uuid": {"uuid": service_name}
#            },
#            'service_type'        : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE,
#            "service_endpoint_ids": [
#                {'device_id': {'device_uuid': {'uuid': constellation_uuid}}, 'endpoint_uuid': {'uuid': hub_if_uuid}, 'topology_id': json_topology_id("admin", context_id=json_context_uuid)},
#                {'device_id': {'device_uuid': {'uuid': constellation_uuid}}, 'endpoint_uuid': {'uuid': leaf_if_uuid}, 'topology_id': json_topology_id("admin", context_id=json_context_uuid)}
#            ],
#            'service_status'      : {'service_status': ServiceStatusEnum.SERVICESTATUS_PLANNED},
#            'service_constraints' : [],
#        }
#
#        json_tapi_settings = {
#            'capacity_value'  : 50.0,
#            'capacity_unit'   : 'GHz',
#            'layer_proto_name': 'PHOTONIC_MEDIA',
#            'layer_proto_qual': 'tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC',
#            'direction'       : 'UNIDIRECTIONAL',
#        }
#        config_rule = json_config_rule_set('/settings', json_tapi_settings)
#
#        with connected_client(service_client) as sc:
#            endpoints, sr['service_endpoint_ids'] = sr['service_endpoint_ids'], []
#            try:
#                create_response = sc.CreateService(Service(**sr))
#            except Exception as e:
#                flash(f'Failure to update service name {service_name} with endpoints and configuration, exception {str(e)}', 'danger')
#                return redirect(request.url)
#            
#            sr['service_endpoint_ids'] = endpoints
#            sr['service_config'] = {'config_rules': [config_rule]}
#
#            try:
#                update_response = sc.UpdateService(Service(**sr))
#                flash(f'Created service {update_response.service_uuid.uuid}', 'success')
#            except Exception as e: 
#                flash(f'Failure to update service {create_response.service_uuid.uuid} with endpoints and configuration, exception {str(e)}', 'danger')
#                return redirect(request.url)
#
#            return redirect(url_for('service.home'))

@service.get('<path:service_uuid>/detail')
def detail(service_uuid: str):
    if 'context_uuid' not in session or 'topology_uuid' not in session:
        flash("Please select a context!", "warning")
        return redirect(url_for("main.home"))
    context_uuid = session['context_uuid']

    try:
        context_client.connect()
        endpoint_ids = list()
        service_obj = get_service_by_uuid(context_client, service_uuid, rw_copy=False)
        if service_obj is None:
            flash('Context({:s})/Service({:s}) not found'.format(str(context_uuid), str(service_uuid)), 'danger')
            service_obj = Service()
        else:
            endpoint_ids.extend(service_obj.service_endpoint_ids)
            connections: Connection = context_client.ListConnections(service_obj.service_id)
            connections = connections.connections
            for connection in connections: endpoint_ids.extend(connection.path_hops_endpoint_ids)

        if len(endpoint_ids) > 0:
            device_names, endpoints_data = get_endpoint_names(context_client, endpoint_ids)
        else:
            device_names, endpoints_data = dict(), dict()

        context_client.close()
        return render_template(
            'service/detail.html', service=service_obj, connections=connections, device_names=device_names,
            endpoints_data=endpoints_data, ste=ServiceTypeEnum, sse=ServiceStatusEnum, ile=IsolationLevelEnum, type = type, f_action = f_action, l_action = l_action)
    except Exception as e:
        flash('The system encountered an error and cannot show the details of this service.', 'warning')
        current_app.logger.exception(e)
        return redirect(url_for('service.home'))

@service.get('<path:service_uuid>/delete')                                          #Route for deleting a specific service     
def delete(service_uuid: str):
    if 'context_uuid' not in session or 'topology_uuid' not in session:
        flash("Please select a context!", "warning")
        return redirect(url_for("main.home"))
    context_uuid = session['context_uuid']

    try:
        request = ServiceId()
        request.service_uuid.uuid = service_uuid
        request.context_id.context_uuid.uuid = context_uuid
        service_client.connect()
        service_client.DeleteService(request)
        service_client.close()

        flash('Service "{:s}" deleted successfully!'.format(service_uuid), 'success')
    except Exception as e:
        flash('Problem deleting service "{:s}": {:s}'.format(service_uuid, str(e.details())), 'danger')
        current_app.logger.exception(e)
    return redirect(url_for('service.home'))

#Added routes for creating a new Service
@service.route('add', methods=['GET', 'POST'])                                      #Route for adding a new service          [Selecting the type of operation to be performed - First Form]
def add():
    form_1 = AddServiceForm_1()
    if form_1.validate_on_submit():
        #store the selected service type in session
        #session['service_type'] = form_1.service_type.data
        #redirect to the same page to display the second form
        if form_1.service_type.data == 'ACL_L2':
            return redirect(url_for('service.add_configure_ACL_L2'))
        elif form_1.service_type.data == 'ACL_IPV4':
            return redirect(url_for('service.add_configure_ACL_IPV4'))      
        elif form_1.service_type.data == 'ACL_IPV6':
            return redirect(url_for('service.add_configure_ACL_IPV6'))      
        elif form_1.service_type.data == 'L2VPN':
            return redirect(url_for('service.add_configure_L2VPN'))      
        elif form_1.service_type.data == 'L3VPN':
            return redirect(url_for('service.add_configure_L3VPN'))
    # display the first form
    return render_template('service/add.html', form_1=form_1, submit_text='Continue to configuraton')

@service.route('add/configure/ACL_L2', methods=['GET', 'POST'])                     #Route for adding a new ACL_L2 service   [Setting the parameters for defining the service]
def add_configure_ACL_L2():
    form_acl = AddServiceForm_ACL_L2()
    if form_acl.validate_on_submit():
        flash(f'New configuration was created', 'success')
        return redirect(url_for('service.home'))
    print(form_acl.errors)
    return render_template('service/configure_ACL_L2.html', form_acl=form_acl, submit_text='Add New Service')

@service.route('add/configure/ACL_IPV4', methods=['GET', 'POST'])                   #Route for adding a new ACL_IPV4 service [Setting the parameters for defining the service]
def add_configure_ACL_IPV4():
    form_acl = AddServiceForm_ACL_IPV4()
    if form_acl.validate_on_submit():
        flash(f'New configuration was created', 'success')
        return redirect(url_for('service.home'))
    print(form_acl.errors)
    return render_template('service/configure_ACL_IPV4.html', form_acl=form_acl, submit_text='Add New Service')

@service.route('add/configure/ACL_IPV6', methods=['GET', 'POST'])                   #Route for adding a new ACL_IPV6 service [Setting the parameters for defining the service]
def add_configure_ACL_IPV6():
    form_acl = AddServiceForm_ACL_IPV6()
    if form_acl.validate_on_submit():
        flash(f'New configuration was created', 'success')
        return redirect(url_for('service.home'))
    print(form_acl.errors)
    return render_template('service/configure_ACL_IPV6.html', form_acl=form_acl, submit_text='Add New Service')
 
@service.route('add/configure/L2VPN', methods=['GET', 'POST'])                      #Route for adding a new L2VPN service    [Setting the parameters for defining the service]
def add_configure_L2VPN():
    form_l2vpn = AddServiceForm_L2VPN()                                                                         #Load the AddServiceForm_L3VPN form defined in forms.py
    service_obj = Service()                                                                                     #Create a new instance of the Service class

    context_uuid, topology_uuid = get_context_and_topology_uuids()                                              #Get the topology and context UUIDS
    if context_uuid and topology_uuid:                                                                          #If the UUIDs exist
        context_client.connect()                                                                                #Connects to the context service using the context_client object
        grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)   #Call the get_topology() function to retrieve the topology information for the given context and topology UUIDs
        if grpc_topology:                                                                                       #If the topology is defined
            topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}          
            devices = get_filtered_devices(context_client, topo_device_uuids)                                   #Calls the fucntion that returns a list of devices that have UUIDs in the set of topology device UUIDs.
            choices = get_device_choices(devices)                                                               #Returns a list of tuples, where each tuple contains the index of the device in the list and the name of the device
            add_device_choices_to_form(choices, form_l2vpn.service_device_1)                                    #Adds the device choices to the select options for the form (Device1)
            add_device_choices_to_form(choices, form_l2vpn.service_device_2)                                    #Adds the device choices to the select options for the form (Device2)
        else:
            flash('Context({:s})/Topology({:s}) not found'.format(str(context_uuid), str(topology_uuid)), 'danger')     #If the topology is not found, display an error message and set the devices list to an empty list
    else:
        flash('Missing context or topology UUID', 'danger')                                                     #If the topology or context UUID is not found, display an error message

    if form_l2vpn.validate_on_submit():                                                                         #Check if the form has been submitted and is valid
        try:                                                                                                    #Calls a function that validates the selected devices and endpoints exists and are correct 
            [selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2] = validate_selected_devices_and_endpoints(form_l2vpn, devices)
        except Exception as e:                                                                                  #Catch any exception raised during the validation process 
            flash('{:s}'.format(str(e.args[0])), 'danger')
            current_app.logger.exception(e)
            return render_template('service/configure_L2VPN.html', form_l2vpn=form_l2vpn, submit_text='Add New Service')    #Render the L2VPN configuration form with the previously entered data and an error message

        #Check the specific values of the parameters dependent by the vendor of the device
        [vendor_1, vendor_2] = get_device_vendor(form_l2vpn, devices)
        try:
            validate_params_vendor(form_l2vpn, vendor_1, 1)
            validate_params_vendor(form_l2vpn, vendor_2, 2)
        except Exception as e:       
            flash('{:s}'.format(str(e.args[0])), 'danger')
            current_app.logger.exception(e)
            return render_template('service/configure_L2VPN.html', form_l2vpn=form_l2vpn, submit_text='Add New Service')    #Render the L2VPN configuration form with the previously entered data and an error message

        #Create definition of the Service:
        service_uuid, service_type, endpoint_ids = set_service_parameters(service_obj, form_l2vpn, selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2)    #Calls the function to set the Service - Endpoint UUIDS
        constraints = add_constraints(form_l2vpn)                                                               #Calls the function to add the constraint parameters for defining a service
        params_device_1_with_data = get_device_params(form_l2vpn, 1, service_type)                              #Calls the function that getst the parameters that will configure the service in the device-1
        params_device_2_with_data = get_device_params(form_l2vpn, 2, service_type)                              #Calls the function that getst the parameters that will configure the service in the device-2
        print(params_device_1_with_data)
        print(params_device_2_with_data)
        params_settings = {}                                                                                    #Param settings (Defined despite it has no value) -> Avoid error
        config_rules = [                                                                                        #Create the configuration rules from the params_with_data 
            json_config_rule_set(
                    '/settings', params_settings
                ),
            json_config_rule_set(
                '/device[{:s}]/endpoint[{:s}]/settings'.format(str(selected_device_1.name), str(selected_endpoint_1)), params_device_1_with_data
            ),
            json_config_rule_set(
                '/device[{:s}]/endpoint[{:s}]/settings'.format(str(selected_device_2.name), str(selected_endpoint_2)), params_device_2_with_data
            )
        ]
        service_client.connect()
        context_client.connect()
        device_client.connect()
        descriptor_json = json_service_l2nm_planned(service_uuid = service_uuid, endpoint_ids = endpoint_ids, constraints = constraints, config_rules = config_rules, context_uuid= context_uuid)
        descriptor_json = {"services": [descriptor_json]}                                               #Wrap the descriptor between the tag: "services": []"
        try:
            process_descriptors(descriptor_json)
            flash('Service "{:s}" added successfully!'.format(service_obj.service_id.service_uuid.uuid), 'success')     #If the service was added succesfully -> Flash success message with newly added service UUID.
            return redirect(url_for('service.home', service_uuid=service_obj.service_id.service_uuid.uuid))             #If the service was added succesfully -> Redirect to the service.home URL                                                            #Call the process_descriptors function to add the new service defined in the descriptor_json variable           
        except Exception as e:
            flash('Problem adding service: {:s}'.format((str(e.args[0]))), 'danger')                                    #If the service was NOT added succesfully -> Include the exception message in a flashed message 
            current_app.logger.exception(e)                                                                             #If the service was NOT added succesfully -> Log the exception using Flask's logger
        finally:
            context_client.close()                                                                                      
            device_client.close()
            service_client.close()
    return render_template('service/configure_L2VPN.html', form_l2vpn=form_l2vpn, submit_text='Add New Service')

@service.route('add/configure/L3VPN', methods=['GET', 'POST'])                      #Route for adding a new L3VPN service    [Setting the parameters for defining the service]
def add_configure_L3VPN():
    form_l3vpn = AddServiceForm_L3VPN()                                                                         #Load the AddServiceForm_L3VPN form defined in forms.py
    service_obj = Service()                                                                                     #Create a new instance of the Service class

    context_uuid, topology_uuid = get_context_and_topology_uuids()                                              #Get the topology and context UUIDS
    if context_uuid and topology_uuid:                                                                          #If the UUIDs exist
        context_client.connect()                                                                                #Connects to the context service using the context_client object
        grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)   #Call the get_topology() function to retrieve the topology information for the given context and topology UUIDs
        if grpc_topology:                                                                                       #If the topology is defined
            topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}          
            devices = get_filtered_devices(context_client, topo_device_uuids)                                   #Calls the fucntion that returns a list of devices that have UUIDs in the set of topology device UUIDs.
            choices = get_device_choices(devices)                                                               #Returns a list of tuples, where each tuple contains the index of the device in the list and the name of the device
            add_device_choices_to_form(choices, form_l3vpn.service_device_1)                                    #Adds the device choices to the select options for the form (Device1)
            add_device_choices_to_form(choices, form_l3vpn.service_device_2)                                    #Adds the device choices to the select options for the form (Device2)
        else:
            flash('Context({:s})/Topology({:s}) not found'.format(str(context_uuid), str(topology_uuid)), 'danger')     #If the topology is not found, display an error message and set the devices list to an empty list
    else:
        flash('Missing context or topology UUID', 'danger')                                                #If the topology or context UUID is not found, display an error message

    if form_l3vpn.validate_on_submit():
        try:
            [selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2] = validate_selected_devices_and_endpoints(form_l3vpn, devices) #Calls a function that validates the selected devices and endpoints exists and are correct
        except Exception as e:                                                                              # Catch any exception raised during the validation process 
            flash('{:s}'.format(str(e.args[0])), 'danger')
            current_app.logger.exception(e)
            return render_template('service/configure_L3VPN.html', form_l3vpn=form_l3vpn, submit_text='Add New Service')    #Render the L3VPN configuration form with the previously entered data and an error message
        #Create definition of the Service:
        service_uuid, service_type, endpoint_ids = set_service_parameters(service_obj, form_l3vpn, selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2)    #Calls the function to set the Service - Endpoint UUIDS
        constraints = add_constraints(form_l3vpn)                                                       #Calls the function to add the constraint parameters for defining a service
        params_device_1_with_data = get_device_params(form_l3vpn, 1, service_type)
        params_device_2_with_data = get_device_params(form_l3vpn, 2, service_type)
        params_settings = {}
        config_rules = [                                                                                                    #Create the configuration rules from the params_with_data 
            json_config_rule_set(
                    '/settings', params_settings
                ),
            json_config_rule_set(
                '/device[{:s}]/endpoint[{:s}]/settings'.format(str(selected_device_1.name), str(selected_endpoint_1)), params_device_1_with_data
            ),
            json_config_rule_set(
                '/device[{:s}]/endpoint[{:s}]/settings'.format(str(selected_device_2.name), str(selected_endpoint_2)), params_device_2_with_data
            )
        ]
        service_client.connect()
        context_client.connect()
        device_client.connect()
        descriptor_json = json_service_l3nm_planned(service_uuid = service_uuid, endpoint_ids = endpoint_ids, constraints = constraints, config_rules = config_rules, context_uuid= context_uuid)
        descriptor_json = {"services": [descriptor_json]}                                               #Wrap the descriptor between the tag: "services": []"
        try:
            process_descriptors(descriptor_json)
            flash('Service "{:s}" added successfully!'.format(service_obj.service_id.service_uuid.uuid), 'success')     #If the service was added succesfully -> Flash success message with newly added service UUID.
            return redirect(url_for('service.home', service_uuid=service_obj.service_id.service_uuid.uuid))             #If the service was added succesfully -> Redirect to the service.home URL                                                            #Call the process_descriptors function to add the new service defined in the descriptor_json variable           
        except Exception as e:
            flash('Problem adding service: {:s}'.format((str(e.args[0]))), 'danger')                                    #If the service was NOT added succesfully -> Include the exception message in a flashed message 
            current_app.logger.exception(e)                                                                             #If the service was NOT added succesfully -> Log the exception using Flask's logger
        finally:
            context_client.close()                                                                                        
            device_client.close()
            service_client.close()
    return render_template('service/configure_L3VPN.html', form_l3vpn=form_l3vpn, submit_text='Add New Service')


##Function for creating the service
DESCRIPTOR_LOADER_NUM_WORKERS = 10

def process_descriptors(descriptors):                                                                       #The function receives a "descriptors" parameter which has to be a JSON descriptor object 
    descriptor_loader = DescriptorLoader(descriptors, num_workers=DESCRIPTOR_LOADER_NUM_WORKERS)                                                       #Creates a descriptor_loader object 
    results = descriptor_loader.process()                                                                   #Calls the descriptor_loader.process method and saves the result in the results variable 
    for message,level in compose_notifications(results):                                                    #Retrieve the notifications that are obtained in the proccess
        if level == 'error':                                                                                
            LOGGER.warning('ERROR message={:s}'.format(str(message)))                                       #Display any error message in the LOG
        flash(message, level)                                                                               #Show any notification message to the user in the webUI by using flash()

##Functions for having a higher leaver of abstraction and understanding in the code:

def get_context_and_topology_uuids():                                                                       #Retrieve the context and topology UUIDs from the session, if they exist
    context_uuid = session.get('context_uuid')
    context_uuid = session.get('context_uuid')
    topology_uuid = session.get('topology_uuid')
    return context_uuid, topology_uuid                                                                      #Return the UUIDs as a tuple, or None if either is missing

def get_filtered_devices(context_client, topo_device_uuids):                                                #Call the ListDevices() method on the context client to retrieve a list of all devices
    grpc_devices = context_client.ListDevices(Empty())                                          
    return [device for device in grpc_devices.devices if device.device_id.device_uuid.uuid in topo_device_uuids]    #Filter the list of devices to only include those with UUIDs that appear in the topology    

def get_device_choices(devices):                                                                            #Create the tuple (Number, Device) that will be added to the form
    return [(i, str(device.name)) for i, device in enumerate(devices)]

def add_device_choices_to_form(choices, form):                                                              #Add the device choices (tuple) to the select options of the correspondent form
    form.choices += choices

def validate_selected_devices_and_endpoints(form, devices):                                                 #Validates that the 2 selected devices and 2 endpoints exist and are valid. Then it returns them
    selected_device_1 = devices[int(form.service_device_1.data)]                                                #Selected_Device1 will be the one selected by the user in the previously defined form field
    selected_device_2 = devices[int(form.service_device_2.data)]                                                #Selected_Device2 will be the one selected by the user in the previously defined form field
    if selected_device_1 == selected_device_2:
        raise ValidationError('The devices must be different!. Please select two valid and different devices')      # If it is not a valid endpoint -> Raise a Validation Error
    elif form.service_endpoint_1.data not in [endpoint.name for endpoint in selected_device_1.device_endpoints]:    # Check if the endpoint submitted by the user is a valid endpoint of the selected device
        raise ValidationError('The selected endpoint: ' + form.service_endpoint_1.data + ' is not a valid endpoint for: '+ selected_device_1.name + '. Please select an endpoint that is available for this device')
    elif form.service_endpoint_2.data not in [endpoint.name for endpoint in selected_device_2.device_endpoints]:    # Check if the endpoint submitted by the user is a valid endpoint of the selected device
        raise ValidationError('The selected endpoint: ' + form.service_endpoint_2.data + ' is not a valid endpoint for: '+ selected_device_2.name + '. Please select an endpoint that is available for this device')
    else:
        selected_endpoint_1 = form.service_endpoint_1.data                                                      #If the selected endpoint is valid, save it in a variable
        selected_endpoint_2 = form.service_endpoint_2.data                                                      #If the selected endpoint is valid, save it in a variable
    return selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2                       #Return the devices and endpoints

def get_device_vendor(form, devices):
    selected_device_1 = devices[int(form.service_device_1.data)]                                                #Selected_Device1 will be the one selected by the user in the previously defined form field
    selected_device_2 = devices[int(form.service_device_2.data)]                                                #Selected_Device2 will be the one selected by the user in the previously defined form field
    
    for config_rule in selected_device_1.device_config.config_rules:
        if "vendor" in config_rule.custom.resource_value:
            vendor_config_rule_1 = config_rule.custom.resource_value

    for config_rule in selected_device_2.device_config.config_rules:
        if "vendor" in config_rule.custom.resource_value:
            vendor_config_rule_2 = config_rule.custom.resource_value

    config_rule_dict_1 = json.loads(vendor_config_rule_1)
    config_rule_dict_2 = json.loads(vendor_config_rule_2)
    vendor_value_1 = config_rule_dict_1["vendor"]
    vendor_value_2 = config_rule_dict_2["vendor"]
    return vendor_value_1, vendor_value_2

def validate_params_vendor(form, vendor, device_num):  #num is an auxiliar variable that can be 1 or 2 for knowing if it corresponds to the first or second device
    if vendor == "ADVA":
        if form.NI_name.data != f"ELAN-AC:{getattr(form, f'Device_{device_num}_IF_vlan_id').data}":
            raise ValidationError('For an ADVA device, the name of the Network Instance should have this name: "ELAN-AC:vlanID"')

        elif getattr(form, f'Device_{device_num}_NI_VC_ID').data != getattr(form, f'Device_{device_num}_IF_vlan_id').data:
            raise ValidationError('For an ADVA device, the value of the VlanID and the value of the VC_ID must be the same')
    else:
        None
    return None

def set_service_parameters(service_obj, form, selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2):  #Function to retrieve and set the service parameters for defining the service
    #Service UUID:
    service_obj.service_id.service_uuid.uuid = str(form.service_name.data)                            #Create the Service UUID (Unique Identifier of the service) from the service name
    service_uuid = service_obj.service_id.service_uuid.uuid
    #Service type [OPTIONS Defined in Context.proto]: 0(Unknown), 1(L3NM), 2(L2NM), 3(TAPI_CONNECTIVITY_SERVICE), 4(ACL) 
    service_obj.service_type = int(form.service_type.data)                                                  #Set the Service type as selected by the user in the form
    service_type = service_obj.service_type
    # Set the endpoint IDs
    endpoint_ids = [                                                                                        #Create a list containing a element that represents the Selected Device ID and the Selected Endpoint
        json_endpoint_id(json_device_id(selected_device_1.name), str(selected_endpoint_1)),
        json_endpoint_id(json_device_id(selected_device_2.name), str(selected_endpoint_2))
    ]
    return service_uuid, service_type, endpoint_ids

def add_constraints(form):                                                                                  #Function to add the constraints for a definition of a service
    constraints = []                                                                                        #Constraints -> Creates a list in which the constraints for the service will be added                                               
    if form.service_capacity.data:
        constraints.append(json_constraint_sla_capacity(float(form.service_capacity.data)))                                 #Capacity [Gbps]
    if form.service_latency.data:
        constraints.append(json_constraint_sla_latency(float(form.service_latency.data)))                                   #Latency [ms]
    if form.service_availability.data:
        constraints.append(json_constraint_sla_availability(1, True, float(form.service_availability.data)))                #Availability [%]
    if form.service_isolation.data is not None and form.service_isolation.data != '':
        constraints.append(json_constraint_sla_isolation([getattr(IsolationLevelEnum, str(form.service_isolation.data))]))  #Isolation (Predefined values)

    return constraints                                                                                                      #Returns a list with the constraints and values       

def get_device_params(form, device_num, form_type):                                                         #Function to retrieve and set the device parameters for defining the service
    if form_type == 2:                                                                                      #Type2 = L2NM
        device_params = {
            'ni_name': str(getattr(form, 'NI_name').data),
            'sub_interface_index': str(getattr(form, f'Device_{device_num}_IF_index').data),
            'vlan_id': str(getattr(form, f'Device_{device_num}_IF_vlan_id').data),
            'remote_router': str(getattr(form, f'Device_{device_num}_NI_remote_system').data),
            'circuit_id': str(getattr(form, f'Device_{device_num}_NI_VC_ID').data),
            'mtu': str(getattr(form, f'Device_{device_num}_IF_mtu').data),
            'ni_description': str(getattr(form, 'NI_description').data),
            'subif_description': str(getattr(form, f'Device_{device_num}_IF_description').data),
        }
    elif form_type == 1:                                                                                    #Type1 = L3NM
        if device_num == 1:
            policy_az_field = 'NI_import_policy'
            policy_za_field = 'NI_export_policy'
        elif device_num == 2:
            policy_az_field = 'NI_export_policy'
            policy_za_field = 'NI_import_policy'
        device_params = {
            'ni_name': str(getattr(form, 'NI_name').data),
            'bgp_as':str(getattr(form, 'NI_as').data),
            'route_distinguisher': str(getattr(form, 'NI_route_distinguisher').data),
            'sub_interface_index': str(getattr(form, f'Device_{device_num}_IF_index').data),
            'router_id': str(getattr(form, 'NI_router_id').data),
            'vlan_id': str(getattr(form, f'Device_{device_num}_IF_vlan_id').data),
            'address_ip': str(getattr(form, f'Device_{device_num}_IF_address_ip').data),
            'address_prefix': str(getattr(form, f'Device_{device_num}_IF_address_prefix').data),
            'policy_AZ': str(getattr(form, policy_az_field).data),
            'policy_ZA': str(getattr(form, policy_za_field).data),
            'mtu': str(getattr(form, f'Device_{device_num}_IF_mtu').data),
            'ni_description': str(getattr(form, 'NI_description').data),
            'subif_description': str(getattr(form, f'Device_{device_num}_IF_description').data),
        }
    else:
        raise ValueError(f'Unsupported form type: {form_type}')

    params_with_data = {k: v for k, v in device_params.items() if v is not None and str(v) != 'None' and v != ''}       #Retrieve the params that do not have value (None or ' ')
    return params_with_data
