# 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 grpc
from typing import Tuple
from common.Constants import DEFAULT_CONTEXT_UUID, DEFAULT_TOPOLOGY_UUID
from common.orm.Database import Database
from common.orm.HighLevel import get_object, get_or_create_object, update_or_create_object
from common.orm.backend.Tools import key_to_str
from common.proto.context_pb2 import Service, ServiceId
from common.rpc_method_wrapper.ServiceExceptions import InvalidArgumentException
from context.client.ContextClient import ContextClient
from .ConfigModel import delete_all_config_rules, grpc_config_rules_to_raw, update_config
from .ConstraintModel import delete_all_constraints, grpc_constraints_to_raw, update_constraints
from .ContextModel import ContextModel
from .EndPointModel import EndPointModel
from .RelationModels import ServiceEndPointModel
from .ServiceModel import ServiceModel, grpc_to_enum__service_status, grpc_to_enum__service_type
from .TopologyModel import TopologyModel

def update_service_in_local_database(database : Database, service : Service) -> Tuple[ServiceModel, bool]:
    service_uuid = service.service_id.service_uuid.uuid
    service_context_uuid = service.service_id.context_id.context_uuid.uuid
    if len(service_context_uuid) == 0: service_context_uuid = DEFAULT_CONTEXT_UUID

    topology_uuids = {}
    for i,endpoint_id in enumerate(service.service_endpoint_ids):
        endpoint_uuid                  = endpoint_id.endpoint_uuid.uuid
        endpoint_device_uuid           = endpoint_id.device_id.device_uuid.uuid
        endpoint_topology_uuid         = endpoint_id.topology_id.topology_uuid.uuid
        endpoint_topology_context_uuid = endpoint_id.topology_id.context_id.context_uuid.uuid

        if len(endpoint_device_uuid) == 0:
            raise InvalidArgumentException(
                'request.service_endpoint_ids[{:d}].device_id.device_uuid.uuid'.format(i), endpoint_device_uuid,
                ['not set'])

        if len(endpoint_topology_context_uuid) == 0: endpoint_topology_context_uuid = service_context_uuid
        if service_context_uuid != endpoint_topology_context_uuid:
            raise InvalidArgumentException(
                'request.service_endpoint_ids[{:d}].topology_id.context_id.context_uuid.uuid'.format(i),
                endpoint_device_uuid,
                ['should be == {:s}({:s})'.format('service_id.context_id.context_uuid.uuid', service_context_uuid)])

        topology_uuids.setdefault(endpoint_topology_uuid, set()).add(
            'request.service_endpoint_ids[{:d}].device_id.device_uuid.uuid'.format(i))

    if len(topology_uuids) > 1:
        raise InvalidArgumentException(
            'request.service_endpoint_ids', '...',
            ['Multiple different topology_uuid values specified: {:s}'.format(str(topology_uuids))])
    if len(topology_uuids) == 1:
        topology_uuid = topology_uuids.popitem()[0]
    else:
        topology_uuid = DEFAULT_TOPOLOGY_UUID

    result : Tuple[ContextModel, bool] = get_or_create_object(
        database, ContextModel, service_context_uuid, defaults={'context_uuid': service_context_uuid})
    db_context, _ = result

    str_topology_key = None
    if len(topology_uuid) > 0:
        str_topology_key = key_to_str([service_context_uuid, topology_uuid])
        result : Tuple[TopologyModel, bool] = get_or_create_object(
            database, TopologyModel, str_topology_key, defaults={
                'context_fk': db_context, 'topology_uuid': topology_uuid})
        #db_topology, _ = result

    str_service_key = key_to_str([service_context_uuid, service_uuid])

    config_rules = grpc_config_rules_to_raw(service.service_config.config_rules)
    delete_all_config_rules(database, str_service_key, 'running')
    running_config_result = update_config(database, str_service_key, 'running', config_rules)

    constraints = grpc_constraints_to_raw(service.service_constraints)
    delete_all_constraints(database, str_service_key, 'running')
    running_constraints_result = update_constraints(database, str_service_key, 'running', constraints)

    result : Tuple[ContextModel, bool] = get_or_create_object(
        database, ContextModel, service_context_uuid, defaults={
            'context_uuid': service_context_uuid,
        })
    db_context, _ = result

    result : Tuple[ServiceModel, bool] = update_or_create_object(database, ServiceModel, str_service_key, {
        'context_fk'            : db_context,
        'service_uuid'          : service_uuid,
        'service_type'          : grpc_to_enum__service_type(service.service_type),
        'service_status'        : grpc_to_enum__service_status(service.service_status.service_status),
        'service_constraints_fk': running_constraints_result[0][0],
        'service_config_fk'     : running_config_result[0][0],
    })
    db_service, updated = result

    for i,endpoint_id in enumerate(service.service_endpoint_ids):
        endpoint_uuid        = endpoint_id.endpoint_uuid.uuid
        endpoint_device_uuid = endpoint_id.device_id.device_uuid.uuid

        str_endpoint_key = key_to_str([endpoint_device_uuid, endpoint_uuid])
        if str_topology_key is not None:
            str_endpoint_key = key_to_str([str_endpoint_key, str_topology_key], separator=':')
        db_endpoint : EndPointModel = get_object(database, EndPointModel, str_endpoint_key)

        str_service_endpoint_key = key_to_str([str_service_key, str_endpoint_key], separator='--')
        result : Tuple[ServiceEndPointModel, bool] = get_or_create_object(
            database, ServiceEndPointModel, str_service_endpoint_key, {
                'service_fk': db_service, 'endpoint_fk': db_endpoint})
        _, service_endpoint_created = result
        updated = updated or service_endpoint_created

    return db_service, updated

def sync_service_from_context(
    context_uuid : str, service_uuid : str, context_client : ContextClient, database : Database
    ) -> Tuple[ServiceModel, bool]:

    try:
        service : Service = context_client.GetService(ServiceId(
            context_id={'context_uuid': {'uuid': context_uuid}},
            service_uuid={'uuid': service_uuid}))
    except grpc.RpcError as e:
        if e.code() != grpc.StatusCode.NOT_FOUND: raise # pylint: disable=no-member
        return None
    return update_service_in_local_database(database, service)

def sync_service_to_context(db_service : ServiceModel, context_client : ContextClient) -> None:
    if db_service is None: return
    context_client.SetService(Service(**db_service.dump(
        include_endpoint_ids=True, include_constraints=True, include_config_rules=True)))

def delete_service_from_context(db_service : ServiceModel, context_client : ContextClient) -> None:
    if db_service is None: return
    context_client.RemoveService(ServiceId(**db_service.dump_id()))
