# Copyright 2018 Ericsson
#
# 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 uuid

import os_traits
from oslo_log import log as logging

from neutron_lib._i18n import _
from neutron_lib import constants as const
from neutron_lib.placement import constants as place_const


LOG = logging.getLogger(__name__)


def physnet_trait(physnet):
    """A Placement trait name to represent being connected to a physnet.

    :param physnet: The physnet name.
    :returns: The trait name representing the physnet.
    """
    return os_traits.normalize_name('%s%s' % (
        place_const.TRAIT_PREFIX_PHYSNET, physnet))


def vnic_type_trait(vnic_type):
    """A Placement trait name to represent support for a vnic_type.

    :param physnet: The vnic_type.
    :returns: The trait name representing the vnic_type.
    """
    return os_traits.normalize_name('%s%s' % (
        place_const.TRAIT_PREFIX_VNIC_TYPE, vnic_type))


def six_uuid5(namespace, name):
    """A uuid.uuid5 variant that takes utf-8 'name' both in Python 2 and 3.

    :param namespace: A UUID object used as a namespace in the generation of a
                      v5 UUID.
    :param name: Any string (either bytecode or unicode) used as a name in the
                 generation of a v5 UUID.
    :returns: A v5 UUID object.
    """

    # NOTE(bence romsics):
    # uuid.uuid5() behaves seemingly consistently but still incompatibly
    # different in cPython 2 and 3. Both expects the 'name' parameter to have
    # the type of the default string literal in each language version.
    # That is:
    #     The cPython 2 variant expects a byte string.
    #     The cPython 3 variant expects a unicode string.
    # Which types are called respectively 'str' and 'str' for the sake of
    # confusion. But the sha1() hash inside uuid5() always needs a byte string,
    # so we have to treat the two versions asymmetrically. See also:
    #
    # cPython 2.7:
    # https://github.com/python/cpython/blob
    #        /ea9a0994cd0f4bd37799b045c34097eb21662b3d/Lib/uuid.py#L603
    # cPython 3.6:
    # https://github.com/python/cpython/blob
    #        /e9e2fd75ccbc6e9a5221cf3525e39e9d042d843f/Lib/uuid.py#L628
    return uuid.uuid5(namespace=namespace, name=name)


# NOTE(bence romsics): The spec said: "Agent resource providers shall
# be identified by their already existing Neutron agent UUIDs [...]"
#
# https://review.opendev.org/#/c/508149/14/specs/rocky
#        /minimum-bandwidth-allocation-placement-api.rst@465
#
# However we forgot that agent UUIDs are not stable through a few
# admin operations like after a manual 'openstack network agent
# delete'. Here we make up a stable UUID instead.
def agent_resource_provider_uuid(namespace, host):
    """Generate a stable UUID for an agent.

    :param namespace: A UUID object identifying a mechanism driver (including
                      its agent).
    :param host: The hostname of the agent.
    :returns: A unique and stable UUID identifying an agent.
    """
    return six_uuid5(namespace=namespace, name=host)


def device_resource_provider_uuid(namespace, host, device, separator=':'):
    """Generate a stable UUID for a physical network device.

    :param namespace: A UUID object identifying a mechanism driver (including
                      its agent).
    :param host: The hostname of the agent.
    :param device: A host-unique name of the physical network device.
    :param separator: A string used in assembling a name for uuid5(). Choose
                      one that cannot occur either in 'host' or 'device'.
                      Optional.
    :returns: A unique and stable UUID identifying a physical network device.
    """
    name = separator.join([host, device])
    return six_uuid5(namespace=namespace, name=name)


def _parse_bandwidth_value(bw_str):
    """Parse the config string of a bandwidth value to an integer.

    :param bw_str: A decimal string represantation of an integer, allowing
                   the empty string.
    :raises: ValueError on invalid input.
    :returns: The bandwidth value as an integer or None if not set in config.
    """
    try:
        bw = None
        if bw_str:
            bw = int(bw_str)
            if bw < 0:
                raise ValueError()
    except ValueError:
        raise ValueError(_(
            'Cannot parse resource_provider_bandwidths. '
            'Expected: non-negative integer bandwidth value, got: %s') %
            bw_str)
    return bw


def parse_rp_bandwidths(bandwidths):
    """Parse and validate config option: resource_provider_bandwidths.

    Input in the config:
        resource_provider_bandwidths = eth0:10000:10000,eth1::10000,eth2::,eth3
    Input here:
        ['eth0:10000:10000', 'eth1::10000', 'eth2::', 'eth3']
    Output:
        {
            'eth0': {'egress': 10000, 'ingress': 10000},
            'eth1': {'egress': None, 'ingress': 10000},
            'eth2': {'egress': None, 'ingress': None},
            'eth3': {'egress': None, 'ingress': None},
        }

    :param bandwidths: The list of 'interface:egress:ingress' bandwidth
                       config options as pre-parsed by oslo_config.
    :raises: ValueError on invalid input.
    :returns: The fully parsed bandwidth config as a dict of dicts.
    """

    rv = {}
    for bandwidth in bandwidths:
        if ':' not in bandwidth:
            bandwidth += '::'
        try:
            device, egress_str, ingress_str = bandwidth.split(':')
        except ValueError:
            raise ValueError(_(
                'Cannot parse resource_provider_bandwidths. '
                'Expected: DEVICE:EGRESS:INGRESS, got: %s') % bandwidth)
        if device in rv:
            raise ValueError(_(
                'Cannot parse resource_provider_bandwidths. '
                'Same device listed multiple times: %s') % device)
        egress = _parse_bandwidth_value(egress_str)
        ingress = _parse_bandwidth_value(ingress_str)
        rv[device] = {
            const.EGRESS_DIRECTION: egress,
            const.INGRESS_DIRECTION: ingress,
        }
    return rv


def parse_rp_inventory_defaults(inventory_defaults):
    """Parse and validate config option: parse_rp_inventory_defaults.

    Cast the dict values to the proper numerical types.

    Input in the config:
        resource_provider_inventory_defaults = allocation_ratio:1.0,min_unit:1
    Input here:
        {
            'allocation_ratio': '1.0',
            'min_unit': '1',
        }
    Output here:
        {
            'allocation_ratio': 1.0,
            'min_unit': 1,
        }

    :param inventory_defaults: The dict of inventory parameters and values (as
                               strings) as pre-parsed by oslo_config.
    :raises: ValueError on invalid input.
    :returns: The fully parsed inventory parameters and values (as numerical
              values) as a dict.
    """

    unexpected_options = (set(inventory_defaults.keys()) -
                          place_const.INVENTORY_OPTIONS)
    if unexpected_options:
        raise ValueError(_(
            'Cannot parse inventory_defaults. Unexpected options: %s') %
            ','.join(unexpected_options))

    # allocation_ratio is a float
    try:
        if 'allocation_ratio' in inventory_defaults:
            inventory_defaults['allocation_ratio'] = float(
                inventory_defaults['allocation_ratio'])
            if inventory_defaults['allocation_ratio'] < 0:
                raise ValueError()
    except ValueError:
        raise ValueError(_(
            'Cannot parse inventory_defaults.allocation_ratio. '
            'Expected: non-negative float, got: %s') %
            inventory_defaults['allocation_ratio'])

    # the others are ints
    for key in ('min_unit', 'max_unit', 'reserved', 'step_size'):
        try:
            if key in inventory_defaults:
                inventory_defaults[key] = int(inventory_defaults[key])
                if inventory_defaults[key] < 0:
                    raise ValueError()
        except ValueError:
            raise ValueError(_(
                'Cannot parse inventory_defaults.%(key)s. '
                'Expected: non-negative int, got: %(got)s') % {
                    'key': key,
                    'got': inventory_defaults[key],
            })

    return inventory_defaults
