# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
#
# 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 json, logging
from typing import List, Optional, Tuple
from common.Constants import DEFAULT_CONTEXT_UUID, DEFAULT_TOPOLOGY_UUID, INTERDOMAIN_TOPOLOGY_UUID
from common.proto.context_pb2 import (
    ConfigRule, Constraint, ContextId, Device, Empty, EndPointId, Slice, SliceStatusEnum)
from common.tools.context_queries.CheckType import device_type_is_network, endpoint_type_is_border
from common.tools.context_queries.InterDomain import get_local_device_uuids
from common.tools.grpc.ConfigRules import copy_config_rules
from common.tools.grpc.Constraints import copy_constraints
from common.tools.grpc.Tools import grpc_message_to_json, grpc_message_to_json_string
from common.tools.object_factory.Context import json_context_id
from context.client.ContextClient import ContextClient

LOGGER = logging.getLogger(__name__)

def compute_slice_owner(
    context_client : ContextClient, traversed_domains : List[Tuple[str, Device, bool, List[EndPointId]]]
) -> Optional[str]:
    traversed_domain_uuids = {traversed_domain[0] for traversed_domain in traversed_domains}

    existing_topology_ids = context_client.ListTopologyIds(ContextId(**json_context_id(DEFAULT_CONTEXT_UUID)))
    existing_topology_uuids = {
        topology_id.topology_uuid.uuid for topology_id in existing_topology_ids.topology_ids
    }
    existing_topology_uuids.discard(DEFAULT_TOPOLOGY_UUID)
    existing_topology_uuids.discard(INTERDOMAIN_TOPOLOGY_UUID)

    candidate_owner_uuids = traversed_domain_uuids.intersection(existing_topology_uuids)
    if len(candidate_owner_uuids) != 1:
        data = {
            'traversed_domain_uuids' : [td_uuid for td_uuid in traversed_domain_uuids ],
            'existing_topology_uuids': [et_uuid for et_uuid in existing_topology_uuids],
            'candidate_owner_uuids'  : [co_uuid for co_uuid in candidate_owner_uuids  ],
        }
        LOGGER.warning('Unable to identify slice owner: {:s}'.format(json.dumps(data)))
        return None

    return candidate_owner_uuids.pop()

def compose_slice(
    context_uuid : str, slice_uuid : str, endpoint_ids : List[EndPointId], constraints : List[Constraint] = [],
    config_rules : List[ConfigRule] = [], owner_uuid : Optional[str] = None
) -> Slice:
    slice_ = Slice()
    slice_.slice_id.context_id.context_uuid.uuid = context_uuid             # pylint: disable=no-member
    slice_.slice_id.slice_uuid.uuid = slice_uuid                            # pylint: disable=no-member
    slice_.slice_status.slice_status = SliceStatusEnum.SLICESTATUS_PLANNED  # pylint: disable=no-member

    if owner_uuid is not None:
        slice_.slice_owner.owner_uuid.uuid = owner_uuid                     # pylint: disable=no-member

    if len(endpoint_ids) >= 2:
        slice_.slice_endpoint_ids.add().CopyFrom(endpoint_ids[0])           # pylint: disable=no-member
        slice_.slice_endpoint_ids.add().CopyFrom(endpoint_ids[-1])          # pylint: disable=no-member

    if len(constraints) > 0:
        copy_constraints(constraints, slice_.slice_constraints)             # pylint: disable=no-member

    if len(config_rules) > 0:
        copy_config_rules(config_rules, slice_.slice_config.config_rules)   # pylint: disable=no-member

    return slice_

def map_abstract_endpoints_to_real(
    context_client : ContextClient, local_domain_uuid : str, abstract_endpoint_ids : List[EndPointId]
) -> List[EndPointId]:

    local_device_uuids = get_local_device_uuids(context_client)
    all_devices = context_client.ListDevices(Empty())

    map_endpoints_to_devices = dict()
    for device in all_devices.devices:
        LOGGER.info('[map_abstract_endpoints_to_real] Checking device {:s}'.format(
            grpc_message_to_json_string(device)))

        if device_type_is_network(device.device_type):
            LOGGER.info('[map_abstract_endpoints_to_real]   Ignoring network device')
            continue
        device_uuid = device.device_id.device_uuid.uuid
        if device_uuid not in local_device_uuids:
            LOGGER.info('[map_abstract_endpoints_to_real]   Ignoring non-local device')
            continue

        for endpoint in device.device_endpoints:
            LOGGER.info('[map_abstract_endpoints_to_real]   Checking endpoint {:s}'.format(
                grpc_message_to_json_string(endpoint)))
            endpoint_id = endpoint.endpoint_id
            device_uuid = endpoint_id.device_id.device_uuid.uuid
            endpoint_uuid = endpoint_id.endpoint_uuid.uuid
            map_endpoints_to_devices[(device_uuid, endpoint_uuid)] = endpoint_id
            if endpoint_type_is_border(endpoint.endpoint_type):
                map_endpoints_to_devices[(local_domain_uuid, endpoint_uuid)] = endpoint_id

    LOGGER.info('[map_abstract_endpoints_to_real] map_endpoints_to_devices={:s}'.format(
        str({
            endpoint_tuple:grpc_message_to_json(endpoint_id)
            for endpoint_tuple,endpoint_id in map_endpoints_to_devices.items()
        })))

    # map abstract device/endpoints to real device/endpoints
    real_endpoint_ids = []
    for endpoint_id in abstract_endpoint_ids:
        LOGGER.info('[map_abstract_endpoints_to_real] Mapping endpoint_id {:s} ...'.format(
                grpc_message_to_json_string(endpoint_id)))
        device_uuid = endpoint_id.device_id.device_uuid.uuid
        endpoint_uuid = endpoint_id.endpoint_uuid.uuid
        _endpoint_id = map_endpoints_to_devices.get((device_uuid, endpoint_uuid))
        if _endpoint_id is None:
            LOGGER.warning('map_endpoints_to_devices={:s}'.format(str(map_endpoints_to_devices)))
            MSG = 'Unable to map abstract EndPoint({:s}) to real one.'
            raise Exception(MSG.format(grpc_message_to_json_string(endpoint_id)))
        
        LOGGER.info('[map_abstract_endpoints_to_real] ... to endpoint_id {:s}'.format(
                grpc_message_to_json_string(_endpoint_id)))
        real_endpoint_ids.append(_endpoint_id)

    return real_endpoint_ids
