# 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 Dict
from flask import request
from flask.json import jsonify
from flask.wrappers import Response
from flask_restful import Resource
from werkzeug.exceptions import UnsupportedMediaType
from common.Constants import DEFAULT_CONTEXT_UUID
from common.Settings import get_setting
from common.tools.grpc.Tools import grpc_message_to_json_string
from context.client.ContextClient import ContextClient
from context.proto.context_pb2 import ConfigActionEnum, Service, ServiceId
from service.client.ServiceClient import ServiceClient
from .schemas.site_network_access import SCHEMA_SITE_NETWORK_ACCESS
from .tools.Authentication import HTTP_AUTH
from .tools.HttpStatusCodes import HTTP_NOCONTENT, HTTP_SERVERERROR
from .tools.Validator import validate_message
from .Constants import DEFAULT_ADDRESS_FAMILIES, DEFAULT_MTU, DEFAULT_SUB_INTERFACE_INDEX

LOGGER = logging.getLogger(__name__)

def process_site_network_access(context_client : ContextClient, site_network_access : Dict) -> Service:
    vpn_id = site_network_access['vpn-attachment']['vpn-id']
    cvlan_id = site_network_access['connection']['tagged-interface']['dot1q-vlan-tagged']['cvlan-id']
    bearer_reference = site_network_access['bearer']['bearer-reference']

    # Assume bearer_reference    = '<device_uuid>:<endpoint_uuid>:<router_id>'
    # Assume route_distinguisher = 0:<cvlan_id>
    device_uuid,endpoint_uuid,router_id = bearer_reference.split(':')
    route_distinguisher = '0:{:d}'.format(cvlan_id)

    # pylint: disable=no-member
    service_id = ServiceId()
    service_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_UUID
    service_id.service_uuid.uuid = vpn_id

    service_readonly = context_client.GetService(service_id)
    service = Service()
    service.CopyFrom(service_readonly)

    for endpoint_id in service.service_endpoint_ids:                        # pylint: disable=no-member
        if endpoint_id.device_id.device_uuid.uuid != device_uuid: continue
        if endpoint_id.endpoint_uuid.uuid != endpoint_uuid: continue
        break   # found, do nothing
    else:
        # not found, add it
        endpoint_id = service.service_endpoint_ids.add()                    # pylint: disable=no-member
        endpoint_id.device_id.device_uuid.uuid = device_uuid
        endpoint_id.endpoint_uuid.uuid = endpoint_uuid

    for config_rule in service.service_config.config_rules:                 # pylint: disable=no-member
        if config_rule.resource_key != 'settings': continue
        json_settings = json.loads(config_rule.resource_value)

        if 'route_distinguisher' not in json_settings:                      # missing, add it
            json_settings['route_distinguisher'] = route_distinguisher
        elif json_settings['route_distinguisher'] != route_distinguisher:   # differs, raise exception
            msg = 'Specified RouteDistinguisher({:s}) differs from Service RouteDistinguisher({:s})'
            raise Exception(msg.format(str(json_settings['route_distinguisher']), str(route_distinguisher)))

        if 'mtu' not in json_settings:                                      # missing, add it
            json_settings['mtu'] = DEFAULT_MTU
        elif json_settings['mtu'] != DEFAULT_MTU:                           # differs, raise exception
            msg = 'Specified MTU({:s}) differs from Service MTU({:s})'
            raise Exception(msg.format(str(json_settings['mtu']), str(DEFAULT_MTU)))

        if 'address_families' not in json_settings:                         # missing, add it
            json_settings['address_families'] = DEFAULT_ADDRESS_FAMILIES
        elif json_settings['address_families'] != DEFAULT_ADDRESS_FAMILIES: # differs, raise exception
            msg = 'Specified AddressFamilies({:s}) differs from Service AddressFamilies({:s})'
            raise Exception(msg.format(str(json_settings['address_families']), str(DEFAULT_ADDRESS_FAMILIES)))

        config_rule.resource_value = json.dumps(json_settings, sort_keys=True)
        break
    else:
        # not found, add it
        config_rule = service.service_config.config_rules.add()             # pylint: disable=no-member
        config_rule.action = ConfigActionEnum.CONFIGACTION_SET
        config_rule.resource_key = 'settings'
        config_rule.resource_value = json.dumps({
            'route_distinguisher': route_distinguisher,
            'mtu': DEFAULT_MTU,
            'address_families': DEFAULT_ADDRESS_FAMILIES,
        }, sort_keys=True)

    endpoint_settings_key = 'device[{:s}]/endpoint[{:s}]/settings'.format(device_uuid, endpoint_uuid)
    for config_rule in service.service_config.config_rules:                 # pylint: disable=no-member
        if config_rule.resource_key != endpoint_settings_key: continue
        json_settings = json.loads(config_rule.resource_value)

        if 'router_id' not in json_settings:                                # missing, add it
            json_settings['router_id'] = router_id
        elif json_settings['router_id'] != router_id:                       # differs, raise exception
            msg = 'Specified RouterId({:s}) differs from Service RouterId({:s})'
            raise Exception(msg.format(str(json_settings['router_id']), str(router_id)))

        if 'sub_interface_index' not in json_settings:                      # missing, add it
            json_settings['sub_interface_index'] = DEFAULT_SUB_INTERFACE_INDEX
        elif json_settings['sub_interface_index'] != DEFAULT_SUB_INTERFACE_INDEX:   # differs, raise exception
            msg = 'Specified SubInterfaceIndex({:s}) differs from Service SubInterfaceIndex({:s})'
            raise Exception(msg.format(
                str(json_settings['sub_interface_index']), str(DEFAULT_SUB_INTERFACE_INDEX)))

        config_rule.resource_value = json.dumps(json_settings, sort_keys=True)
        break
    else:
        # not found, add it
        config_rule = service.service_config.config_rules.add()             # pylint: disable=no-member
        config_rule.action = ConfigActionEnum.CONFIGACTION_SET
        config_rule.resource_key = endpoint_settings_key
        config_rule.resource_value = json.dumps({
            'router_id': router_id,
            'sub_interface_index': DEFAULT_SUB_INTERFACE_INDEX,
        }, sort_keys=True)

    return service

def process_list_site_network_access(
    context_client : ContextClient, service_client : ServiceClient, request_data : Dict) -> Response:

    LOGGER.debug('Request: {:s}'.format(str(request_data)))
    validate_message(SCHEMA_SITE_NETWORK_ACCESS, request_data)

    errors = []
    for site_network_access in request_data['ietf-l2vpn-svc:site-network-access']:
        try:
            service_request = process_site_network_access(context_client, site_network_access)
            LOGGER.debug('service_request = {:s}'.format(grpc_message_to_json_string(service_request)))
            service_reply = service_client.UpdateService(service_request)
            if service_reply != service_request.service_id: # pylint: disable=no-member
                raise Exception('Service update failed. Wrong Service Id was returned')
        except Exception as e: # pylint: disable=broad-except
            LOGGER.exception('Something went wrong Updating Service {:s}'.format(str(request)))
            errors.append({'error': str(e)})

    response = jsonify(errors)
    response.status_code = HTTP_NOCONTENT if len(errors) == 0 else HTTP_SERVERERROR
    return response

class L2VPN_SiteNetworkAccesses(Resource):
    def __init__(self) -> None:
        super().__init__()
        self.context_client = ContextClient(
            get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC'))
        self.service_client = ServiceClient(
            get_setting('SERVICESERVICE_SERVICE_HOST'), get_setting('SERVICESERVICE_SERVICE_PORT_GRPC'))

    @HTTP_AUTH.login_required
    def post(self, site_id : str):
        if not request.is_json: raise UnsupportedMediaType('JSON payload is required')
        LOGGER.debug('Site_Id: {:s}'.format(str(site_id)))
        return process_list_site_network_access(self.context_client, self.service_client, request.json)

    @HTTP_AUTH.login_required
    def put(self, site_id : str):
        if not request.is_json: raise UnsupportedMediaType('JSON payload is required')
        LOGGER.debug('Site_Id: {:s}'.format(str(site_id)))
        return process_list_site_network_access(self.context_client, self.service_client, request.json)
