# Copyright 2013 OpenStack Foundation
# Copyright 2013 Rackspace Hosting
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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 tempfile

from oslo_log import log as logging

from trove.common import cfg
from trove.common import configurations
from trove.common import exception
from trove.common.notification import EndNotification
from trove.common import utils
from trove.guestagent.common import operating_system
from trove.guestagent.datastore import manager
from trove.guestagent import guest_log
from trove.guestagent.utils import docker as docker_util
from trove.guestagent.utils import mysql as mysql_util
from trove.instance import service_status

LOG = logging.getLogger(__name__)
CONF = cfg.CONF


class MySqlManager(manager.Manager):
    def __init__(self, mysql_app, mysql_app_status, mysql_admin,
                 manager_name='mysql'):
        super(MySqlManager, self).__init__(manager_name)

        self.app = mysql_app
        self.status = mysql_app_status
        self.adm = mysql_admin
        self.volume_do_not_start_on_reboot = False

    @property
    def configuration_manager(self):
        return self.app.configuration_manager

    def get_service_status(self):
        try:
            with mysql_util.SqlClient(self.app.get_engine()) as client:
                cmd = "SELECT 1;"
                client.execute(cmd)

            LOG.debug("Database service check: database query is responsive")
            return service_status.ServiceStatuses.HEALTHY
        except Exception:
            return super(MySqlManager, self).get_service_status()

    def get_start_db_params(self, data_dir):
        return f'--datadir={data_dir}'

    def do_prepare(self, context, packages, databases, memory_mb, users,
                   device_path, mount_point, backup_info,
                   config_contents, root_password, overrides,
                   cluster_config, snapshot, ds_version=None):
        """This is called from prepare in the base class."""
        data_dir = mount_point + '/data'
        self.app.stop_db()
        operating_system.ensure_directory(data_dir,
                                          user=CONF.database_service_uid,
                                          group=CONF.database_service_uid,
                                          as_root=True)
        # This makes sure the include dir is created.
        self.app.set_data_dir(data_dir)

        # Prepare mysql configuration
        LOG.info('Preparing database configuration')
        self.app.configuration_manager.reset_configuration(config_contents)
        self.app.update_overrides(overrides)

        # Restore data from backup and reset root password
        if backup_info:
            self.perform_restore(context, data_dir, backup_info)
            self.reset_password_for_restore(ds_version=ds_version,
                                            data_dir=data_dir)

        # Start database service.
        command = self.get_start_db_params(data_dir)
        self.app.start_db(ds_version=ds_version, command=command)

        self.app.secure()
        enable_remote_root = (backup_info and self.adm.is_root_enabled())
        if enable_remote_root:
            self.status.report_root(context)
        else:
            self.app.secure_root()

        if snapshot:
            # This instance is a replication slave
            self.attach_replica(context, snapshot, snapshot['config'])

    def create_backup(self, context, backup_info):
        """Create backup for the database.

        :param context: User context object.
        :param backup_info: a dictionary containing the db instance id of the
                            backup task, location, type, and other data.
        """
        LOG.info(f"Creating backup {backup_info['id']}")
        with EndNotification(context):
            # Set /var/run/mysqld to allow localhost access.
            volumes_mapping = {
                '/var/lib/mysql': {'bind': '/var/lib/mysql', 'mode': 'rw'},
                "/var/run/mysqld": {"bind": "/var/run/mysqld", "mode": "ro"},
                '/tmp': {'bind': '/tmp', 'mode': 'rw'}
            }
            self.app.create_backup(context, backup_info,
                                   volumes_mapping=volumes_mapping,
                                   need_dbuser=True)

    def get_datastore_log_defs(self):
        owner = cfg.get_configuration_property('database_service_uid')
        datastore_dir = self.app.get_data_dir()
        server_section = configurations.MySQLConfParser.SERVER_CONF_SECTION
        long_query_time = CONF.get(self.manager).get(
            'guest_log_long_query_time') / 1000
        general_log_file = self.build_log_file_name(
            self.GUEST_LOG_DEFS_GENERAL_LABEL, owner,
            datastore_dir=datastore_dir)
        error_log_file = self.validate_log_file('/var/log/mysqld.log', owner)
        slow_query_log_file = self.build_log_file_name(
            self.GUEST_LOG_DEFS_SLOW_QUERY_LABEL, owner,
            datastore_dir=datastore_dir)
        return {
            self.GUEST_LOG_DEFS_GENERAL_LABEL: {
                self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.USER,
                self.GUEST_LOG_USER_LABEL: owner,
                self.GUEST_LOG_FILE_LABEL: general_log_file,
                self.GUEST_LOG_SECTION_LABEL: server_section,
                self.GUEST_LOG_ENABLE_LABEL: {
                    'general_log': 'on',
                    'general_log_file': general_log_file,
                    'log_output': 'file',
                },
                self.GUEST_LOG_DISABLE_LABEL: {
                    'general_log': 'off',
                },
            },
            self.GUEST_LOG_DEFS_SLOW_QUERY_LABEL: {
                self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.USER,
                self.GUEST_LOG_USER_LABEL: owner,
                self.GUEST_LOG_FILE_LABEL: slow_query_log_file,
                self.GUEST_LOG_SECTION_LABEL: server_section,
                self.GUEST_LOG_ENABLE_LABEL: {
                    'slow_query_log': 'on',
                    'slow_query_log_file': slow_query_log_file,
                    'long_query_time': long_query_time,
                },
                self.GUEST_LOG_DISABLE_LABEL: {
                    'slow_query_log': 'off',
                },
            },
            self.GUEST_LOG_DEFS_ERROR_LABEL: {
                self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.SYS,
                self.GUEST_LOG_USER_LABEL: owner,
                self.GUEST_LOG_FILE_LABEL: error_log_file,
            },
        }

    def is_log_enabled(self, logname):
        if logname == self.GUEST_LOG_DEFS_GENERAL_LABEL:
            value = self.configuration_manager.get_value(
                'general_log', section='mysqld', default='off')
            LOG.debug(f"The config value of general_log is {value}")
            return value == 'on'
        elif logname == self.GUEST_LOG_DEFS_SLOW_QUERY_LABEL:
            value = self.configuration_manager.get_value(
                'slow_query_log', section='mysqld', default='off')
            LOG.debug(f"The config value of slow_query_log is {value}")
            return value == 'on'

        return False

    def apply_overrides(self, context, overrides):
        LOG.info("Applying database config.")
        self.app.apply_overrides(overrides)
        LOG.info("Finished applying database config.")

    def reset_password_for_restore(self, ds_version=None,
                                   data_dir='/var/lib/mysql/data'):
        """Reset the root password after restore the db data.

        We create a temporary database container by running mysqld_safe to
        reset the root password.
        """
        LOG.info('Starting to reset password for restore')

        try:
            root_pass = self.app.get_auth_password(file="root.cnf")
        except exception.UnprocessableEntity:
            root_pass = utils.generate_random_password()
            self.app.save_password('root', root_pass)

        init_file = tempfile.NamedTemporaryFile(mode='w')
        operating_system.write_file(
            init_file.name,
            f"ALTER USER 'root'@'localhost' IDENTIFIED BY '{root_pass}';"
        )
        err_file = tempfile.NamedTemporaryFile(suffix='.err')

        # Get the original file owner and group before changing the owner.
        from pathlib import Path
        init_file_path = Path(init_file.name)
        init_file_owner = init_file_path.owner()
        init_file_group = init_file_path.group()

        # Allow database service user to access the temporary files.
        try:
            for file in [init_file.name, err_file.name]:
                operating_system.chown(file, CONF.database_service_uid,
                                       CONF.database_service_uid, force=True,
                                       as_root=True)
        except Exception as err:
            LOG.error('Failed to change file owner, error: %s', str(err))
            for file in [init_file.name, err_file.name]:
                LOG.debug('Reverting the %s owner to %s '
                          'before close it.', file, init_file_owner)
                operating_system.chown(file, init_file_owner,
                                       init_file_group, force=True,
                                       as_root=True)
            init_file.close()
            err_file.close()
            raise err

        # Allow database service user to access the temporary files.
        command = (
            f'mysqld --init-file={init_file.name} '
            f'--log-error={err_file.name} '
            f'--datadir={data_dir} '
        )
        extra_volumes = {
            init_file.name: {"bind": init_file.name, "mode": "rw"},
            err_file.name: {"bind": err_file.name, "mode": "rw"},
        }

        # Start the database container process.
        try:
            self.app.start_db(ds_version=ds_version, command=command,
                              extra_volumes=extra_volumes)
        except Exception as err:
            LOG.error('Failed to reset password for restore, error: %s',
                      str(err))
            raise err  # re-raised at the end of the finally clause
        finally:
            try:
                LOG.debug(
                    'The init container log: %s',
                    docker_util.get_container_logs(self.app.docker_client)
                )
                docker_util.remove_container(self.app.docker_client)
            except Exception as err:
                LOG.error('Failed to remove container. error: %s',
                          str(err))
                pass
            for file in [init_file.name, err_file.name]:
                LOG.debug('Reverting the %s owner to %s '
                          'before close it.', file, init_file_owner)
                operating_system.chown(file, init_file_owner,
                                       init_file_group, force=True,
                                       as_root=True)
            init_file.close()
            err_file.close()

        LOG.info('Finished to reset password for restore')

    def _validate_slave_for_replication(self, context, replica_info):
        if replica_info['replication_strategy'] != self.replication_strategy:
            raise exception.IncompatibleReplicationStrategy(
                replica_info.update({
                    'guest_strategy': self.replication_strategy
                }))

        volume_stats = self.get_filesystem_stats(context, None)
        if (volume_stats.get('total', 0.0) <
                replica_info['dataset']['dataset_size']):
            raise exception.InsufficientSpaceForReplica(
                replica_info.update({
                    'slave_volume_size': volume_stats.get('total', 0.0)
                }))

    def attach_replica(self, context, replica_info, slave_config, **kwargs):
        LOG.info("Attaching replica, replica_info: %s", replica_info)
        try:
            if 'replication_strategy' in replica_info:
                self._validate_slave_for_replication(context, replica_info)

            self.replication.enable_as_slave(self.app, replica_info,
                                             slave_config)
        except Exception as err:
            LOG.error("Error enabling replication, error: %s", str(err))
            self.status.set_status(service_status.ServiceStatuses.FAILED)
            raise

    def make_read_only(self, context, read_only):
        LOG.info("Executing make_read_only(%s)", read_only)
        self.app.make_read_only(read_only)

    def get_latest_txn_id(self, context):
        LOG.info("Calling get_latest_txn_id.")
        return self.app.get_latest_txn_id()

    def get_last_txn(self, context):
        LOG.info("Calling get_last_txn")
        return self.app.get_last_txn()

    def wait_for_txn(self, context, txn):
        LOG.info("Calling wait_for_txn.")
        self.app.wait_for_txn(txn)

    def upgrade(self, context, upgrade_info):
        """Upgrade the database."""
        LOG.info('Starting to upgrade database, upgrade_info: %s',
                 upgrade_info)
        self.app.upgrade(upgrade_info)

    def rebuild(self, context, ds_version, config_contents=None,
                config_overrides=None):
        """Restore datastore service after instance rebuild."""
        LOG.info("Starting to restore database service")
        self.status.begin_install()

        mount_point = CONF.get(CONF.datastore_manager).mount_point
        data_dir = mount_point + '/data'
        operating_system.ensure_directory(data_dir,
                                          user=CONF.database_service_uid,
                                          group=CONF.database_service_uid,
                                          as_root=True)
        # This makes sure the include dir is created.
        self.app.set_data_dir(data_dir)

        try:
            # Prepare mysql configuration
            LOG.debug('Preparing database configuration')
            self.app.configuration_manager.reset_configuration(config_contents)
            self.app.update_overrides(config_overrides)

            # Start database service.
            command = self.get_start_db_params(data_dir)
            self.app.start_db(ds_version=ds_version, command=command)
        except Exception as e:
            LOG.error(f"Failed to restore database service after rebuild, "
                      f"error: {str(e)}")
            self.prepare_error = True
            raise
        finally:
            self.status.end_install(error_occurred=self.prepare_error)
