""" Open5GS Actuator Manager

	This module implements a simple Actuator Manager for Open5GS network core.
	It discovers networking relationships between network functions in the core and
	the connected data network.

	UE ---- gNodeB ----- UPF ------- data network

	The actuator-specific configuration includes:
		
		- ``auth``:

			- ``api_key``: A token generated by the Kubernetes cluster for the service account used for context discovery.
			- ``host``: DNS name or IP address of the Kubernetes endpoint (including port number)

		- ``config``:
		
			- ``cacert``: Location of the CA certificate used to sign the endpoint HTTPS certificate (if not installed in the local host).
 			- ``namespaces``: list of namespace names to query (all namespace if not given).

"""

from dotenv import dotenv_values

import logging
import yaml
import ipaddress
from glob import glob

from otupy.profiles import slpf
from otupy.profiles.ctxd.data.application import Application
from otupy import IPv4Addr, IPv4Net


from otupy.actuators.ctxd.ctxd_actuator import CTXDActuator
from otupy.profiles.ctxd import *

from otupy import ArrayOf, actuator_implementation
import otupy.profiles.ctxd as ctxd

logger = logging.getLogger(__name__)


@actuator_implementation("ctxd-open5gs")
class CTXDActuator_open5gs(CTXDActuator):
	""" Open5GS Actuator Manager

		Extend the base `CTDXActuator` to retrieve services and links for a Open5GS core. 
		
		A bit of understanding about this actuator. A network operator deploys its network
		and provides the ctxd service. There are alternative deployment ways: in servers, 
		in virtual machines, as cloud native application. We currently assume the cloud
		native deployment in Kubernetes, and this means we have the manifest files available
		to create the links to K8S resources. Support for additional deployment options
		should be added in the future, if necessary.

	"""

	config_file : any = None #configuration file path

	def __init__(self, **kwargs):
		""" Initialization

			The necessary parameters are:
			- auth: Authentication information to connect to a Kubernetes cluster
			Optional parameters:
			- namespaces: List of namespaces which resources are to be discovered. If not provided, resources from all namespaces will be discovered.
			- dns: DNS name for the kubernets cloud
		"""
		super().__init__(**kwargs)

		self.mobilenets = []
		self.mobileterminals_services = []
		try:
			self._load_open5gs_config(**kwargs)
			self.configured = True
		except Exception as e:
			logger.error("Unable to load open5gs configuration: %s", e)
			self.configured = False

		try:
			self.service_name=kwargs['config']['name']
		except Exception as e:
			logger.error("Unable to get open5gs service name: %s", e)
			self.service_name="open5gs"


	def discover_context(self):
		self.discover_services()
		self.discover_links()

	def discover_services(self):
		""" Mobile network + data network 

			This function creates two services for the mobile network and its backing data network
			Additionally, it creates the users of the network.
		"""
		if not self.configured:
			return

		# Main 5G composite service
		mobile = MobileNetwork({'name': self.net_name, 'mcc': self.mcc, 'mnc': self.mnc,
				'nets': ArrayOf(IPNetAddress)(self.mobilenets) })
		mobilenet = Network(name="Open5Gs", description="Open5Gs core and UERASIM RAN",
				type=NetworkType(mobile), version=self.cn5t_version)

		self.service_sid=SId.create_from_service_type(mobilenet)
		self.services.append(Service(name=Name(self.service_name), type=ServiceType(mobilenet),
				sid=self.service_sid, 
				subservices=ArrayOf(SId)(self.mobile_services), owner=self.owner, release=None))

		# Data network 
		data = IPNetwork({'nets': [IPNetAddress(self.datanet)]})
		datanet = Network(name="DataNetwork", description="Open5Gs data network",
				type=NetworkType(data))

		self.datanet_service = Service(name=Name("Data Network"), type=ServiceType(datanet),
				sid=SId.create_from_service_type(datanet, domain=self.service_sid.name),
				domain=self.service_sid.name,
				subservices=ArrayOf(SId)(), owner=self.service_sid.name, release=None)
		self.services.append(self.datanet_service)

		# Tunnel network to connect mobile terminals
		tunnel = TunnelNetwork({'server': self.upf, 'nets': ArrayOf(IPNetAddress)(self.mobilenets)})
		tunnelnet = Network(name="Tunnel", description="5G internal tunnel",
							type=NetworkType(tunnel))
		self.tunnel_service = Service(name=Name("5G Tunnel"), type=ServiceType(tunnelnet),
										sid=SId.create_from_service_type(tunnelnet, domain=self.service_sid.name),
										domain=self.service_sid.name,
										subservices=None, owner=self.service_sid.name, release=None)
		self.services.append(self.tunnel_service)


		# Router between tunnel network and data network
		netfun = NetworkFunction(name=self.upf.name, id=None,
				version=self.cn5t_version,
				description="UPF routing function",
				type=NetworkFunctionType( Router() ))
		self.router=Service(name=Name("Router"),
				sid=SId.create_from_service_type(netfun, domain=self.service_sid.name),
				domain=self.service_sid.name,
				type=ServiceType( netfun ), owner=self.service_sid.name)
		self.services.append(self.router)

		for u in self.users:
			# TODO: discover connected devices by querying the mobile network service
			iot = Host(name=u['name'], description="Mobile phone", 
							type=HostType(IoT(type="phone")))
			self.mobileterminals_services.append(Service(name=Name(u['name']), type=ServiceType(iot),
					sid=SId.create_from_service_type(iot),
					subservices=ArrayOf(SId)(), owner=self.owner, release=None))
	


	def discover_links(self):
		if not self.configured:
			return

		# Network service hosted in cloud
		cloud_sid=SId(name=self.k8s_domain, type=ServiceType.get_type_name(Cloud),
					subtype="k8s", domain=self.k8s_domain)
		peer = Peer(service_name=Name("Kubernetes"), sid=cloud_sid,
							role=PeerRole.host, consumer=self.get_consumer(sid=cloud_sid))
		self.links.append(Link(name=Name(self.service_name), sid=self.service_sid,
									link_type=LinkType.hosting, role=PeerRole.guest,
									peers=ArrayOf(Peer)([peer])))


		# Links between upf and datanet/tunnelnet
		peer = Peer(service_name=Name(self.upf.name), sid=self.upf,
							role=PeerRole.endpoint, consumer=self.get_consumer(sid=self.upf))
		self.links.append(Link(name=self.datanet_service.name, sid=self.datanet_service.sid,
								description="Data Network connected to upf",
								link_type=LinkType.packet_flow, role=PeerRole.forwarding, 
								peers=ArrayOf(Peer)([peer])))
		self.links.append(Link(name=self.tunnel_service.name, sid=self.tunnel_service.sid,
								description="Tunnel Network connected to upf",
								link_type=LinkType.packet_flow, role=PeerRole.forwarding,
								peers=ArrayOf(Peer)([peer])))
		# Router hosted in upf
		peer.role=PeerRole.host
		self.links.append(Link(name=self.router.name, sid=self.router.sid,
								description="Routing function implemented on UPF",
								link_type=LinkType.hosting, role=PeerRole.guest, 
								peers=ArrayOf(Peer)([peer])))
		# Links between upf router and datanet/tunnelnet
		peer = Peer(service_name=Name(self.router.name), sid=self.router.sid,
							role=PeerRole.forwarding, consumer=None)
		self.links.append(Link(name=self.datanet_service.name, sid=self.datanet_service.sid,
								description="Routing function connected to Data Network",
								link_type=LinkType.packet_flow, role=PeerRole.endpoint, 
								peers=ArrayOf(Peer)([peer])))
		self.links.append(Link(name=self.tunnel_service.name, sid=self.tunnel_service.sid,
								description="Routing function connected to Tunnel Network",
								link_type=LinkType.packet_flow, role=PeerRole.endpoint, 
								peers=ArrayOf(Peer)([peer])))
		print(Link(name=self.tunnel_service.name, sid=self.tunnel_service.sid,
								description="Routing function connected to Tunnel Network",
								link_type=LinkType.packet_flow, role=PeerRole.endpoint, 
								peers=ArrayOf(Peer)([peer])))

		# Add link between mobile terminals and tunnel network
		# Note: we do not consider the role of the gNodeB
		for u in self.mobileterminals_services:
			peer = Peer(service_name=u.name, sid=u.sid,
					role=PeerRole.endpoint, # Network transport packets
					consumer=self.get_consumer(sid=u.sid))
			description="User "+str(u.name)+" connected to network "+str(self.tunnel_service.name)
			self.links.append(Link(name=self.tunnel_service.name, sid=self.tunnel_service.sid,
						description=description,
						link_type=LinkType.packet_flow, role=PeerRole.forwarding,
						peers=ArrayOf(Peer)([peer])))

	def _load_open5gs_config(self, **kwargs):
		if kwargs['config']['deployment'] == 'kubernetes' and \
				kwargs['config']['manager'] == 'cn5t':
			self._load_open5gs_cn5t(**kwargs)
		else:
			raise NotImplementedError("Unsupported deployment/manager option")


	def _load_open5gs_cn5t(self, **kwargs):

		# Load configuration parameters
		try:
			config=dotenv_values(kwargs['config']['config_dir']+'/config/cn5t.env')
		except:
			logger.error("Unable to load cn5t configuration file")
			self.configured=False
		self.datanet=config['DATANETWORK1']
		self.k8s_domain=config['K8S_DOMAIN']
		self.k8s_namespace=config['K8S_NAMESPACE']
		self.cn5t_version=config['CN5T_RELEASE']
		self.mcc=config['OPEN5GS_MCC']
		self.mnc=config['OPEN5GS_MNC']
		self.ssd=config['OPEN5GS_SST']
		self.net_name=config['OPEN5GS_NETNAME']
		malicious_users=config['DDOS_REPLICAS']
		licit_users=config['CURL_REPLICAS']
		self.users = []
		logger.warn("TODO: implement real user discovery from mobile network")
		for u in range(0, int(licit_users)):
			self.users.append({'name': 'curl'+str(u)})
		for u in range(0, int(malicious_users)):
			self.users.append({'name': 'goldeneye'+str(u)})

		with open(kwargs['config']['config_dir']+"/templates/config/30_open5gs/amf-configmap.yaml") as file:
			c=yaml.safe_load(file)
		for k,v in c['data'].items():
			amf_conf=yaml.safe_load(c['data'][k])

#		self.net_name=amf_conf['amf']['network_name']['full']
#		for i in amf_conf['amf']['plmn_support']:
#			self.mcc=str(i['plmn_id']['mcc'])
#			self.mnc=i['plmn_id']['mnc']

		with open(kwargs['config']['config_dir']+"/templates/config/30_open5gs/upf-configmap.yaml") as file:
			c=yaml.safe_load(file)
		for k,v in c['data'].items():
			upf_conf=yaml.safe_load(c['data'][k])

		for i in upf_conf['upf']['subnet']:
			self.mobilenets.append( ipaddress.ip_network(i['addr'],strict=False) )

		self.mobile_services = []
		for file in glob(kwargs['config']['config_dir']+"/templates/network/30_open5gs/*.yaml"):
			try:
				with open(file) as f:
					configs = yaml.load_all(f, Loader=yaml.FullLoader)
					for c in configs:
						try:
							if c['kind'] == "StatefulSet" or c['kind'] == "Deployment":
								sid=SId(name= c['metadata']['name'], type="app", domain=self.k8s_domain, 
										namespace=self.k8s_namespace, version=None) # Don't use version: not visible in Kubernetes!
								self.mobile_services.append(sid)
								try:
									if c['spec']['template']['metadata']['labels']['epc-role'] == 'upf':
										self.upf=sid
								except:
									pass
						except Exception as e:
							logger.warning("Unable to retrieve mobile network service list: %s", e)
			except Exception as e: 
				logger.error("Unable to parse %s: %s", file, e)
	



