# Copyright 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 importlib
import itertools
import os

from neutron_lib.db import constants as db_const
from neutron_lib.db import utils as db_utils
from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from oslo_log import log as logging
from oslo_log import versionutils
import stevedore

from neutron._i18n import _
from neutron.conf.services import provider_configuration as prov_config

LOG = logging.getLogger(__name__)

SERVICE_PROVIDERS = 'neutron.service_providers'

# TODO(HenryG): use MovedGlobals to deprecate this.
serviceprovider_opts = prov_config.serviceprovider_opts

prov_config.register_service_provider_opts()


class NeutronModule(object):
    """A Neutron extension module."""

    def __init__(self, service_module):
        self.module_name = service_module
        self.repo = {
            'mod': self._import_or_none(),
            'ini': None
        }

    def _import_or_none(self):
        try:
            return importlib.import_module(self.module_name)
        except ImportError:
            return None

    def installed(self):
        LOG.debug("NeutronModule installed = %s", self.module_name)
        return self.module_name

    def module(self):
        return self.repo['mod']

    # Return an INI parser for the child module
    def ini(self, neutron_dir=None):
        if self.repo['ini'] is None:
            ini_file = cfg.ConfigOpts()
            prov_config.register_service_provider_opts(ini_file)

            if neutron_dir is not None:
                neutron_dirs = [neutron_dir]
            else:
                try:
                    neutron_dirs = cfg.CONF.config_dirs
                except cfg.NoSuchOptError:
                    neutron_dirs = None
                if not neutron_dirs:
                    neutron_dirs = ['/etc/neutron']

            # load configuration from all matching files to reflect oslo.config
            # behaviour
            config_files = []
            for neutron_dir in neutron_dirs:
                ini_path = os.path.join(neutron_dir,
                                        '%s.conf' % self.module_name)
                if os.path.exists(ini_path):
                    config_files.append(ini_path)

            # NOTE(ihrachys): we could pass project=self.module_name instead to
            # rely on oslo.config to find configuration files for us, but:
            # 1. that would render neutron_dir argument ineffective;
            # 2. that would break loading configuration file from under
            # /etc/neutron in case no --config-dir is passed.
            # That's why we need to explicitly construct CLI here.
            ini_file(args=list(itertools.chain.from_iterable(
                ['--config-file', file_] for file_ in config_files
            )))
            self.repo['ini'] = ini_file

        return self.repo['ini']

    def service_providers(self):
        """Return the service providers for the extension module."""
        providers = []
        # Attempt to read the config from cfg.CONF first; when passing
        # --config-dir, the option is merged from all the definitions
        # made across all the imported config files
        try:
            providers = cfg.CONF.service_providers.service_provider
        except cfg.NoSuchOptError:
            pass

        # Alternatively, if the option is not available, try to load
        # it from the provider module's config file; this may be
        # necessary, if modules are loaded on the fly (DevStack may
        # be an example)
        if not providers:
            providers = self.ini().service_providers.service_provider

            if providers:
                versionutils.report_deprecated_feature(
                    LOG,
                    'Implicit loading of service providers from '
                    'neutron_*.conf files is deprecated and will be '
                    'removed in Ocata release.')

        return providers


# global scope function that should be used in service APIs
def normalize_provider_name(name):
    return name.lower()


def get_provider_driver_class(driver, namespace=SERVICE_PROVIDERS):
    """Return path to provider driver class

    In order to keep backward compatibility with configs < Kilo, we need to
    translate driver class paths after advanced services split. This is done by
    defining old class path as entry point in neutron package.
    """
    try:
        driver_manager = stevedore.driver.DriverManager(
            namespace, driver).driver
    except ImportError:
        return driver
    except RuntimeError:
        return driver
    new_driver = "%s.%s" % (driver_manager.__module__,
                            driver_manager.__name__)
    LOG.warning(
        "The configured driver %(driver)s has been moved, automatically "
        "using %(new_driver)s instead. Please update your config files, "
        "as this automatic fixup will be removed in a future release.",
        {'driver': driver, 'new_driver': new_driver})
    return new_driver


def parse_service_provider_opt(service_module='neutron', service_type=None):

    """Parse service definition opts and returns result."""
    def validate_name(name):
        if len(name) > db_const.NAME_FIELD_SIZE:
            raise n_exc.Invalid(
                _("Provider name %(name)s is limited by %(len)s characters")
                % {'name': name, 'len': db_const.NAME_FIELD_SIZE})

    neutron_mod = NeutronModule(service_module)
    svc_providers_opt = neutron_mod.service_providers()

    LOG.debug("Service providers = %s", svc_providers_opt)

    res = []
    for prov_def in svc_providers_opt:
        split = prov_def.split(':')
        try:
            svc_type, name, driver = split[:3]
            if service_type and service_type != svc_type:
                continue
        except ValueError:
            raise n_exc.Invalid(_("Invalid service provider format"))
        validate_name(name)
        name = normalize_provider_name(name)
        default = False
        if len(split) == 4 and split[3]:
            if split[3] == 'default':
                default = True
            else:
                msg = (_("Invalid provider format. "
                         "Last part should be 'default' or empty: %s") %
                       prov_def)
                LOG.error(msg)
                raise n_exc.Invalid(msg)

        driver = get_provider_driver_class(driver)
        res.append({'service_type': svc_type,
                    'name': name,
                    'driver': driver,
                    'default': default})
    return res


class ServiceProviderNotFound(n_exc.InvalidInput):
    message = _("Service provider '%(provider)s' could not be found "
                "for service type %(service_type)s")


class DefaultServiceProviderNotFound(n_exc.InvalidInput):
    message = _("Service type %(service_type)s does not have a default "
                "service provider")


class ServiceProviderAlreadyAssociated(n_exc.Conflict):
    message = _("Resource '%(resource_id)s' is already associated with "
                "provider '%(provider)s' for service type '%(service_type)s'")


class ProviderConfiguration(object):

    def __init__(self, svc_module='neutron', svc_type=None):
        self.providers = {}
        for prov in parse_service_provider_opt(svc_module, svc_type):
            self.add_provider(prov)

    def _ensure_driver_unique(self, driver):
        for v in self.providers.values():
            if v['driver'] == driver:
                msg = (_("Driver %s is not unique across providers") %
                       driver)
                LOG.error(msg)
                raise n_exc.Invalid(msg)

    def _ensure_default_unique(self, type, default):
        if not default:
            return
        for k, v in self.providers.items():
            if k[0] == type and v['default']:
                msg = _("Multiple default providers "
                        "for service %s") % type
                LOG.error(msg)
                raise n_exc.Invalid(msg)

    def add_provider(self, provider):
        self._ensure_driver_unique(provider['driver'])
        self._ensure_default_unique(provider['service_type'],
                                    provider['default'])
        provider_type = (provider['service_type'], provider['name'])
        if provider_type in self.providers:
            msg = (_("Multiple providers specified for service "
                     "%s") % provider['service_type'])
            LOG.error(msg)
            raise n_exc.Invalid(msg)
        self.providers[provider_type] = {'driver': provider['driver'],
                                         'default': provider['default']}

    def _check_entry(self, k, v, filters):
        # small helper to deal with query filters
        if not filters:
            return True
        for index, key in enumerate(['service_type', 'name']):
            if key in filters:
                if k[index] not in filters[key]:
                    return False

        for key in ['driver', 'default']:
            if key in filters:
                if v[key] not in filters[key]:
                    return False
        return True

    def get_service_providers(self, filters=None, fields=None):
        return [db_utils.resource_fields({'service_type': k[0],
                                          'name': k[1],
                                          'driver': v['driver'],
                                          'default': v['default']},
                                         fields)
                for k, v in self.providers.items()
                if self._check_entry(k, v, filters)]
