import os

from parallels.core import safe_format
from parallels.core.logging import get_logger
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ControlledInterruption, ExecutionOptions
from parallels.core.runners.utils import pipe_command_to_file, pipe_command
from parallels.core.utils.common import safe_string_repr, cached
from parallels.core.utils.database_utils import \
    restore_mysql_dump_under_user_linux, restore_mysql_dump_under_user_windows
from parallels.plesk.connections.target_server import PleskTargetServer
from parallels.plesk.source.web import messages
from parallels.plesk.source.web.actions.base.database import DatabaseAction
from parallels.plesk.source.web.actions.deploy.database import DeployDatabases
from parallels.plesk.source.web.actions.deploy.database_user import DeployDatabaseUsers
from parallels.plesk.source.web.utils.site_migration_exception import SiteMigrationError
from parallels.plesk.source.web.utils.statistics import safe_calc_data_size

logger = get_logger(__name__)


class CopyDatabaseContent(DatabaseAction):
    def get_description(self):
        """Get short description of action as string

        :rtype: str | unicode
        """
        return messages.ACTION_COPY_DATABASE_CONTENT

    def get_failure_message(self, global_context):
        """Get message for situation when action failed

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: str | unicode
        """
        return messages.ACTION_COPY_DATABASE_CONTENT_FAILED

    def is_critical(self):
        """If action is critical or not

        If action is critical and it failed for a subscription, migration tool
        won't run the next operations for the subscription.

        :rtype: bool
        """
        return False

    def get_failure_message_for_database(self, global_context, database):
        """Get message for situation when action failed for specific database

        :type global_context: parallels.plesk.source.web.global_context.WebGlobalMigrationContext
        :type database: parallels.plesk.source.web.migrated_database.WebMigratedDatabase
        :rtype: str | unicode | None
        """
        return safe_format(
            messages.ACTION_COPY_DATABASE_CONTENT_FAILED_FOR_DATABASE,
            name=database.target_name
        )

    def get_pre_requirement_actions(self):
        """Get list of pre-requirements to execute that action: list of action classes.

        If any of these pre-requirements were not executed or execution failed, then this action won't be executed.

        :rtype: list[object]
        """
        return [DeployDatabases, DeployDatabaseUsers]

    def run_for_database(self, global_context, database):
        """Run action for single migrated database

        :type global_context: parallels.plesk.source.web.global_context.WebGlobalMigrationContext
        :type database: parallels.plesk.source.web.migrated_database.WebMigratedDatabase
        :rtype: list[parallels.core.reports.model.issue.Issue] | None
        """
        source = global_context.conn.get_source()

        # Do not construct database dump file name by database name, to:
        # 1) Avoid possible security issues.
        # Database name is specified by the customer, so for example a hacker
        # could specify something like '../../etc/passwd' there.
        # 2) Avoid issues with file names, when symbols allowed in database name are not allowed on filesystem.
        #
        # It is possible to use single database name is because we migrate databases one-by-one,
        # no parallel execution is considered there.
        database_dump_name = 'dbdump.sql'

        source_web_rpc_runner = source.get_web_rpc_runner()
        assert isinstance(source_web_rpc_runner, BaseRunner)

        command = u'{mysqldump_path} --no-defaults --host={host} --port={port} --user={user} --default-character-set=' \
                  + (u'utf8mb4' if self._mysql_supports_utf8mb4(source, database) else u'utf8') \
                  + u' --set-charset {database_name}'
        arguments = dict(
            mysqldump_path=source.get_path_to_mysqldump(),
            host=database.source_host,
            port=database.source_port,
            user=database.database_user.source_username,
            database_name=database.source_name
        )
        execution_options = ExecutionOptions(
            environment_variables={
                "MYSQL_PWD": database.database_user.source_password
            }
        )
        session_db_dump_path = global_context.session_files.get_path_to_database_dump()
        try:
            # create SQL dump of database on source
            results = pipe_command_to_file(
                source_web_rpc_runner, command, arguments, execution_options, session_db_dump_path
            )
        except Exception as e:
            logger.exception()
            raise SiteMigrationError(
                safe_format(
                    messages.ACTION_COPY_DATABASE_CONTENT_FAILED_DUMP, database_name=database.source_name
                ),
                details=safe_string_repr(e)
            )
        self._check_results(results, database.source_name, source_web_rpc_runner.is_windows)

        # retrieve target server where database dump will be restored
        if database.target_server is None:
            raise SiteMigrationError(
                safe_format(
                    messages.ACTION_COPY_DATABASE_CONTENT_FAILED_NO_DB_SERVER,
                    database_name=database.source_name, database_type='MySQL'
                )
            )

        # restore database dump on target server
        with database.target_server.runner() as target_runner:
            # copy database dump on target database server
            target_db_dump_dir = database.target_server.panel_server.get_session_file_path('db-dumps')
            target_db_dump_path = database.target_server.join_file_path(
                target_db_dump_dir, database_dump_name
            )
            target_runner.mkdir(target_db_dump_dir)
            target_runner.upload_file(session_db_dump_path, target_db_dump_path)

            with safe_calc_data_size():
                global_context.db_dumps_size += os.path.getsize(target_db_dump_path)

            with global_context.migrator_server.runner() as local_runner:
                assert isinstance(local_runner, BaseRunner)
                local_runner.remove_file(session_db_dump_path)

            # restore dump
            try:
                controlled_interruption = ControlledInterruption(stop_mark=global_context.stop_mark)
                plesk_server = global_context.conn.target.plesk_server
                assert isinstance(plesk_server, PleskTargetServer)

                if database.target_server.utilities_server.is_windows():
                    restore_mysql_dump_under_user_windows(
                        server=database.target_server,
                        system_user=global_context.site_info_storage.get_target_sysuser_login(),
                        # store temporary files for restoration directly in virtual host directory of
                        # subscription
                        temp_directory=plesk_server.get_vhost_dir(
                            global_context.site_info_storage.get_target_subscription()
                        ),
                        database_name=database.target_name,
                        database_login=database.database_user.target_username,
                        database_password=database.database_user.source_password,
                        dump_file=target_db_dump_path,
                        controlled_interruption=controlled_interruption
                    )
                else:
                    restore_mysql_dump_under_user_linux(
                        server=database.target_server,
                        system_user=global_context.site_info_storage.get_target_sysuser_login(),
                        # store temporary files for restoration directly in virtual host directory of
                        # subscription
                        temp_directory=plesk_server.get_vhost_dir(
                            global_context.site_info_storage.get_target_subscription()
                        ),
                        database_name=database.target_name,
                        database_login=database.database_user.target_username,
                        database_password=database.database_user.source_password,
                        dump_file=target_db_dump_path,
                        controlled_interruption=controlled_interruption
                    )
            except Exception as e:
                logger.exception()
                raise SiteMigrationError(
                    safe_format(
                        messages.ACTION_COPY_DATABASE_CONTENT_FAILED_RESTORE, name=database.source_name
                    ),
                    details=safe_string_repr(e)
                )

            if database.target_server.utilities_server.is_windows():
                target_runner.remove_file(target_db_dump_path)

    @staticmethod
    def _check_results(results, db_name, is_windows):
        """
        :type results: parallels.core.runners.utils.PipeToFileCommandResult
        :type db_name: str | unicode
        :type is_windows: bool
        """
        if results.exit_code == 0:
            return
        if is_windows:
            # Stderr will be dropped on WebRpcAgent's side if it runs on Windows.
            # This behaviour is caused by PHP limitations on Windows.
            raise SiteMigrationError(safe_format(
                messages.DATABASE_DUMP_FAILED_WINDOWS,
                db_name=db_name, exit_code=results.exit_code
            ))
        else:
            raise SiteMigrationError(safe_format(
                messages.DATABASE_DUMP_FAILED_LINUX,
                db_name=db_name, exit_code=results.exit_code, stderr=results.stderr
            ))

    @cached
    def _mysql_supports_utf8mb4(self, source, database):
        """
        :type source: parallels.plesk.source.web.connections.WebSource
        :type database: parallels.plesk.source.web.migrated_database.WebMigratedDatabase
        :rtype: bool
        """
        source_web_rpc_runner = source.get_web_rpc_runner()
        assert isinstance(source_web_rpc_runner, BaseRunner)

        command = u'{mysqldump_path} --no-defaults --compact --no-create-info ' \
                  u'--host={host} --port={port} --user={user} ' \
                  u'information_schema character_sets --where="CHARACTER_SET_NAME=\'utf8mb4\'"'
        arguments = dict(
            mysqldump_path=source.get_path_to_mysqldump(),
            host=database.source_host,
            port=database.source_port,
            user=database.database_user.source_username
        )
        execution_options = ExecutionOptions(
            environment_variables={
                "MYSQL_PWD": database.database_user.source_password
            }
        )

        try:
            results = pipe_command(source_web_rpc_runner, command, arguments, execution_options)
        except Exception as e:
            logger.fdebug(
                messages.ACTION_COPY_DATABASE_CONTENT_FAILED_CHECK_UTF8MB4,
                database_name=database.source_name, details=safe_string_repr(e)
            )
            logger.exception()
            return False

        return len(results.stdout) > 0
