import ntpath
import os

from parallels.core import messages, MigrationError
from parallels.core.logging import hide_text, get_logger
from parallels.core.registry import Registry
from parallels.core.runners.entities import ExecutionResults
from parallels.core.runners.exceptions.connection_check import ConnectionCheckError
from parallels.core.runners.exceptions.directory_remove_exception import DirectoryRemoveException
from parallels.core.runners.exceptions.mssql import MSSQLException
from parallels.core.runners.windows.base import WindowsRunner
from parallels.core.utils.common import safe_format, open_no_inherit, cached
from parallels.core.utils.common.sql_utils import format_sql_results_for_log
from parallels.core.utils.steps_profiler import get_default_steps_profiler
from parallels.core.utils.windows_agent_pool import WindowsAgentPool

logger = get_logger(__name__)
profiler = get_default_steps_profiler()


class WindowsAgentRunner(WindowsRunner):
    """
    Runner for Windows nodes which have server based on
    lib/python/parallels/source/common/extras/transfer-agent/server.py

    Runner tries to install Windows agent automatically on remote node with paexec.
    In case that is not possible (for example, there is no Samba connection, so
    paexec does not work), you could install agent on remote node manually.
    """
    def __init__(self, settings, host_description=None, proxy_to=None, allow_autodeploy=True):
        self.settings = settings
        self._proxy_to = proxy_to
        super(WindowsAgentRunner, self).__init__(host_description=host_description, hostname=self._host)
        self.remote = WindowsAgentPool.get_instance().get(
            settings, proxy_to, allow_autodeploy=allow_autodeploy
        )

    @property
    @cached
    def native_output_encoding(self):
        """Encoding, in which we have output of native OS utilities

        :rtype: str
        """
        return self.remote.get_native_output_encoding()

    def remove_directory(self, directory, is_remove_root=True):
        result = self.remote.remove_directory(directory, is_remove_root)
        if not result:
            raise DirectoryRemoveException(safe_format(
                messages.FAILED_TO_REMOVE_DIRECTORY, directory=directory, server=self._host
            ))

    def check(self, server_description):
        """Check connection to the server

        :type server_description: str | unicode
        :raises: parallels.core.runners.exceptions.connection_check.ConnectionCheckError
        :rtype: None
        """
        try:
            return self.remote.test_connection()
        except Exception:
            logger.exception()
            raise ConnectionCheckError(
                messages.FAILED_TO_CONNECT_TO_SERVER_WITH_RPC_AGENT.format(server=server_description)
            )

    def get_env(self, name):
        return self.remote.get_env(name)

    def piped_sh_unchecked(
        self, cmd_str, args,
        consume_stdout_function, consume_stderr_function, finalize_function, error_function,
        read_stdin=False
    ):
        """Execute command, and read stdout/stderr dynamically, as it goes

        Function is similar to sh_unchecked, but does not return anything. Instead it calls functions
        once the output is received.

        Consume stdout/stderr function is called once we get the next portion of data from command's stdout/stderr.
        The only argument it accepts is that data (as byte string).

        Once the command is finished, finalize function is called. The only argument is exit code of the command.

        :type cmd_str: str | unicode
        :type args: dict | None
        :type consume_stdout_function: (str) -> None
        :type consume_stderr_function: (str) -> None
        :type finalize_function: (int) -> None
        :type error_function: (str | unicode) -> None
        :type read_stdin: bool
        :rtype: parallels.core.utils.process_handle.ProcessHandle | None
        """
        logger.fdebug(
            messages.EXECUTE_COMMAND_IN_PIPED_MODE, server=self.host_description,
            command=self.format_command_for_log(cmd_str, args)
        )
        return self.remote.piped_sh_unchecked(
            cmd_str, args,
            consume_stdout_function, consume_stderr_function,
            finalize_function, error_function,
            read_stdin=read_stdin
        )

    def _execute_command_raw(self, command, arguments, execution_options):
        """Execute command and return results, but do not log, do not check for exit code, and so on.

        :type command: str | unicode 
        :type arguments: dict[str | unicode, str | unicode] 
        :type execution_options: parallels.core.runners.entities.ExecutionOptions 
        :rtype: parallels.core.runners.entities.ExecutionResults 
        """
        exit_code, stdout, stderr = self.remote.sh_unchecked(
            command, arguments, execution_options.stdin_content, execution_options.environment_variables,
            execution_options.log_output, execution_options.working_dir, execution_options.redirect_output_file,
            output_codepage='raw'
        )
        return ExecutionResults(exit_code, stdout, stderr)

    def get_file(self, remote_filename, local_filename):
        logger.debug(messages.DEBUG_DOWNLOAD_FILE, remote_filename, local_filename, self._host)
        return self.remote.get_file(remote_filename, local_filename)

    def upload_file(self, local_filename, remote_filename):
        logger.debug(messages.DEBUG_UPLOAD_FILE, local_filename, remote_filename, self._host)
        with open_no_inherit(local_filename, 'rb') as fp:
            with profiler.measure_command_call(
                messages.PROFILE_UPLOAD_FILE % (remote_filename,), self.host_description
            ):
                self.remote.upload_file_content(remote_filename, fp.read())

    def upload_file_content(self, filename, content):
        logger.debug(messages.DEBUG_UPLOAD_FILE_CONTENTS, filename, self._host)
        with profiler.measure_command_call(messages.PROFILE_UPLOAD_FILE % (filename,), self.host_description):
            self.remote.upload_file_content(filename, content)

    def get_file_contents(self, remote_filename):
        """Get contents of the file.

        :type remote_filename: str | unicode
        :rtype: str | unicode
        """
        logger.debug(messages.DEBUG_DOWNLOAD_FILE_CONTENTS, remote_filename, self._host)
        with profiler.measure_command_call(messages.PROFILE_DOWNLOAD_FILE % (remote_filename,), self.host_description):
            return self.remote.get_file_contents(remote_filename)

    def move(self, src_path, dst_path):
        logger.debug(messages.DEBUG_MOVE_FILE % (src_path, dst_path, self._host))
        with profiler.measure_command_call(messages.PROFILE_MOVE_FILE % (src_path, dst_path), self.host_description):
            self.remote.move(src_path, dst_path)

    def get_files_list(self, path):
        logger.debug(messages.DEBUG_LIST_FILES % (path, self._host))
        with profiler.measure_command_call(messages.PROFILE_LIST_FILES % path, self.host_description):
            return self.remote.get_files_list(path)

    def remove_file(self, filename):
        logger.debug(messages.DEBUG_REMOVE_FILE, filename, self._host)
        with profiler.measure_command_call(messages.PROFILE_REMOVE_FILE % filename, self.host_description):
            self.remote.remove_file(filename)

    def mkdir(self, dirname):
        logger.debug(messages.DEBUG_CREATE_DIRECTORY, dirname, self._host)
        with profiler.measure_command_call(messages.PROFILE_CREATE_DIRECTORY % (dirname,), self.host_description):
            self.remote.mkdir(dirname)

    def download_directory(self, remote_directory, local_directory):
        migrator_server = Registry().get_instance().get_context().migrator_server
        with migrator_server.runner() as local_runner:
            local_runner.mkdir(local_directory)
        for directory in self.remote.get_directories_list_recursive(remote_directory):
            with migrator_server.runner() as local_runner:
                local_runner.mkdir(os.path.join(local_directory, directory))

        for filename in self.remote.get_files_list_recursive(remote_directory):
            with migrator_server.runner() as local_runner:
                local_runner.upload_file_content(
                    os.path.join(local_directory, filename),
                    self.remote.get_file_contents(ntpath.join(remote_directory, filename))
                )

    def upload_directory(self, local_path, remote_server, remote_path):
        """Upload directory from local server to remote

        :type local_path: str | unicode
        :type remote_server: parallels.core.connections.server.Server
        :type remote_path: str | unicode
        :rtype: None
        """
        self.remote.mkdir(remote_path)
        migrator_server = Registry().get_instance().get_context().migrator_server
        with migrator_server.runner() as local_runner:
            for directory in local_runner.iter_dirs_list_nested(local_path):
                self.remote.mkdir(ntpath.join(remote_path, ntpath.relpath(directory, local_path)))
            for filename in local_runner.iter_files_list_nested(local_path):
                self.upload_file(filename, ntpath.join(remote_path, ntpath.relpath(filename, local_path)))

    def download_directory_as_zip(self, remote_directory, local_zip_filename):
        migrator_server = Registry().get_instance().get_context().migrator_server
        with migrator_server.runner() as local_runner:
            local_runner.upload_file_content(local_zip_filename, self.remote.get_directory_as_zip(remote_directory))

    def file_exists(self, filename):
        return self.remote.file_exists(filename)

    def is_dir(self, path):
        """Returns True, False or None if file does not exist
        :type path: str | unicode
        :rtype: bool | None
        """
        return self.remote.is_dir(path)

    def callback(self, command):
        context = Registry.get_instance().get_context()
        target_connection = context.conn.target
        return self.remote.callback(
            command,
            target_connection.main_node_ip,
            target_connection.settings.windows_auth.username,
            target_connection.settings.windows_auth.password
        )

    def execute_sql(self, query, query_args=None, connection_settings=None, log_output=True):
        """Execute SQL query
        Returns list of rows, each row is a dictionary.
        Query and query arguments:
        * execute_sql("SELECT * FROM clients WHERE type=%s AND country=%s", ['client', 'US'])
        Connection settings: host, port, user, password, database

        :type query: str|unicode
        :type query_args: list|dict
        :type connection_settings: dict
        :type log_output: bool
        :rtype: list[dict]
        :raises: parallels.core.MigrationError
        """
        if isinstance(query_args, dict):
            query_str = query + str(dict((key, "'%s'" % val) for (key, val) in query_args.items()))
        elif isinstance(query_args, (tuple, list)):
            query_str = query + str(tuple("'%s'" % arg for arg in query_args))
        else:
            query_str = query
        with logger.debug_level_context(messages.EXECUTE_SQL_QUERY):
            logger.fdebug(messages.EXECUTE_SQL_QUERY_QUERY, query=query_str)
            with profiler.measure_command_call(
                safe_format(messages.EXECUTE_SQL_QUERY_QUERY, query=query_str), self.host_description
            ):
                result = self.remote.execute_sql(query, query_args, connection_settings, log_output)
                logger.fdebug(
                    messages.EXECUTE_SQL_QUERY_RESULT,
                    result=hide_text(
                        format_sql_results_for_log(result.get('rowset')),
                        not log_output
                    )
                )
                if result.get('status') != 'ok':
                    raise MigrationError(safe_format(
                        messages.FAILED_TO_EXECUTE_SQL_QUERY,
                        reason=result['message']
                    ))
                return result['rowset']

    def mssql_execute(self, connection_settings, query, args=None):
        """Execute MSSQL query, do not return any results

        :type connection_settings: parallels.core.runners.entities.MSSQLConnectionSettings
        :type query: str | unicode
        :type args: dict | None
        :raises parallels.core.runners.exceptions.mssql.MSSQLException:
        :rtype: None
        """
        self._log_mssql_query(connection_settings, query, args)
        result = self.remote.mssql_execute(connection_settings.as_dictionary(), query, args)
        if result['status'] == 'success':
            return
        else:
            raise MSSQLException(result['message'], result['code'], connection_settings, query, args, self._host)

    def mssql_query(self, connection_settings, query, args=None):
        """Execute MSSQL query, return results as a list of dictionaries

        :type connection_settings: parallels.core.runners.entities.MSSQLConnectionSettings
        :type query: str | unicode
        :type args: dict | None
        :raises parallels.core.runners.exceptions.mssql.MSSQLException:
        :rtype: list[dict]
        """
        self._log_mssql_query(connection_settings, query, args)
        result = self.remote.mssql_query(connection_settings.as_dictionary(), query, args)
        if result['status'] == 'success':
            self._log_mssql_query_result(result['rowset'])
            return result['rowset']
        else:
            raise MSSQLException(result['message'], result['code'], connection_settings, query, args, self._host)

    def resolve(self, address):
        """Resolve hostname into IP address.

        Return the first IP address, or None if we were not able to resolve.
        If address is already a valid IP address - return it as is.

        :type address: str | unicode
        :rtype: None | str | unicode
        """
        return self.remote.resolve(address)

    def resolve_all(self, address):
        """Resolve hostname into list of IP addresses.

        If address is already a valid IP address - return
        list that contains only that address.
        Otherwise resolve and return list of IP addresses
        (IPv6 and IPv4 all in one list).
        If we were not able to resolve, empty list is returned.

        :type address: str | unicode
        :rtype: list[str | unicode]
        """
        return self.remote.resolve_all(address)

    def add_allowed_program_firewall_rule(self, name, program):
        """
        :param str|unicode name: Firewall rule name
        :param str|unicode program: Full path to program executable
        """
        message = messages.DEBUG_ADD_ALLOWED_PROGRAM_FIREWALL_RULE.format(rule=name, program=program)
        logger.debug(message)
        with profiler.measure_command_call(message, self.host_description):
            self.remote.add_allowed_program_firewall_rule(name, program)

    def delete_allowed_program_firewall_rule(self, name, program):
        """Delete firewall rule. Call is completed successfully for existent and non-existent rules.
        :param str|unicode name: Firewall rule name
        :param str|unicode program: Full path to program executable
        """
        message = messages.DEBUG_DELETE_ALLOWED_PROGRAM_FIREWALL_RULE.format(rule=name, program=program)
        logger.debug(message)
        with profiler.measure_command_call(message, self.host_description):
            self.remote.delete_allowed_program_firewall_rule(name, program)

    def is_built_in_administrator(self, username):
        """Check that system account with given username is built-in administrator
        :type username: str
        :rtype: bool
        """
        return self.remote.is_built_in_administrator(username)

    def is_run_by_built_in_administrator(self):
        """Check that current process executed by built-in administrator
        :rtype: bool
        """
        return self.remote.is_run_by_built_in_administrator()

    @property
    def _host(self):
        if self._proxy_to is not None:
            return self._proxy_to
        else:
            return self.settings.ip
