# 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.

# Convert the path defined as explicit hops with ingress and egress endpoints per device into a set of connections and
# compute the dependencies among them.
#
# Example:
# o-- int DC1 eth1 -- 10/1 CS1 1/2 -- 1/2 R2 2/1 -- a7.. OLS 60.. -- 2/1 R3 1/1 -- 1/1 CS2 10/1 -- eth1 DC2 int --o
#         APP              PKT            PKT            CTRL            PKT           PKT              APP
#
# path_hops = [
#     {'device': 'DC1-GW', 'ingress_ep': 'int', 'egress_ep': 'eth1'},
#     {'device': 'CS1-GW1', 'ingress_ep': '10/1', 'egress_ep': '1/2'},
#     {'device': 'TN-R2', 'ingress_ep': '1/2', 'egress_ep': '2/1'},
#     {'device': 'TN-OLS', 'ingress_ep': 'a7a80b23a703', 'egress_ep': '60519106029e'},
#     {'device': 'TN-R3', 'ingress_ep': '2/1', 'egress_ep': '1/1'},
#     {'device': 'CS2-GW1', 'ingress_ep': '1/1', 'egress_ep': '10/1'},
#     {'device': 'DC2-GW', 'ingress_ep': 'eth1', 'egress_ep': 'int'}
# ]
#
# connections=[
#     (UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e'), ServiceTypeEnum.TAPI, [
#             {'device': 'TN-OLS', 'ingress_ep': '833760219d0f', 'egress_ep': 'cf176771a4b9'}
#         ], []),
#     (UUID('c2e57966-5d82-4705-a5fe-44cf6487219e'), ServiceTypeEnum.L2NM, [
#             {'device': 'CS1-GW1', 'ingress_ep': '10/1', 'egress_ep': '1/2'},
#             {'device': 'TN-R2', 'ingress_ep': '1/2', 'egress_ep': '2/1'},
#             {'device': 'TN-R3', 'ingress_ep': '2/1', 'egress_ep': '1/1'},
#             {'device': 'CS2-GW1', 'ingress_ep': '1/1', 'egress_ep': '10/1'}
#         ], [UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e')]),
#     (UUID('1e205c82-f6ea-4977-9e97-dc27ef1f4802'), ServiceTypeEnum.L2NM, [
#             {'device': 'DC1-GW', 'ingress_ep': 'int', 'egress_ep': 'eth1'},
#             {'device': 'DC2-GW', 'ingress_ep': 'eth1', 'egress_ep': 'int'}
#         ], [UUID('c2e57966-5d82-4705-a5fe-44cf6487219e')])
# ]

import enum, json, queue, uuid
from typing import Dict, List, Optional, Tuple
from common.DeviceTypes import DeviceTypeEnum
from common.proto.context_pb2 import Device, ServiceTypeEnum

class StackActionEnum(enum.Enum):
    PATH_INGRESS         = 'ingress'
    CREATE_CONNECTION    = 'create'
    APPEND_PATH_HOP      = 'append'
    CHAIN_CONNECTION     = 'chain'
    TERMINATE_CONNECTION = 'terminate'

def is_datacenter(dev_type : Optional[DeviceTypeEnum]) -> bool:
    return dev_type in {DeviceTypeEnum.DATACENTER, DeviceTypeEnum.EMULATED_DATACENTER}

def is_packet_router(dev_type : Optional[DeviceTypeEnum]) -> bool:
    return dev_type in {DeviceTypeEnum.PACKET_ROUTER, DeviceTypeEnum.EMULATED_PACKET_ROUTER}

def is_packet_switch(dev_type : Optional[DeviceTypeEnum]) -> bool:
    return dev_type in {DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH}

def is_packet_device(dev_type : Optional[DeviceTypeEnum]) -> bool:
    return is_packet_router(dev_type) or is_packet_switch(dev_type)

def is_tfs_controller(dev_type : Optional[DeviceTypeEnum]) -> bool:
    return dev_type in {DeviceTypeEnum.TERAFLOWSDN_CONTROLLER}

def is_mw_controller(dev_type : Optional[DeviceTypeEnum]) -> bool:
    return dev_type in {DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM, DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM}

def is_ipm_controller(dev_type : Optional[DeviceTypeEnum]) -> bool:
    return dev_type in {DeviceTypeEnum.XR_CONSTELLATION, DeviceTypeEnum.EMULATED_XR_CONSTELLATION}

def is_ols_controller(dev_type : Optional[DeviceTypeEnum]) -> bool:
    return dev_type in {DeviceTypeEnum.OPEN_LINE_SYSTEM, DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM}

def is_subdevice(dev_manager : Optional[str]) -> bool:
    return dev_manager is not None

def is_subdevice_equal(dev_manager_a : Optional[str], dev_manager_b : Optional[str]) -> bool:
    if dev_manager_a is None and dev_manager_b is None: return True
    if dev_manager_a is not None and dev_manager_b is not None: return dev_manager_a == dev_manager_b
    return False

def get_action(
    prv_type : Optional[DeviceTypeEnum], prv_manager : Optional[str],
    cur_type : DeviceTypeEnum, cur_manager : Optional[str]
) -> StackActionEnum:
    if prv_type is None:
        return StackActionEnum.PATH_INGRESS

    if is_datacenter(prv_type):
        if is_packet_device(cur_type): return StackActionEnum.CREATE_CONNECTION
        if is_tfs_controller(cur_type): return StackActionEnum.CREATE_CONNECTION

    if is_packet_device(prv_type):
        if is_datacenter(cur_type): return StackActionEnum.TERMINATE_CONNECTION
        if is_packet_device(cur_type):
            if is_subdevice_equal(cur_manager, prv_manager): return StackActionEnum.APPEND_PATH_HOP
            if is_subdevice(prv_manager) and not is_subdevice(cur_manager): return StackActionEnum.TERMINATE_CONNECTION
            if not is_subdevice(prv_manager) and is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION

        if is_mw_controller(cur_type) and not is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION
        if is_ols_controller(cur_type) and not is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION
        if is_tfs_controller(cur_type) and is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION

    if is_mw_controller(prv_type) or is_ols_controller(prv_type):
        if is_packet_device(cur_type): return StackActionEnum.TERMINATE_CONNECTION

    if is_tfs_controller(prv_type):
        if is_tfs_controller(cur_type) and is_subdevice_equal(prv_manager, cur_manager): return StackActionEnum.APPEND_PATH_HOP
        if is_datacenter(cur_type): return StackActionEnum.TERMINATE_CONNECTION
        if is_packet_device(cur_type): return StackActionEnum.TERMINATE_CONNECTION
        if is_mw_controller(cur_type) or is_ols_controller(cur_type): return StackActionEnum.CHAIN_CONNECTION

    str_fields = ', '.join([
        'prv_type={:s}'.format(str(prv_type)), 'prv_manager={:s}'.format(str(prv_manager)),
        'cur_type={:s}'.format(str(cur_type)), 'cur_manager={:s}'.format(str(cur_manager)),
    ])
    raise Exception('Undefined Action for ({:s})'.format(str_fields))

def get_device_manager_uuid(device : Device) -> Optional[str]:
    for config_rule in device.device_config.config_rules:
        if config_rule.WhichOneof('config_rule') != 'custom': continue
        if config_rule.custom.resource_key != '_manager': continue
        device_manager_id = json.loads(config_rule.custom.resource_value)
        return device_manager_id['uuid']
    return None

def get_device_type(
    grpc_device : Device, device_dict : Dict[str, Tuple[Dict, Device]], device_manager_uuid : Optional[str]
) -> DeviceTypeEnum:
    if device_manager_uuid is None:
        return DeviceTypeEnum._value2member_map_[grpc_device.device_type] # pylint: disable=no-member
    device_manager_tuple = device_dict.get(device_manager_uuid)
    if device_manager_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_manager_uuid)))
    _,grpc_device = device_manager_tuple
    return DeviceTypeEnum._value2member_map_[grpc_device.device_type] # pylint: disable=no-member

SERVICE_TYPE_LXNM = {ServiceTypeEnum.SERVICETYPE_L3NM, ServiceTypeEnum.SERVICETYPE_L2NM}

def get_service_type(device_type : DeviceTypeEnum, prv_service_type : ServiceTypeEnum) -> ServiceTypeEnum:
    if is_tfs_controller(device_type) or is_packet_router(device_type):
        if prv_service_type in SERVICE_TYPE_LXNM: return prv_service_type
    if is_packet_switch(device_type) or is_mw_controller(device_type):
        if prv_service_type == ServiceTypeEnum.SERVICETYPE_L2NM: return prv_service_type
    if is_ols_controller(device_type) or is_ipm_controller(device_type):
        return ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE

    str_fields = ', '.join([
        'device_type={:s}'.format(str(device_type)),
    ])
    raise Exception('Undefined Service Type for ({:s})'.format(str_fields))

def convert_explicit_path_hops_to_connections(
    path_hops : List[Dict], device_dict : Dict[str, Tuple[Dict, Device]],
    main_service_uuid : str, main_service_type : ServiceTypeEnum
) -> List[Tuple[str, int, List[str], List[str]]]:

    connection_stack = queue.LifoQueue()
    connections : List[Tuple[str, int, List[str], List[str]]] = list()
    prv_device_uuid = None
    prv_device_type = None
    prv_manager_uuid = None

    for path_hop in path_hops:
        device_uuid = path_hop['device']
        if prv_device_uuid == device_uuid: continue
        device_tuple = device_dict.get(device_uuid)
        if device_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_uuid)))
        _,grpc_device = device_tuple

        manager_uuid = get_device_manager_uuid(grpc_device)
        device_type = get_device_type(grpc_device, device_dict, manager_uuid)
        action = get_action(prv_device_type, prv_manager_uuid, device_type, manager_uuid)

        if action == StackActionEnum.PATH_INGRESS:
            connection_stack.put((main_service_uuid, main_service_type, [path_hop], []))
        elif action == StackActionEnum.CREATE_CONNECTION:
            connection_uuid = str(uuid.uuid4())
            prv_service_type = connection_stack.queue[-1][1]
            service_type = get_service_type(device_type, prv_service_type)
            connection_stack.put((connection_uuid, service_type, [path_hop], []))
        elif action == StackActionEnum.APPEND_PATH_HOP:
            connection_stack.queue[-1][2].append(path_hop)
        elif action == StackActionEnum.CHAIN_CONNECTION:
            connection = connection_stack.get()
            connections.append(connection)
            connection_stack.queue[-1][3].append(connection[0])

            connection_uuid = str(uuid.uuid4())
            prv_service_type = connection_stack.queue[-1][1]
            service_type = get_service_type(device_type, prv_service_type)
            connection_stack.put((connection_uuid, service_type, [path_hop], []))
        elif action == StackActionEnum.TERMINATE_CONNECTION:
            connection = connection_stack.get()
            connections.append(connection)
            connection_stack.queue[-1][3].append(connection[0])
            connection_stack.queue[-1][2].append(path_hop)
        else:
            raise Exception('Uncontrolled condition')

        prv_device_uuid = device_uuid
        prv_device_type = device_type
        prv_manager_uuid = manager_uuid

    # path egress
    connections.append(connection_stack.get())
    assert connection_stack.empty()
    return connections

def convert_explicit_path_hops_to_plain_connection(
    path_hops : List[Dict], main_service_uuid : str, main_service_type : ServiceTypeEnum
) -> List[Tuple[str, int, List[str], List[str]]]:

    connection : Tuple[str, int, List[str], List[str]] = \
        (main_service_uuid, main_service_type, [], [])

    prv_device_uuid = None
    for path_hop in path_hops:
        device_uuid = path_hop['device']
        if prv_device_uuid == device_uuid: continue
        connection[2].append(path_hop)
        prv_device_uuid = device_uuid

    return [connection]
