# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP
# Copyright 2017 Fujitsu LIMITED
# 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 logging

from keystoneauth1 import identity
from keystoneauth1 import session
from keystoneclient import discover

from monasca_agent.common import singleton
from monasca_agent import version as ma_version

LOG = logging.getLogger(__name__)


_DEFAULT_SERVICE_TYPE = 'monitoring'
_DEFAULT_ENDPOINT_TYPE = 'public'


def _sanitize_args(data):
    """Removes keys for which value is None.

    :param data: dictionary with data
    :type data: dict
    :return: cleaned data
    :rtype: dict

    """
    return {k: v for k, v in data.items() if v is not None}


def get_session(**kwargs):
    """Creates new keystone session.

    Method uses :py:class:`keystoneauth1.identity.Password`
    abstracting from underlying Keystone version

    This method is capable of creating a session regardless of
    Keystone version (either v2 or v3). However if:

    - using **Keystone v2** following arguments [domain_id, domain_name,
      project_domain_id and project_domain_name] should not be set. Keystone V2
      does not support authentication with domain scope.
    - using **Keystone v2** following arguments are prohibited:
      [user_domain_id, user_domain_name]
    - using **Keystone v3** be careful with the scope of authentication.
      For more details about scopes refer to identity_tokens_ and v3_identity_

    .. _v3_api: https://docs.openstack.org/api-ref/identity/v3/index.html?expanded=token-authe
    ntication-with-scoped-authorization-detail
    .. _identity_tokens: https://docs.openstack.org/admin-guide/identity-tokens.html

    In overall:

    - for **Keystone V2** following arguments are allowed:
      [auth_url, user_id, username, password, trust_id, tenant_name,
      tenant_id, project_name, project_id].
    * for **Keystone V3** following argumenta are allowed:
      [auth_url, user_id, username, password, user_domain_id, user_domain_name,
      trust_id, project_id, project_name, project_domain_id,
      project_domain_name, domain_id, domain_name, tenant_id, tenant_name]

    However, note that project_id and project_name will override tenant_id
    and tenant_name, as in::

        >>> project_id = project_id or tenant_id
        >>> project_name = project_name or tenant_name

    Arguments tenant_id and tenant_name are kept for sake of
    backward compatibility between two versions of Keystone.

    Note:
        Keystone version is resolved on the runtime
        by keystoneauth1 library

    :param string auth_url: URL of keystone service.
    :param string username: Username for authentication.
    :param string password: Password for authentication.
    :param string user_id: User ID for authentication.
    :param string user_domain_id: User's domain ID for authentication
                            (replaced by default_domain_if if set)
    :param string user_domain_name: User's domain name for authentication
                            (replaced by default_domain_name if set)
    :param string project_id: Project ID for authentication
    :param string project_name: Project Name for authentication
    :param string project_domain_id: Project Domain ID for authentication
    :param string project_domain_name: Project Domain Name for authentication
    :param string tenant_id: Tenant ID for authentication
                            (replaced by project_id if set)
    :param string tenant_name: Tenant Name for authentication
                            (replaced by project_name if set)
    :param string domain_id: Domain ID for authentication.
    :param string domain_name: Domain name for authentication
    :param string trust_id: Trust ID for authentication.
    :param string default_domain_id: Default domain ID for authentication.
    :param string default_domain_name: Default domain name for authentication
    :param float keystone_timeout: A timeout to pass to requests. This should be a
                      numerical value indicating some amount (or fraction)
                      of seconds or 0 for no timeout. (optional, defaults
                      to 0)
    :param verify: The verification arguments to pass to requests. These are of
                   the same form as requests expects, so True or False to
                   verify (or not) against system certificates or a path to a
                   bundle or CA certs to check against or None for requests to
                   attempt to locate and use certificates. (optional, defaults
                   to True)
    :param bool reauthenticate: Should reauthenticate if token expires
                        (optional, defaults to True)
    :return: session instance
    :rtype: keystoneauth1.session.Session

    """

    LOG.debug('Initializing keystone session using generic password')

    auth = identity.Password(
        auth_url=kwargs.get('auth_url', None),
        username=kwargs.get('username', None),
        password=kwargs.get('password', None),
        user_id=kwargs.get('user_id', None),
        user_domain_id=kwargs.get('user_domain_id', None),
        user_domain_name=kwargs.get('user_domain_name', None),
        project_id=kwargs.get('project_id', None),
        project_name=kwargs.get('project_name', None),
        project_domain_id=kwargs.get('project_domain_id', None),
        project_domain_name=kwargs.get('project_domain_name', None),
        tenant_id=kwargs.get('tenant_id', None),
        tenant_name=kwargs.get('tenant_name', None),
        domain_id=kwargs.get('domain_id', None),
        domain_name=kwargs.get('domain_name', None),
        trust_id=kwargs.get('trust_id', None),
        default_domain_id=kwargs.get('default_domain_id', None),
        default_domain_name=kwargs.get('default_domain_name', None),
        reauthenticate=kwargs.get('reauthenticate', True)
    )
    sess = session.Session(auth=auth,
                           app_name='monasca-agent',
                           app_version=ma_version.version_string,
                           user_agent='monasca-agent',
                           timeout=kwargs.get('keystone_timeout', None),
                           verify=kwargs.get('verify', True))
    return sess


def get_client(**kwargs):
    """Creates new keystone client.

    Initializes new keystone client.
    Method does not assume what version of keystone is used.
    That responsibility is delegated to
    :py:class:`keystoneauth1.discover.Discover`.
    Version of the keystone will be the newest one available.

    There are two ways to call this method:

    using existing session object (:py:class:`keystoneauth1.session.Session`::

        >>> s = session.Session(**args)
        >>> c = get_client(session=s)

    initializing new keystone client from credentials::

        >>> c = get_client({'username':'mini-mon', 'password':'test', ...})

    :param kwargs: list of arguments passed to method
    :type kwargs: dict
    :return: keystone client instance
    :rtype: Union[keystoneclient.v3.client.Client,
                  keystoneclient.v2_0.client.Client]
    """

    if 'session' not in kwargs:
        LOG.debug('Initializing fresh keystone client')
        sess = get_session(**kwargs)
    else:
        LOG.debug('Initializing keystone client from existing session')
        sess = kwargs.get('session')

    disc = discover.Discover(session=sess)
    LOG.debug('Available keystone versions are %s' % disc.version_data())

    ks = disc.create_client(**kwargs)
    ks.auth_ref = sess.auth.get_auth_ref(session=sess)
    LOG.info('Using keystone version %s', ks.version)

    return ks


def get_args(config):
    """Utility to extract keystone args from agent's config.

    Method retrieves all keystone related settings, from
    agent's configuration, that are actually set.

    :param config: agent's config
    :type config: dict
    :returns: cleaned args
    :rtype: dict

    """
    raw_args = {
        'auth_url': config.get('keystone_url', None),
        'username': config.get('username', None),
        'password': config.get('password', None),
        'user_id': config.get('user_id', None),
        'user_domain_id': config.get('user_domain_id', None),
        'user_domain_name': config.get('user_domain_name', None),
        'project_id': config.get('project_id', None),
        'project_name': config.get('project_name', None),
        'project_domain_name': config.get('project_domain_name', None),
        'project_domain_id': config.get('project_domain_id', None),
        'domain_id': config.get('domain_id', None),
        'domain_name': config.get('domain_name', None),
        'tenant_id': config.get('tenant_id', None),
        'tenant_name': config.get('tenant_name', None),
        'trust_id': config.get('trust_id', None),
        'default_domain_id': config.get('default_domain_id', None),
        'default_domain_name': config.get('default_domain_name', None),
        'url': config.get('url', None),  # hardcoded monasca-api url
        'service_type': config.get('service_type', _DEFAULT_SERVICE_TYPE),
        'endpoint_type': config.get('endpoint_type', _DEFAULT_ENDPOINT_TYPE),
        'region_name': config.get('region_name', None),
        'keystone_timeout': config.get('keystone_timeout', None),
        'verify': False if config.get('insecure') else config.get('ca_file', None),
        'reauthenticate': config.get('reauthenticate', True)
    }
    clean_args = _sanitize_args(raw_args)

    LOG.debug('Removed %d keys that did not present values in configuration',
              len(raw_args) - len(clean_args))

    return clean_args


class Keystone(object, metaclass=singleton.Singleton):

    def __init__(self, config):
        self._config = get_args(config)
        self._keystone_client = None

    def _init_client(self):
        """Get a new keystone client object.

        For more details see:

        - :py:func:`monasca_agent.common.keystone.get_session(**args)`
        - :py:func:`monasca_agent.common.keystone.get_client(**args)`

        Note:
            This method initializes client only once on
            behalf of its own

        :return: keystone client instance
        :rtype: Union[keystoneclient.v3.client.Client,
                      keystoneclient.v2_0.client.Client]

        """

        if self._keystone_client:
            LOG.debug('Keystone client is already initialized')
            return self._keystone_client

        ks = get_client(**self._config)
        self._keystone_client = ks

        return ks

    def get_monasca_url(self):
        """Retrieves monasca endpoint url.

        monasca endpoint url can be retrieved from two locations:

        * agent configuration (value must be present under api.url key)
        * keystone catalog (requires settings api.service_type,
          api.endpoint_type and api.region_name)

        First method tries low-cost approach: checking if url is available
        in configuration file. If not, it moves to querying the keystone
        catalog

        :return: monasca endpoint url
        :rtype: basestring

        """
        if self._config.get('url', None):
            endpoint = self._config.get('url')
            LOG.debug('Using monasca-api url %s from configuration' % endpoint)
        else:
            # NOTE(trebskit) no need to sanitize these values here
            # as we're using already local (clean) copy
            args = {
                'service_type': self._config.get('service_type'),
                'interface': self._config.get('endpoint_type'),
                'region_name': self._config.get('region_name', None)  # that one has no default
            }
            catalog = self._init_client().auth_ref.service_catalog
            endpoint = catalog.url_for(**args)
            LOG.debug('Using monasca-api url %s from catalog[%s]'
                      % (endpoint, args))

        return endpoint

    def get_token(self):
        """Validate token is project scoped and return it if it is

        project_id and auth_token were fetched when keystone client was created

        """
        return self._init_client().auth_token

    def refresh_token(self):
        """Gets a new keystone client object and token
        This method should be called if the token has expired

        """
        self._keystone_client = None
        return self.get_token()

    def get_session(self):
        """Returns session of this client.

        :return: session instance
        :rtype: keystoneauth1.session.Session

        """
        return self._init_client().session
