# Copyright 2013 Cloudbase Solutions Srl
#
#    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 oslo_log import log as oslo_logging

from cloudbaseinit import conf as cloudbaseinit_conf
from cloudbaseinit import constant
from cloudbaseinit.osutils import factory as osutils_factory
from cloudbaseinit.plugins.common import base
from cloudbaseinit.plugins.common import constants as plugin_constant
from cloudbaseinit.utils import crypt

CONF = cloudbaseinit_conf.CONF
LOG = oslo_logging.getLogger(__name__)


class SetUserPasswordPlugin(base.BasePlugin):

    def _encrypt_password(self, ssh_pub_key, password):
        cm = crypt.CryptManager()
        return cm.public_encrypt(ssh_pub_key, password)

    def _get_password(self, service, shared_data):
        injected = False
        if CONF.inject_user_password:
            password = service.get_admin_password()
        else:
            password = None

        if password:
            injected = True
            LOG.warn('Using admin_pass metadata user password. Consider '
                     'changing it as soon as possible')
        else:
            password = shared_data.get(plugin_constant.SHARED_DATA_PASSWORD)

        return password, injected

    def _set_metadata_password(self, password, service):
        if service.is_password_set:
            LOG.debug('User\'s password already set in the instance metadata '
                      'and it cannot be updated in the instance metadata')
            return True
        else:
            user_pwd_encryption_key = service.get_user_pwd_encryption_key()
            if user_pwd_encryption_key:
                enc_password_b64 = self._encrypt_password(
                    user_pwd_encryption_key, password)
                return service.post_password(enc_password_b64)
            else:
                LOG.info('No SSH public key available for password encryption')
                return True

    def _set_password(self, service, osutils, user_name, shared_data):
        """Change the password for the received username if it is required.

        The used password can be the one received from the metadata provider,
        if it does exist, or a random one will be generated.

        .. notes:
            This method has a different behaviour depending on the value of
            :meth:`~can_update password` if this is True the password will
            be set only if the :meth:`~is_password_changed` is also True.
        """
        if service.can_update_password and not service.is_password_changed():
            LOG.info('Updating password is not required.')
            return None

        password, injected = self._get_password(service, shared_data)
        if not password:
            LOG.debug('Generating a random user password')
            password = osutils.generate_random_password(
                CONF.user_password_length)

        osutils.set_user_password(user_name, password)
        self._change_logon_behaviour(user_name, password_injected=injected)
        return password

    def _change_logon_behaviour(self, username, password_injected=False):
        """Post set password logic

        If the option is activated, force the user to change the
        password at next logon.
        """
        if CONF.first_logon_behaviour == constant.NEVER_CHANGE:
            return

        clear_text = (CONF.first_logon_behaviour ==
                      constant.CLEAR_TEXT_INJECTED_ONLY)
        always = CONF.first_logon_behaviour == constant.ALWAYS_CHANGE
        if always or (clear_text and password_injected):
            osutils = osutils_factory.get_os_utils()
            osutils.change_password_next_logon(username)

    def execute(self, service, shared_data):
        # TODO(alexpilotti): The username selection logic must be set in the
        # CreateUserPlugin instead if using CONF.username
        user_name = shared_data.get(plugin_constant.SHARED_DATA_USERNAME,
                                    CONF.username)

        osutils = osutils_factory.get_os_utils()
        if osutils.user_exists(user_name):
            password = self._set_password(service, osutils,
                                          user_name, shared_data)
            if password:
                LOG.info('Password succesfully updated for user %s' %
                         user_name)
                # TODO(alexpilotti): encrypt with DPAPI
                shared_data[plugin_constant.SHARED_DATA_PASSWORD] = password

                if not service.can_post_password:
                    LOG.info('Cannot set the password in the metadata as it '
                             'is not supported by this service')
                else:
                    self._set_metadata_password(password, service)

        if service.can_update_password:
            # If the metadata provider can update the password, the plugin
            # must run at every boot in order to update the password if
            # it was changed.
            return base.PLUGIN_EXECUTE_ON_NEXT_BOOT, False
        else:
            return base.PLUGIN_EXECUTION_DONE, False
