# 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, requests, uuid
from typing import Dict, List, Optional, Tuple
from common.proto.context_pb2 import Connection, Device, DeviceList, EndPointId, Link, LinkList, Service
from common.proto.pathcomp_pb2 import PathCompReply, PathCompRequest
from common.tools.grpc.Tools import grpc_message_to_json_string
from pathcomp.frontend.Config import BACKEND_URL
from .tools.ComposeRequest import compose_device, compose_link, compose_service

class _Algorithm:
    def __init__(self, algorithm_id : str, sync_paths : bool, class_name=__name__) -> None:
        # algorithm_id: algorithm to be executed
        # sync_paths: if multiple services are included in the request, tunes how to prevent contention. If true,
        #             services are computed one after the other and resources assigned to service i, are considered as
        #             used when computing services i+1..n; otherwise, resources are never marked as used during the
        #             path computation.

        self.logger = logging.getLogger(class_name)
        self.algorithm_id = algorithm_id
        self.sync_paths = sync_paths

        self.device_list : List[Dict] = list()
        self.device_dict : Dict[str, Tuple[Dict, Device]] = dict()
        self.endpoint_dict : Dict[str, Dict[str, Tuple[Dict, EndPointId]]] = dict()
        self.link_list : List[Dict] = list()
        self.link_dict : Dict[str, Tuple[Dict, Link]] = dict()
        self.endpoint_to_link_dict : Dict[Tuple[str, str], Tuple[Dict, Link]] = dict()
        self.service_list : List[Dict] = list()
        self.service_dict : Dict[Tuple[str, str], Tuple[Dict, Service]] = dict()

    def add_devices(self, grpc_devices : DeviceList) -> None:
        for grpc_device in grpc_devices.devices:
            json_device = compose_device(grpc_device)
            self.device_list.append(json_device)

            device_uuid = json_device['device_Id']
            self.device_dict[device_uuid] = (json_device, grpc_device)

            device_endpoint_dict : Dict[str, Tuple[Dict, EndPointId]] = dict()
            for json_endpoint,grpc_endpoint in zip(json_device['device_endpoints'], grpc_device.device_endpoints):
                endpoint_uuid = json_endpoint['endpoint_id']['endpoint_uuid']
                endpoint_tuple = (json_endpoint['endpoint_id'], grpc_endpoint.endpoint_id)
                device_endpoint_dict[endpoint_uuid] = endpoint_tuple

            self.endpoint_dict[device_uuid] = device_endpoint_dict

    def add_links(self, grpc_links : LinkList) -> None:
        for grpc_link in grpc_links.links:
            json_link = compose_link(grpc_link)
            self.link_list.append(json_link)

            link_uuid = json_link['link_Id']
            self.link_dict[link_uuid] = (json_link, grpc_link)

            for link_endpoint_id in json_link['link_endpoint_ids']:
                link_endpoint_id = link_endpoint_id['endpoint_id']
                device_uuid = link_endpoint_id['device_id']
                endpoint_uuid = link_endpoint_id['endpoint_uuid']
                endpoint_key = (device_uuid, endpoint_uuid)
                link_tuple = (json_link, grpc_link)
                self.endpoint_to_link_dict[endpoint_key] = link_tuple

    def add_service_requests(self, request : PathCompRequest) -> None:
        for grpc_service in request.services:
            json_service = compose_service(grpc_service)
            self.service_list.append(json_service)
            service_id = json_service['serviceId']
            service_key = (service_id['contextId'], service_id['service_uuid'])
            service_tuple = (json_service, grpc_service)
            self.service_dict[service_key] = service_tuple

    def execute(self, dump_request_filename : Optional[str] = None, dump_reply_filename : Optional[str] = None) -> None:
        request = {'serviceList': self.service_list, 'deviceList': self.device_list, 'linkList': self.link_list}

        if dump_request_filename is not None:
            with open(dump_request_filename, 'w', encoding='UTF-8') as f:
                f.write(json.dumps(request, sort_keys=True, indent=4))

        reply = requests.post(BACKEND_URL, json=request)
        self.status_code = reply.status_code
        self.raw_reply = reply.content.decode('UTF-8')

        if dump_reply_filename is not None:
            with open(dump_reply_filename, 'w', encoding='UTF-8') as f:
                f.write('status_code={:s} reply={:s}'.format(str(self.status_code), str(self.raw_reply)))

        self.logger.info('status_code={:s} reply={:s}'.format(str(reply.status_code), str(self.raw_reply)))

        if reply.status_code not in {requests.codes.ok}:
            raise Exception('Backend error({:s}) for request({:s})'.format(
                str(self.raw_reply), json.dumps(request, sort_keys=True)))
        
        self.json_reply = reply.json()

    def add_path_to_connection(self, connection : Connection, path_endpoints : List[Dict]) -> None:
        for endpoint in path_endpoints:
            device_uuid = endpoint['device_id']
            endpoint_uuid = endpoint['endpoint_uuid']
            endpoint_id = connection.path_hops_endpoint_ids.add()
            endpoint_id.CopyFrom(self.endpoint_dict[device_uuid][endpoint_uuid][1])

    def add_connection_to_reply(self, reply : PathCompReply, service : Service) -> Connection:
        connection = reply.connections.add()
        connection.connection_id.connection_uuid.uuid = str(uuid.uuid4())
        connection.service_id.CopyFrom(service.service_id)
        return connection

    def add_service_to_reply(self, reply : PathCompReply, context_uuid : str, service_uuid : str) -> Service:
        service_key = (context_uuid, service_uuid)
        tuple_service = self.service_dict.get(service_key)
        if tuple_service is None: raise Exception('ServiceKey({:s}) not found'.format(str(service_key)))
        _, grpc_service = tuple_service

        # TODO: implement support for multi-point services
        service_endpoint_ids = grpc_service.service_endpoint_ids
        if len(service_endpoint_ids) != 2: raise NotImplementedError('Service must have 2 endpoints')

        service = reply.services.add()
        service.CopyFrom(grpc_service)

        return grpc_service

    def get_reply(self) -> PathCompReply:
        response_list = self.json_reply.get('response-list', [])
        reply = PathCompReply()
        for response in response_list:
            service_id = response['serviceId']
            grpc_service = self.add_service_to_reply(reply, service_id['contextId'], service_id['service_uuid'])

            no_path_issue = response.get('noPath', {}).get('issue')
            if no_path_issue is not None:
                # no path found: leave connection with no endpoints
                # no_path_issue == 1 => no path due to a constraint
                continue

            for service_path in response['path']:
                grpc_connection = self.add_connection_to_reply(reply, grpc_service)
                self.add_path_to_connection(grpc_connection, service_path['devices'])

                # ... "path-capacity": {"total-size": {"value": 200, "unit": 0}},
                # ... "path-latency": {"fixed-latency-characteristic": "10.000000"},
                # ... "path-cost": {"cost-name": "", "cost-value": "5.000000", "cost-algorithm": "0.000000"},
                #path_capacity = service_path['path-capacity']['total-size']
                #path_capacity_value = path_capacity['value']
                #path_capacity_unit = CapacityUnit(path_capacity['unit'])
                #path_latency = service_path['path-latency']['fixed-latency-characteristic']
                #path_cost = service_path['path-cost']
                #path_cost_name = path_cost['cost-name']
                #path_cost_value = path_cost['cost-value']
                #path_cost_algorithm = path_cost['cost-algorithm']

        return reply
