# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2013 OpenStack Foundation.
# 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 sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.orm import exc
from sqlalchemy.orm import joinedload

from quantum.common import constants
from quantum.db import agents_db
from quantum.db import model_base
from quantum.db import models_v2
from quantum.extensions import agentscheduler
from quantum.openstack.common import log as logging


LOG = logging.getLogger(__name__)


class NetworkDhcpAgentBinding(model_base.BASEV2):
    """Represents binding between quantum networks and DHCP agents."""

    network_id = sa.Column(sa.String(36),
                           sa.ForeignKey("networks.id", ondelete='CASCADE'),
                           primary_key=True)
    dhcp_agent = orm.relation(agents_db.Agent)
    dhcp_agent_id = sa.Column(sa.String(36),
                              sa.ForeignKey("agents.id",
                                            ondelete='CASCADE'),
                              primary_key=True)


class RouterL3AgentBinding(model_base.BASEV2, models_v2.HasId):
    """Represents binding between quantum routers and L3 agents."""

    router_id = sa.Column(sa.String(36),
                          sa.ForeignKey("routers.id", ondelete='CASCADE'))
    l3_agent = orm.relation(agents_db.Agent)
    l3_agent_id = sa.Column(sa.String(36),
                            sa.ForeignKey("agents.id",
                                          ondelete='CASCADE'))


class AgentSchedulerDbMixin(agentscheduler.AgentSchedulerPluginBase,
                            agents_db.AgentDbMixin):
    """Mixin class to add agent scheduler extension to db_plugin_base_v2."""

    dhcp_agent_notifier = None
    l3_agent_notifier = None
    network_scheduler = None
    router_scheduler = None

    @staticmethod
    def is_eligible_agent(active, agent):
        if active is None:
            # filtering by activeness is disabled, all agents are eligible
            return True
        else:
            # note(rpodolyaka): original behaviour is saved here: if active
            #                   filter is set, only agents which are 'up'
            #                   (i.e. have a recent heartbeat timestamp)
            #                   are eligible, even if active is False
            return not agents_db.AgentDbMixin.is_agent_down(
                agent['heartbeat_timestamp'])

    def get_dhcp_agents_hosting_networks(
        self, context, network_ids, active=None):
        if not network_ids:
            return []
        query = context.session.query(NetworkDhcpAgentBinding)
        query = query.options(joinedload('dhcp_agent'))
        if len(network_ids) == 1:
            query = query.filter(
                NetworkDhcpAgentBinding.network_id == network_ids[0])
        elif network_ids:
            query = query.filter(
                NetworkDhcpAgentBinding.network_id in network_ids)
        if active is not None:
            query = (query.filter(agents_db.Agent.admin_state_up == active))

        return [binding.dhcp_agent
                for binding in query
                if AgentSchedulerDbMixin.is_eligible_agent(active,
                                                           binding.dhcp_agent)]

    def add_network_to_dhcp_agent(self, context, id, network_id):
        self._get_network(context, network_id)
        with context.session.begin(subtransactions=True):
            agent_db = self._get_agent(context, id)
            if (agent_db['agent_type'] != constants.AGENT_TYPE_DHCP or
                not agent_db['admin_state_up']):
                raise agentscheduler.InvalidDHCPAgent(id=id)
            dhcp_agents = self.get_dhcp_agents_hosting_networks(
                context, [network_id])
            for dhcp_agent in dhcp_agents:
                if id == dhcp_agent.id:
                    raise agentscheduler.NetworkHostedByDHCPAgent(
                        network_id=network_id, agent_id=id)
            binding = NetworkDhcpAgentBinding()
            binding.dhcp_agent_id = id
            binding.network_id = network_id
            context.session.add(binding)
        if self.dhcp_agent_notifier:
            self.dhcp_agent_notifier.network_added_to_agent(
                context, network_id, agent_db.host)

    def remove_network_from_dhcp_agent(self, context, id, network_id):
        agent = self._get_agent(context, id)
        with context.session.begin(subtransactions=True):
            try:
                query = context.session.query(NetworkDhcpAgentBinding)
                binding = query.filter(
                    NetworkDhcpAgentBinding.network_id == network_id,
                    NetworkDhcpAgentBinding.dhcp_agent_id == id).one()
            except exc.NoResultFound:
                raise agentscheduler.NetworkNotHostedByDhcpAgent(
                    network_id=network_id, agent_id=id)
            context.session.delete(binding)
        if self.dhcp_agent_notifier:
            self.dhcp_agent_notifier.network_removed_from_agent(
                context, network_id, agent.host)

    def list_networks_on_dhcp_agent(self, context, id):
        query = context.session.query(NetworkDhcpAgentBinding.network_id)
        query = query.filter(NetworkDhcpAgentBinding.dhcp_agent_id == id)

        net_ids = [item[0] for item in query]
        if net_ids:
            return {'networks':
                    self.get_networks(context, filters={'id': net_ids})}
        else:
            return {'networks': []}

    def list_active_networks_on_active_dhcp_agent(self, context, host):
        agent = self._get_agent_by_type_and_host(
            context, constants.AGENT_TYPE_DHCP, host)
        if not agent.admin_state_up:
            return []
        query = context.session.query(NetworkDhcpAgentBinding.network_id)
        query = query.filter(NetworkDhcpAgentBinding.dhcp_agent_id == agent.id)

        net_ids = [item[0] for item in query]
        if net_ids:
            return self.get_networks(
                context,
                filters={'id': net_ids, 'admin_state_up': [True]}
            )
        else:
            return []

    def list_dhcp_agents_hosting_network(self, context, network_id):
        dhcp_agents = self.get_dhcp_agents_hosting_networks(
            context, [network_id])
        agent_ids = [dhcp_agent.id for dhcp_agent in dhcp_agents]
        if agent_ids:
            return {
                'agents':
                self.get_agents(context, filters={'id': agent_ids})}
        else:
            return {'agents': []}

    def add_router_to_l3_agent(self, context, id, router_id):
        """Add a l3 agent to host a router."""
        router = self.get_router(context, router_id)
        with context.session.begin(subtransactions=True):
            agent_db = self._get_agent(context, id)
            if (agent_db['agent_type'] != constants.AGENT_TYPE_L3 or
                not agent_db['admin_state_up'] or
                not self.get_l3_agent_candidates(router, [agent_db])):
                raise agentscheduler.InvalidL3Agent(id=id)
            query = context.session.query(RouterL3AgentBinding)
            try:
                binding = query.filter(
                    RouterL3AgentBinding.l3_agent_id == agent_db.id,
                    RouterL3AgentBinding.router_id == router_id).one()
                if binding:
                    raise agentscheduler.RouterHostedByL3Agent(
                        router_id=router_id, agent_id=id)
            except exc.NoResultFound:
                pass

            result = self.auto_schedule_routers(context,
                                                agent_db.host,
                                                router_id)
            if not result:
                raise agentscheduler.RouterSchedulingFailed(
                    router_id=router_id, agent_id=id)

        if self.l3_agent_notifier:
            routers = self.get_sync_data(context, [router_id])
            self.l3_agent_notifier.router_added_to_agent(
                context, routers, agent_db.host)

    def remove_router_from_l3_agent(self, context, id, router_id):
        """Remove the router from l3 agent.

        After it, the router will be non-hosted until there is update which
        lead to re schedule or be added to another agent manually.
        """
        agent = self._get_agent(context, id)
        with context.session.begin(subtransactions=True):
            query = context.session.query(RouterL3AgentBinding)
            query = query.filter(
                RouterL3AgentBinding.router_id == router_id,
                RouterL3AgentBinding.l3_agent_id == id)
            try:
                binding = query.one()
            except exc.NoResultFound:
                raise agentscheduler.RouterNotHostedByL3Agent(
                    router_id=router_id, agent_id=id)
            context.session.delete(binding)
        if self.l3_agent_notifier:
            self.l3_agent_notifier.router_removed_from_agent(
                context, router_id, agent.host)

    def list_routers_on_l3_agent(self, context, id):
        query = context.session.query(RouterL3AgentBinding.router_id)
        query = query.filter(RouterL3AgentBinding.l3_agent_id == id)

        router_ids = [item[0] for item in query]
        if router_ids:
            return {'routers':
                    self.get_routers(context, filters={'id': router_ids})}
        else:
            return {'routers': []}

    def list_active_sync_routers_on_active_l3_agent(
            self, context, host, router_id):
        agent = self._get_agent_by_type_and_host(
            context, constants.AGENT_TYPE_L3, host)
        if not agent.admin_state_up:
            return []
        query = context.session.query(RouterL3AgentBinding.router_id)
        query = query.filter(
            RouterL3AgentBinding.l3_agent_id == agent.id)
        if router_id:
            query = query.filter(RouterL3AgentBinding.router_id == router_id)

        router_ids = [item[0] for item in query]
        if router_ids:
            return self.get_sync_data(context, router_ids=router_ids,
                                      active=True)
        else:
            return []

    def get_l3_agents_hosting_routers(self, context, router_ids,
                                      admin_state_up=None,
                                      active=None):
        if not router_ids:
            return []
        query = context.session.query(RouterL3AgentBinding)
        if len(router_ids) > 1:
            query = query.options(joinedload('l3_agent')).filter(
                RouterL3AgentBinding.router_id.in_(router_ids))
        else:
            query = query.options(joinedload('l3_agent')).filter(
                RouterL3AgentBinding.router_id == router_ids[0])
        if admin_state_up is not None:
            query = (query.filter(agents_db.Agent.admin_state_up ==
                                  admin_state_up))
        l3_agents = [binding.l3_agent for binding in query]
        if active is not None:
            l3_agents = [l3_agent for l3_agent in
                         l3_agents if not
                         agents_db.AgentDbMixin.is_agent_down(
                         l3_agent['heartbeat_timestamp'])]
        return l3_agents

    def _get_l3_bindings_hosting_routers(self, context, router_ids):
        if not router_ids:
            return []
        query = context.session.query(RouterL3AgentBinding)
        if len(router_ids) > 1:
            query = query.options(joinedload('l3_agent')).filter(
                RouterL3AgentBinding.router_id.in_(router_ids))
        else:
            query = query.options(joinedload('l3_agent')).filter(
                RouterL3AgentBinding.router_id == router_ids[0])
        return query.all()

    def list_l3_agents_hosting_router(self, context, router_id):
        with context.session.begin(subtransactions=True):
            bindings = self._get_l3_bindings_hosting_routers(
                context, [router_id])
            results = []
            for binding in bindings:
                l3_agent_dict = self._make_agent_dict(binding.l3_agent)
                results.append(l3_agent_dict)
            if results:
                return {'agents': results}
            else:
                return {'agents': []}

    def schedule_network(self, context, created_network):
        if self.network_scheduler:
            chosen_agent = self.network_scheduler.schedule(
                self, context, created_network)
            if not chosen_agent:
                LOG.warn(_('Fail scheduling network %s'), created_network)
            return chosen_agent

    def auto_schedule_networks(self, context, host):
        if self.network_scheduler:
            self.network_scheduler.auto_schedule_networks(self, context, host)

    def get_l3_agents(self, context, active=None, filters=None):
        query = context.session.query(agents_db.Agent)
        query = query.filter(
            agents_db.Agent.agent_type == constants.AGENT_TYPE_L3)
        if active is not None:
            query = (query.filter(agents_db.Agent.admin_state_up == active))
        if filters:
            for key, value in filters.iteritems():
                column = getattr(agents_db.Agent, key, None)
                if column:
                    query = query.filter(column.in_(value))

        return [l3_agent
                for l3_agent in query
                if AgentSchedulerDbMixin.is_eligible_agent(active, l3_agent)]

    def get_l3_agent_candidates(self, sync_router, l3_agents):
        """Get the valid l3 agents for the router from a list of l3_agents."""
        candidates = []
        for l3_agent in l3_agents:
            if not l3_agent.admin_state_up:
                continue
            agent_conf = self.get_configuration_dict(l3_agent)
            router_id = agent_conf.get('router_id', None)
            use_namespaces = agent_conf.get('use_namespaces', True)
            handle_internal_only_routers = agent_conf.get(
                'handle_internal_only_routers', True)
            gateway_external_network_id = agent_conf.get(
                'gateway_external_network_id', None)
            if not use_namespaces and router_id != sync_router['id']:
                continue
            ex_net_id = (sync_router['external_gateway_info'] or {}).get(
                'network_id')
            if ((not ex_net_id and not handle_internal_only_routers) or
                (ex_net_id and gateway_external_network_id and
                 ex_net_id != gateway_external_network_id)):
                continue
            candidates.append(l3_agent)
        return candidates

    def auto_schedule_routers(self, context, host, router_id):
        if self.router_scheduler:
            return self.router_scheduler.auto_schedule_routers(
                self, context, host, router_id)

    def schedule_router(self, context, router):
        if self.router_scheduler:
            return self.router_scheduler.schedule(
                self, context, router)

    def schedule_routers(self, context, routers):
        """Schedule the routers to l3 agents."""
        for router in routers:
            self.schedule_router(context, router)

    def update_agent(self, context, id, agent):
        original_agent = self.get_agent(context, id)
        result = super(AgentSchedulerDbMixin, self).update_agent(
            context, id, agent)
        agent_data = agent['agent']
        if ('admin_state_up' in agent_data and
            original_agent['admin_state_up'] != agent_data['admin_state_up']):
            if (original_agent['agent_type'] == constants.AGENT_TYPE_DHCP and
                self.dhcp_agent_notifier):
                self.dhcp_agent_notifier.agent_updated(
                    context, agent_data['admin_state_up'],
                    original_agent['host'])
            elif (original_agent['agent_type'] == constants.AGENT_TYPE_L3 and
                  self.l3_agent_notifier):
                self.l3_agent_notifier.agent_updated(
                    context, agent_data['admin_state_up'],
                    original_agent['host'])
        return result
