# Copyright (c) 2016 IBM
# 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 neutron_lib.api.definitions import dns as dns_apidef
from neutron_lib.api.definitions import l3 as l3_apidef
from neutron_lib.api import extensions
from neutron_lib.api import validators
from neutron_lib import exceptions as n_exc
from neutron_lib.exceptions import dns as dns_exc
from oslo_config import cfg
from oslo_log import log as logging

from neutron._i18n import _
from neutron.db import _resource_extend as resource_extend
from neutron.objects import floatingip as fip_obj
from neutron.objects import network
from neutron.objects import ports as port_obj
from neutron.services.externaldns import driver

LOG = logging.getLogger(__name__)


class DNSActionsData(object):

    def __init__(self, current_dns_name=None, current_dns_domain=None,
                 previous_dns_name=None, previous_dns_domain=None):
        self.current_dns_name = current_dns_name
        self.current_dns_domain = current_dns_domain
        self.previous_dns_name = previous_dns_name
        self.previous_dns_domain = previous_dns_domain


@resource_extend.has_resource_extenders
class DNSDbMixin(object):
    """Mixin class to add DNS methods to db_base_plugin_v2."""

    _dns_driver = None

    @property
    def dns_driver(self):
        if self._dns_driver:
            return self._dns_driver
        if not cfg.CONF.external_dns_driver:
            return
        try:
            self._dns_driver = driver.ExternalDNSService.get_instance()
            LOG.debug("External DNS driver loaded: %s",
                      cfg.CONF.external_dns_driver)
            return self._dns_driver
        except ImportError:
            LOG.exception("ImportError exception occurred while loading "
                          "the external DNS service driver")
            raise dns_exc.ExternalDNSDriverNotFound(
                driver=cfg.CONF.external_dns_driver)

    @staticmethod
    @resource_extend.extends([l3_apidef.FLOATINGIPS])
    def _extend_floatingip_dict_dns(floatingip_res, floatingip_db):
        floatingip_res['dns_domain'] = ''
        floatingip_res['dns_name'] = ''
        if floatingip_db.dns:
            floatingip_res['dns_domain'] = floatingip_db.dns['dns_domain']
            floatingip_res['dns_name'] = floatingip_db.dns['dns_name']
        return floatingip_res

    def _process_dns_floatingip_create_precommit(self, context,
                                                 floatingip_data, req_data):
        # expects to be called within a plugin's session
        dns_domain = req_data.get(dns_apidef.DNSDOMAIN)
        if not validators.is_attr_set(dns_domain):
            return
        if not self.dns_driver:
            return

        dns_name = req_data[dns_apidef.DNSNAME]
        self._validate_floatingip_dns(dns_name, dns_domain)

        current_dns_name, current_dns_domain = (
            self._get_requested_state_for_external_dns_service_create(
                context, floatingip_data, req_data))
        dns_actions_data = None
        if current_dns_name and current_dns_domain:
            fip_obj.FloatingIPDNS(context,
                floatingip_id=floatingip_data['id'],
                dns_name=req_data[dns_apidef.DNSNAME],
                dns_domain=req_data[dns_apidef.DNSDOMAIN],
                published_dns_name=current_dns_name,
                published_dns_domain=current_dns_domain).create()
            dns_actions_data = DNSActionsData(
                current_dns_name=current_dns_name,
                current_dns_domain=current_dns_domain)
        floatingip_data['dns_name'] = dns_name
        floatingip_data['dns_domain'] = dns_domain
        return dns_actions_data

    def _process_dns_floatingip_create_postcommit(self, context,
                                                  floatingip_data,
                                                  dns_actions_data):
        if not dns_actions_data:
            return
        self._add_ips_to_external_dns_service(
            context, dns_actions_data.current_dns_domain,
            dns_actions_data.current_dns_name,
            [floatingip_data['floating_ip_address']])

    def _process_dns_floatingip_update_precommit(self, context,
                                                 floatingip_data):
        # expects to be called within a plugin's session
        if not extensions.is_extension_supported(
                self._core_plugin, dns_apidef.ALIAS):
            return
        if not self.dns_driver:
            return
        dns_data_db = fip_obj.FloatingIPDNS.get_object(
            context, floatingip_id=floatingip_data['id'])
        if dns_data_db and dns_data_db['dns_name']:
            # dns_name and dns_domain assigned for floating ip. It doesn't
            # matter whether they are defined for internal port
            return
        current_dns_name, current_dns_domain = (
            self._get_requested_state_for_external_dns_service_update(
                context, floatingip_data))
        if dns_data_db:
            if (dns_data_db['published_dns_name'] != current_dns_name or
                    dns_data_db['published_dns_domain'] != current_dns_domain):
                dns_actions_data = DNSActionsData(
                    previous_dns_name=dns_data_db['published_dns_name'],
                    previous_dns_domain=dns_data_db['published_dns_domain'])
                if current_dns_name and current_dns_domain:
                    dns_data_db['published_dns_name'] = current_dns_name
                    dns_data_db['published_dns_domain'] = current_dns_domain
                    dns_actions_data.current_dns_name = current_dns_name
                    dns_actions_data.current_dns_domain = current_dns_domain
                else:
                    dns_data_db.delete()
                return dns_actions_data
            else:
                return
        if current_dns_name and current_dns_domain:
            fip_obj.FloatingIPDNS(context,
                floatingip_id=floatingip_data['id'],
                dns_name='',
                dns_domain='',
                published_dns_name=current_dns_name,
                published_dns_domain=current_dns_domain).create()
            return DNSActionsData(current_dns_name=current_dns_name,
                                  current_dns_domain=current_dns_domain)

    def _process_dns_floatingip_update_postcommit(self, context,
                                                  floatingip_data,
                                                  dns_actions_data):
        if not dns_actions_data:
            return
        if dns_actions_data.previous_dns_name:
            self._delete_floatingip_from_external_dns_service(
                context, dns_actions_data.previous_dns_domain,
                dns_actions_data.previous_dns_name,
                [floatingip_data['floating_ip_address']])
        if dns_actions_data.current_dns_name:
            self._add_ips_to_external_dns_service(
                context, dns_actions_data.current_dns_domain,
                dns_actions_data.current_dns_name,
                [floatingip_data['floating_ip_address']])

    def _process_dns_floatingip_delete(self, context, floatingip_data):
        if not extensions.is_extension_supported(
                self._core_plugin, dns_apidef.ALIAS):
            return
        dns_data_db = fip_obj.FloatingIPDNS.get_object(
            context, floatingip_id=floatingip_data['id'])
        if dns_data_db:
            self._delete_floatingip_from_external_dns_service(
                context, dns_data_db['published_dns_domain'],
                dns_data_db['published_dns_name'],
                [floatingip_data['floating_ip_address']])

    def _validate_floatingip_dns(self, dns_name, dns_domain):
        if dns_domain and not dns_name:
            msg = _("dns_domain cannot be specified without a dns_name")
            raise n_exc.BadRequest(resource='floatingip', msg=msg)
        if dns_name and not dns_domain:
            msg = _("dns_name cannot be specified without a dns_domain")
            raise n_exc.BadRequest(resource='floatingip', msg=msg)

    def _get_internal_port_dns_data(self, context, floatingip_data):
        port_dns = port_obj.PortDNS.get_object(
            context, port_id=floatingip_data['port_id'])
        if not (port_dns and port_dns['dns_name']):
            return None, None
        net_dns = network.NetworkDNSDomain.get_net_dns_from_port(
            context=context, port_id=floatingip_data['port_id'])
        if not net_dns:
            return port_dns['dns_name'], None
        return port_dns['dns_name'], net_dns['dns_domain']

    def _delete_floatingip_from_external_dns_service(self, context, dns_domain,
                                                     dns_name, records):
        ips = [str(r) for r in records]
        try:
            self.dns_driver.delete_record_set(context, dns_domain, dns_name,
                                              ips)
        except (dns_exc.DNSDomainNotFound, dns_exc.DuplicateRecordSet) as e:
            LOG.exception("Error deleting Floating IP data from external "
                          "DNS service. Name: '%(name)s'. Domain: "
                          "'%(domain)s'. IP addresses '%(ips)s'. DNS "
                          "service driver message '%(message)s'",
                          {"name": dns_name,
                           "domain": dns_domain,
                           "message": e.msg,
                           "ips": ', '.join(ips)})

    def _get_requested_state_for_external_dns_service_create(self, context,
                                                             floatingip_data,
                                                             req_data):
        fip_dns_name = req_data[dns_apidef.DNSNAME]
        if fip_dns_name:
            return fip_dns_name, req_data[dns_apidef.DNSDOMAIN]
        if floatingip_data['port_id']:
            return self._get_internal_port_dns_data(context, floatingip_data)
        return None, None

    def _get_requested_state_for_external_dns_service_update(self, context,
                                                             floatingip_data):
        if floatingip_data['port_id']:
            return self._get_internal_port_dns_data(context, floatingip_data)
        return None, None

    def _add_ips_to_external_dns_service(self, context, dns_domain, dns_name,
                                         records):
        ips = [str(r) for r in records]
        try:
            self.dns_driver.create_record_set(context, dns_domain, dns_name,
                                              ips)
        except (dns_exc.DNSDomainNotFound, dns_exc.DuplicateRecordSet) as e:
            LOG.exception("Error publishing floating IP data in external "
                          "DNS service. Name: '%(name)s'. Domain: "
                          "'%(domain)s'. DNS service driver message "
                          "'%(message)s'",
                          {"name": dns_name,
                           "domain": dns_domain,
                           "message": e.msg})
