# Copyright (c) 2015 Hewlett-Packard Co.
# All Rights Reserved.
#
#    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 math
import operator

import netaddr
from oslo_db import exception as db_exc
from oslo_utils import uuidutils

from neutron._i18n import _
from neutron.api.v2 import attributes
from neutron.common import constants
from neutron.common import exceptions as n_exc
from neutron.db import models_v2
from neutron.ipam import driver
from neutron.ipam import requests as ipam_req
from neutron.ipam import utils as ipam_utils


class SubnetAllocator(driver.Pool):
    """Class for handling allocation of subnet prefixes from a subnet pool.

       This class leverages the pluggable IPAM interface where possible to
       make merging into IPAM framework easier in future cycles.
    """

    def __init__(self, subnetpool, context):
        super(SubnetAllocator, self).__init__(subnetpool, context)
        self._sp_helper = SubnetPoolHelper()

    def _lock_subnetpool(self):
        """Lock subnetpool associated row.

        This method disallows to allocate concurrently 2 subnets in the same
        subnetpool, it's required to ensure non-overlapping cidrs in the same
        subnetpool.
        """

        current_hash = (self._context.session.query(models_v2.SubnetPool.hash)
                        .filter_by(id=self._subnetpool['id']).scalar())
        if current_hash is None:
            # NOTE(cbrandily): subnetpool has been deleted
            raise n_exc.SubnetPoolNotFound(
                subnetpool_id=self._subnetpool['id'])
        new_hash = uuidutils.generate_uuid()

        # NOTE(cbrandily): the update disallows 2 concurrent subnet allocation
        # to succeed: at most 1 transaction will succeed, others will be
        # rolled back and be caught in neutron.db.v2.base
        query = self._context.session.query(models_v2.SubnetPool).filter_by(
            id=self._subnetpool['id'], hash=current_hash)
        count = query.update({'hash': new_hash})
        if not count:
            raise db_exc.RetryRequest(n_exc.SubnetPoolInUse(
                                      subnet_pool_id=self._subnetpool['id']))

    def _get_allocated_cidrs(self):
        query = self._context.session.query(models_v2.Subnet)
        subnets = query.filter_by(subnetpool_id=self._subnetpool['id'])
        return (x.cidr for x in subnets)

    def _get_available_prefix_list(self):
        prefixes = (x.cidr for x in self._subnetpool.prefixes)
        allocations = self._get_allocated_cidrs()
        prefix_set = netaddr.IPSet(iterable=prefixes)
        allocation_set = netaddr.IPSet(iterable=allocations)
        available_set = prefix_set.difference(allocation_set)
        available_set.compact()
        return sorted(available_set.iter_cidrs(),
                      key=operator.attrgetter('prefixlen'),
                      reverse=True)

    def _num_quota_units_in_prefixlen(self, prefixlen, quota_unit):
        return math.pow(2, quota_unit - prefixlen)

    def _allocations_used_by_tenant(self, quota_unit):
        subnetpool_id = self._subnetpool['id']
        tenant_id = self._subnetpool['tenant_id']
        with self._context.session.begin(subtransactions=True):
            qry = self._context.session.query(models_v2.Subnet)
            allocations = qry.filter_by(subnetpool_id=subnetpool_id,
                                        tenant_id=tenant_id)
            value = 0
            for allocation in allocations:
                prefixlen = netaddr.IPNetwork(allocation.cidr).prefixlen
                value += self._num_quota_units_in_prefixlen(prefixlen,
                                                            quota_unit)
            return value

    def _check_subnetpool_tenant_quota(self, tenant_id, prefixlen):
        quota_unit = self._sp_helper.ip_version_subnetpool_quota_unit(
                                               self._subnetpool['ip_version'])
        quota = self._subnetpool.get('default_quota')

        if quota:
            used = self._allocations_used_by_tenant(quota_unit)
            requested_units = self._num_quota_units_in_prefixlen(prefixlen,
                                                                 quota_unit)

            if used + requested_units > quota:
                raise n_exc.SubnetPoolQuotaExceeded()

    def _allocate_any_subnet(self, request):
        with self._context.session.begin(subtransactions=True):
            self._lock_subnetpool()
            self._check_subnetpool_tenant_quota(request.tenant_id,
                                                request.prefixlen)
            prefix_pool = self._get_available_prefix_list()
            for prefix in prefix_pool:
                if request.prefixlen >= prefix.prefixlen:
                    subnet = next(prefix.subnet(request.prefixlen))
                    gateway_ip = request.gateway_ip
                    if not gateway_ip:
                        gateway_ip = subnet.network + 1
                    pools = ipam_utils.generate_pools(subnet.cidr,
                                                      gateway_ip)

                    return IpamSubnet(request.tenant_id,
                                      request.subnet_id,
                                      subnet.cidr,
                                      gateway_ip=gateway_ip,
                                      allocation_pools=pools)
            msg = _("Insufficient prefix space to allocate subnet size /%s")
            raise n_exc.SubnetAllocationError(reason=msg %
                                              str(request.prefixlen))

    def _allocate_specific_subnet(self, request):
        with self._context.session.begin(subtransactions=True):
            self._lock_subnetpool()
            self._check_subnetpool_tenant_quota(request.tenant_id,
                                                request.prefixlen)
            cidr = request.subnet_cidr
            available = self._get_available_prefix_list()
            matched = netaddr.all_matching_cidrs(cidr, available)
            if len(matched) is 1 and matched[0].prefixlen <= cidr.prefixlen:
                return IpamSubnet(request.tenant_id,
                                  request.subnet_id,
                                  cidr,
                                  gateway_ip=request.gateway_ip,
                                  allocation_pools=request.allocation_pools)
            msg = _("Cannot allocate requested subnet from the available "
                    "set of prefixes")
            raise n_exc.SubnetAllocationError(reason=msg)

    def allocate_subnet(self, request):
        max_prefixlen = int(self._subnetpool['max_prefixlen'])
        min_prefixlen = int(self._subnetpool['min_prefixlen'])
        if request.prefixlen > max_prefixlen:
            raise n_exc.MaxPrefixSubnetAllocationError(
                              prefixlen=request.prefixlen,
                              max_prefixlen=max_prefixlen)
        if request.prefixlen < min_prefixlen:
            raise n_exc.MinPrefixSubnetAllocationError(
                              prefixlen=request.prefixlen,
                              min_prefixlen=min_prefixlen)

        if isinstance(request, ipam_req.AnySubnetRequest):
            return self._allocate_any_subnet(request)
        elif isinstance(request, ipam_req.SpecificSubnetRequest):
            return self._allocate_specific_subnet(request)
        else:
            msg = _("Unsupported request type")
            raise n_exc.SubnetAllocationError(reason=msg)

    def get_subnet(self, subnet_id):
        raise NotImplementedError()

    def update_subnet(self, request):
        raise NotImplementedError()

    def remove_subnet(self, subnet_id):
        raise NotImplementedError()


class IpamSubnet(driver.Subnet):

    def __init__(self,
                 tenant_id,
                 subnet_id,
                 cidr,
                 gateway_ip=None,
                 allocation_pools=None):
        self._req = ipam_req.SpecificSubnetRequest(
            tenant_id,
            subnet_id,
            cidr,
            gateway_ip=gateway_ip,
            allocation_pools=allocation_pools)

    def allocate(self, address_request):
        raise NotImplementedError()

    def deallocate(self, address):
        raise NotImplementedError()

    def get_details(self):
        return self._req


class SubnetPoolReader(object):
    '''Class to assist with reading a subnetpool, loading defaults, and
       inferring IP version from prefix list. Provides a common way of
       reading a stored model or a create request with default table
       attributes.
    '''
    MIN_PREFIX_TYPE = 'min'
    MAX_PREFIX_TYPE = 'max'
    DEFAULT_PREFIX_TYPE = 'default'

    _sp_helper = None

    def __init__(self, subnetpool):
        self._read_prefix_info(subnetpool)
        self._sp_helper = SubnetPoolHelper()
        self._read_id(subnetpool)
        self._read_prefix_bounds(subnetpool)
        self._read_attrs(subnetpool,
                         ['tenant_id', 'name', 'is_default', 'shared'])
        self.description = subnetpool.get('description')
        self._read_address_scope(subnetpool)
        self.subnetpool = {'id': self.id,
                           'name': self.name,
                           'tenant_id': self.tenant_id,
                           'prefixes': self.prefixes,
                           'min_prefix': self.min_prefix,
                           'min_prefixlen': self.min_prefixlen,
                           'max_prefix': self.max_prefix,
                           'max_prefixlen': self.max_prefixlen,
                           'default_prefix': self.default_prefix,
                           'default_prefixlen': self.default_prefixlen,
                           'default_quota': self.default_quota,
                           'address_scope_id': self.address_scope_id,
                           'is_default': self.is_default,
                           'shared': self.shared,
                           'description': self.description}

    def _read_attrs(self, subnetpool, keys):
        for key in keys:
            setattr(self, key, subnetpool[key])

    def _ip_version_from_cidr(self, cidr):
        return netaddr.IPNetwork(cidr).version