# Copyright 2013 OpenStack Foundation
#
# 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 sys

from keystoneclient.common import cms
from oslo_config import cfg
from oslo_log import log
from oslo_log import versionutils
from oslo_serialization import jsonutils
from oslo_utils import importutils
import six
import stevedore

from keystone.common import controller
from keystone.common import dependency
from keystone.common import utils
from keystone.common import wsgi
from keystone import config
from keystone.contrib.federation import constants as federation_constants
from keystone import exception
from keystone.i18n import _, _LI, _LW
from keystone.resource import controllers as resource_controllers


LOG = log.getLogger(__name__)

CONF = cfg.CONF

# registry of authentication methods
AUTH_METHODS = {}
AUTH_PLUGINS_LOADED = False


def load_auth_method(method):
    plugin_name = CONF.auth.get(method) or 'default'
    try:
        namespace = 'keystone.auth.%s' % method
        driver_manager = stevedore.DriverManager(namespace, plugin_name,
                                                 invoke_on_load=True)
        return driver_manager.driver
    except RuntimeError:
        LOG.debug('Failed to load the %s driver (%s) using stevedore, will '
                  'attempt to load using import_object instead.',
                  method, plugin_name)

    @versionutils.deprecated(as_of=versionutils.deprecated.LIBERTY,
                             in_favor_of='entrypoints',
                             what='direct import of driver')
    def _load_using_import(plugin_name):
        return importutils.import_object(plugin_name)

    return _load_using_import(plugin_name)


def load_auth_methods():
    global AUTH_PLUGINS_LOADED

    if AUTH_PLUGINS_LOADED:
        # Only try and load methods a single time.
        return
    # config.setup_authentication should be idempotent, call it to ensure we
    # have setup all the appropriate configuration options we may need.
    config.setup_authentication()
    for plugin in set(CONF.auth.methods):
        AUTH_METHODS[plugin] = load_auth_method(plugin)
    AUTH_PLUGINS_LOADED = True


def get_auth_method(method_name):
    global AUTH_METHODS
    if method_name not in AUTH_METHODS:
        raise exception.AuthMethodNotSupported()
    return AUTH_METHODS[method_name]


class AuthContext(dict):
    """Retrofitting auth_context to reconcile identity attributes.

    The identity attributes must not have conflicting values among the
    auth plug-ins. The only exception is `expires_at`, which is set to its
    earliest value.

    """

    # identity attributes need to be reconciled among the auth plugins
    IDENTITY_ATTRIBUTES = frozenset(['user_id', 'project_id',
                                     'access_token_id', 'domain_id',
                                     'expires_at'])

    def __setitem__(self, key, val):
        if key in self.IDENTITY_ATTRIBUTES and key in self:
            existing_val = self[key]
            if key == 'expires_at':
                # special treatment for 'expires_at', we are going to take
                # the earliest expiration instead.
                if existing_val != val:
                    LOG.info(_LI('"expires_at" has conflicting values '
                                 '%(existing)s and %(new)s.  Will use the '
                                 'earliest value.'),
                             {'existing': existing_val, 'new': val})
                if existing_val is None or val is None:
                    val = existing_val or val
                else:
                    val = min(existing_val, val)
            elif existing_val != val:
                msg = _('Unable to reconcile identity attribute %(attribute)s '
                        'as it has conflicting values %(new)s and %(old)s') % (
                            {'attribute': key,
                             'new': val,
                             'old': existing_val})
                raise exception.Unauthorized(msg)
        return super(AuthContext, self).__setitem__(key, val)


@dependency.requires('resource_api', 'trust_api')
class AuthInfo(object):
    """Encapsulation of "auth" request."""

    @staticmethod
    def create(context, auth=None):
        auth_info = AuthInfo(context, auth=auth)
        auth_info._validate_and_normalize_auth_data()
        return auth_info

    def __init__(self, context, auth=None):
        self.context = context
        self.auth = auth
        self._scope_data = (None, None, None, None)
        # self._scope_data is (domain_id, project_id, trust_ref, unscoped)
        # project scope: (None, project_id, None, None)
        # domain scope: (domain_id, None, None, None)
        # trust scope: (None, None, trust_ref, None)
        # unscoped: (None, None, None, 'unscoped')

    def _assert_project_is_enabled(self, project_ref):
        # ensure the project is enabled
        try:
            self.resource_api.assert_project_enabled(
                project_id=project_ref['id'],
                project=project_ref)
        except AssertionError as e:
            LOG.warning(six.text_type(e))
            six.reraise(exception.Unauthorized, exception.Unauthorized(e),
                        sys.exc_info()[2])

    def _assert_domain_is_enabled(self, domain_ref):
        try:
            self.resource_api.assert_domain_enabled(
                domain_id=domain_ref['id'],
                domain=domain_ref)
        except AssertionError as e:
            LOG.warning(six.text_type(e))
            six.reraise(exception.Unauthorized, exception.Unauthorized(e),
                        sys.exc_info()[2])

    def _lookup_domain(self, domain_info):
        domain_id = domain_info.get('id')
        domain_name = domain_info.get('name')
        domain_ref = None
        if not domain_id and not domain_name:
            raise exception.ValidationError(attribute='id or name',
                                            target='domain')
        try:
            if domain_name:
                domain_ref = self.resource_api.get_domain_by_name(
                    domain_name)