# Copyright 2015 OpenStack LLC.
# 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.

from oslo_db import exception as db_exc
from oslo_utils import uuidutils
from sqlalchemy.orm import exc as orm_exc

from neutron.ipam.drivers.neutrondb_ipam import db_models
from neutron.ipam import exceptions as ipam_exc

# Database operations for Neutron's DB-backed IPAM driver


class IpamSubnetManager(object):

    @classmethod
    def load_by_neutron_subnet_id(cls, session, neutron_subnet_id):
        return session.query(db_models.IpamSubnet).filter_by(
            neutron_subnet_id=neutron_subnet_id).first()

    def __init__(self, ipam_subnet_id, neutron_subnet_id):
        self._ipam_subnet_id = ipam_subnet_id
        self._neutron_subnet_id = neutron_subnet_id

    @property
    def neutron_id(self):
        return self._neutron_subnet_id

    def create(self, session):
        """Create database models for an IPAM subnet.

        This method creates a subnet resource for the IPAM driver and
        associates it with its neutron identifier, if specified.

        :param session: database sesssion.
        :returns: the idenfier of created IPAM subnet
        """
        if not self._ipam_subnet_id:
            self._ipam_subnet_id = uuidutils.generate_uuid()
        ipam_subnet = db_models.IpamSubnet(
            id=self._ipam_subnet_id,
            neutron_subnet_id=self._neutron_subnet_id)
        session.add(ipam_subnet)
        return self._ipam_subnet_id

    @classmethod
    def delete(cls, session, neutron_subnet_id):
        """Delete IPAM subnet.

        IPAM subnet no longer has foreign key to neutron subnet,
        so need to perform delete manually

        :param session: database sesssion
        :param neutron_subnet_id: neutron subnet id associated with ipam subnet
        """
        return session.query(db_models.IpamSubnet).filter_by(
            neutron_subnet_id=neutron_subnet_id).delete()

    def create_pool(self, session, pool_start, pool_end):
        """Create an allocation pool and availability ranges for the subnet.

        This method does not perform any validation on parameters; it simply
        persist data on the database.

        :param pool_start: string expressing the start of the pool
        :param pool_end: string expressing the end of the pool
        :return: the newly created pool object.
        """
        ip_pool = db_models.IpamAllocationPool(
            ipam_subnet_id=self._ipam_subnet_id,
            first_ip=pool_start,
            last_ip=pool_end)
        session.add(ip_pool)
        ip_range = db_models.IpamAvailabilityRange(
            allocation_pool=ip_pool,
            first_ip=pool_start,
            last_ip=pool_end)
        session.add(ip_range)
        return ip_pool

    def delete_allocation_pools(self, session):
        """Remove all allocation pools for the current subnet.

        :param session: database session
        """
        session.query(db_models.IpamAllocationPool).filter_by(
            ipam_subnet_id=self._ipam_subnet_id).delete()

    def list_pools(self, session):
        """Return pools for the current subnet."""
        return session.query(
            db_models.IpamAllocationPool).filter_by(
            ipam_subnet_id=self._ipam_subnet_id)

    def _range_query(self, session):
        return session.query(
            db_models.IpamAvailabilityRange).join(
            db_models.IpamAllocationPool).filter_by(
            ipam_subnet_id=self._ipam_subnet_id)

    def get_first_range(self, session):
        """Return the first availability range for the subnet

        :param session: database session
        :return: first available range as instance of
            neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange
        """
        return self._range_query(session).first()

    def list_ranges_by_subnet_id(self, session):
        """Return availability ranges for a given ipam subnet

        :param session: database session
        :return: list of availability ranges as instances of
            neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange
        """
        return self._range_query(session)

    def list_ranges_by_allocation_pool(self, session, allocation_pool_id):
        """Return availability ranges for a given pool.

        :param session: database session
        :param allocation_pool_id: allocation pool identifier
        :return: list of availability ranges as instances of
            neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange
        """
        return session.query(
            db_models.IpamAvailabilityRange).join(
            db_models.IpamAllocationPool).filter_by(
            id=allocation_pool_id)

    def update_range(self, session, db_range, first_ip=None, last_ip=None):
        """Updates db_range to have new first_ip and last_ip.

        :param session: database session
        :param db_range: IpamAvailabilityRange db object
        :param first_ip: first ip address in range
        :param last_ip: last ip address in range
        :return: count of updated rows
        """
        opts = {}
        if first_ip:
            opts['first_ip'] = str(first_ip)
        if last_ip:
            opts['last_ip'] = str(last_ip)
        if not opts:
            raise ipam_exc.IpamAvailabilityRangeNoChanges()
        try:
            return session.query(
                db_models.IpamAvailabilityRange).filter_by(
                allocation_pool_id=db_range.allocation_pool_id).filter_by(
                first_ip=db_range.first_ip).filter_by(
                last_ip=db_range.last_ip).update(opts)
        except orm_exc.ObjectDeletedError:
            raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed)

    def delete_range(self, session, db_range):
        """Return count of deleted ranges

        :param session: database session
        :param db_range: IpamAvailabilityRange db object
        """
        try:
            return session.query(
                db_models.IpamAvailabilityRange).filter_by(
                allocation_pool_id=db_range.allocation_pool_id).filter_by(
                first_ip=db_range.first_ip).filter_by(
                last_ip=db_range.last_ip).delete()
        except orm_exc.ObjectDeletedError:
            raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed)

    def create_range(self, session, allocation_pool_id,
                     range_start, range_end):
        """Create an availability range for a given pool.

        This method does not perform any validation on parameters; it simply
        persist data on the database.

        :param session: database session
        :param allocation_pool_id: allocation pool identifier
        :param range_start: first ip address in the range
        :param range_end: last ip address in the range
        :return: the newly created availability range as an instance of
            neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange
        """
        new_ip_range = db_models.IpamAvailabilityRange(
            allocation_pool_id=allocation_pool_id,
            first_ip=range_start,
            last_ip=range_end)
        session.add(new_ip_range)
        return new_ip_range

    def check_unique_allocation(self, session, ip_address):
        """Validate that the IP address on the subnet is not in use."""
        iprequest = session.query(db_models.IpamAllocation).filter_by(
            ipam_subnet_id=self._ipam_subnet_id, status='ALLOCATED',
            ip_address=ip_address).first()
        if iprequest:
            return False
        return True

    def list_allocations(self, session, status='ALLOCATED'):
        """Return current allocations for the subnet.

        :param session: database session
        :param status: IP allocation status
        :returns: a list of IP allocation as instance of
            neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAllocation
        """
        return session.query(
            db_models.IpamAllocation).filter_by(
            ipam_subnet_id=self._ipam_subnet_id,
            status=status)

    def create_allocation(self, session, ip_address,
                          status='ALLOCATED'):
        """Create an IP allocation entry.

        :param session: database session
        :param ip_address: the IP address to allocate
        :param status: IP allocation status
        """
        ip_request = db_models.IpamAllocation(
            ip_address=ip_address,
            status=status,
            ipam_subnet_id=self._ipam_subnet_id)
        session.add(ip_request)

    def delete_allocation(self, session, ip_address):
        """Remove an IP allocation for this subnet.

        :param session: database session
        :param ip_address: IP address for which the allocation entry should
            be removed.
        """
        return session.query(db_models.IpamAllocation).filter_by(
            ip_address=ip_address,
            ipam_subnet_id=self._ipam_subnet_id).delete(
                synchronize_session=False)
