import os

from parallels.core import messages
from parallels.core.logging import hide_text, get_logger
from parallels.core.runners.entities import ExecutionOptions, FileListItem
from parallels.core.runners.exceptions.non_zero_exit_code import NonZeroExitCodeException
from parallels.core.utils.clean_logging import clean_dict_args_for_log
from parallels.core.utils.common import safe_string_repr, safe_format, default
from parallels.core.utils.common.sql_utils import format_sql_results_for_log
from parallels.core.utils.steps_profiler import get_default_steps_profiler

logger = get_logger(__name__)
profiler = get_default_steps_profiler()


class BaseRunner(object):
    """Base class for runner abstraction, which provides ability to run commands and work with files on a server"""

    def __init__(self, host_description, hostname):
        """
        :type host_description: str | unicode | None
        :type hostname: str | unicode | None
        """
        self._host_description = host_description
        self._hostname = hostname
        if self._host_description is None:
            self._host_description = "server '%s'" % self._hostname

    @property
    def default_output_encoding(self):
        """Encoding, in which most of the output for commands executed with the runner

        :rtype: str
        """
        return 'utf-8'  # by default, consider that output is encoded in UTF-8

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

        :rtype: str
        """
        return 'utf-8'  # by default, consider that output is encoded in UTF-8

    @property
    def host_description(self):
        """Human-readable description of the host this runner is working with.

        Examples: "server '192.168.1.12'", "database server 'my-db-server.example.com'", "local server", etc
        Host description is used in error messages, when runner failed to execute some command.

        :rtype: str | unicode
        """
        return self._host_description

    @property
    def is_windows(self):
        raise NotImplementedError()

    def piped_sh_unchecked(
        self, command, arguments, execution_options,
        consume_stdout_function, consume_stderr_function, finalize_function, error_function
    ):
        """Execute command, and read stdout/stderr dynamically, as it goes.

        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 command: str | unicode
        :type arguments: dict | None
        :type execution_options: parallels.core.runners.entities.ExecutionOptions
        :type consume_stdout_function: (str) -> None
        :type consume_stderr_function: (str) -> None
        :type finalize_function: (int) -> None
        :type error_function: (str | unicode) -> None
        """
        raise NotImplementedError()

    def execute_command(self, command, arguments=None, execution_options=None):
        """
        :type command: str | unicode 
        :type arguments: dict[str | unicode, str | unicode] | None
        :type execution_options: parallels.core.runners.entities.ExecutionOptions | None 
        :rtype: parallels.core.runners.entities.ExecutionResults 
        """
        arguments = default(arguments, {})
        execution_options = default(execution_options, ExecutionOptions())

        output_encoding = default(execution_options.output_encoding, self.default_output_encoding)

        command_for_log = self.format_command_for_log(command, arguments)

        if execution_options.log_execution:
            logger.start_debug_level(
                messages.DEBUG_EXECUTE_COMMAND,
                server=self.host_description, command=command_for_log
            )

        try:
            with profiler.measure_command_call(command_for_log, self.host_description):
                execution_results = self._execute_command_raw(command, arguments, execution_options)

            if execution_options.log_execution:
                logger.fdebug(
                    messages.DEBUG_COMMAND_RESULTS,
                    stdout=hide_text(
                        safe_string_repr(execution_results.stdout, encoding=output_encoding),
                        not execution_options.log_output
                    ),
                    stderr=hide_text(
                        safe_string_repr(execution_results.stderr, encoding=output_encoding),
                        not execution_options.log_output
                    ),
                    exit_code=execution_results.exit_code
                )
        finally:
            if execution_options.log_execution:
                logger.end_level()

        if not execution_options.ignore_exit_code:
            self._check_exit_code(
                command_for_log, execution_results.exit_code, execution_results.stdout, execution_results.stderr,
                execution_options.log_output, output_encoding
            )

        return execution_results

    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 
        """
        raise NotImplementedError()

    def _check_exit_code(self, command_str, exit_code, stdout, stderr, log_output, output_encoding):
        """Check exit code of a command, raise exception if it is not equal to 0, return stdout if it is equal to 0

        :type command_str: str | unicode
        :type exit_code: int
        :type stdout: str | unicode
        :type stderr: str | unicode
        :type output_encoding: str
        :raises: NonZeroExitCodeException
        :rtype: str | unicode
        """
        if exit_code != 0:
            raise NonZeroExitCodeException(
                safe_format(
                    messages.RUNNER_COMMAND_STR_FAILED_WITH_EXIT_CODE,
                    server=self.host_description, command=command_str,
                    exit_code=exit_code,
                    stdout=hide_text(safe_string_repr(stdout, encoding=output_encoding), not log_output),
                    stderr=hide_text(safe_string_repr(stderr, encoding=output_encoding), not log_output)
                ),
                stdout=stdout,
                stderr=stderr,
                exit_code=exit_code
            )
        else:
            return stdout

    def run_php(self, php_code):
        raise NotImplementedError()

    def append_file_content(self, filename, content):
        raise NotImplementedError()

    def create_file(self, filename):
        raise NotImplementedError()

    def upload_file_content(self, filename, content):
        """Put data into the file.

        :type filename: str | unicode
        :type content: str
        :rtype: None
        """
        raise NotImplementedError()

    def upload_file(self, local_filename, remote_filename):
        """
        :type local_filename: str | unicode
        :type remote_filename: str | unicode
        :rtype: None
        """
        raise NotImplementedError()

    def get_file(self, remote_filename, local_filename):
        """
        :type remote_filename: str | unicode
        :type local_filename: str | unicode
        :rtype: None
        """
        raise NotImplementedError()

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

        :type remote_filename: str | unicode
        :rtype: str | unicode
        """
        raise NotImplementedError()

    def get_file_size(self, remote_filename):
        """Get size of specified file (in bytes)

        :type remote_filename: str | unicode
        :rtype: int
        """
        raise NotImplementedError()

    def move(self, src_path, dst_path):
        raise NotImplementedError()

    def remove_file(self, filename):
        """Remove file. If file does not exist - silently do nothing.

        :type filename: str | unicode
        """
        raise NotImplementedError()

    def remove_directory(self, directory, is_remove_root=True):
        """
        :type directory: str | unicode
        :type is_remove_root: bool
        :rtype: None
        """
        raise NotImplementedError()

    def file_exists(self, filename):
        raise NotImplementedError()

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

    def mkdir(self, dirname):
        """
        :type dirname: str | unicode
        :rtype: None
        """
        raise NotImplementedError()

    def download_directory(self, remote_directory, local_directory):
        raise NotImplementedError()

    def download_directory_as_zip(self, remote_directory, local_zip_filename):
        raise NotImplementedError()

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

        :type local_path: str
        :type remote_server: parallels.core.connections.server.Server
        :type remote_path: str
        """
        raise NotImplementedError()

    def check(self, server_description):
        """Check connection with that runner

        :type server_description: str | unicode
        :rtype None
        """
        raise NotImplementedError()

    def get_files_list(self, path):
        raise NotImplementedError()

    def get_directories_list(self, path):
        """
        :type path: str | unicode
        :rtype: list[str | unicode]
        """
        raise NotImplementedError()

    def get_files_list_extended(self, path):
        """Retrieve list of item in path, with with information whether each item is file or directory

        :type path: str | unicode 
        :rtype: list[parallels.core.runners.entities.FileListItem]
        """
        # Default implementation using "get_files_list" and "get_directories_list" functions.
        # Override to improve performance.
        items = []
        for name in self.get_files_list(path):
            items.append(FileListItem(name, False))
        for name in self.get_directories_list(path):
            items.append(FileListItem(name, True))
        return items

    def get_dir_tree(self, path):
        """Retrieve tree structure of the directory with given path, like the following:

        [
            'file_1',
            'file_2',
            {
                'dir_1': [
                    'file_3'
                ]
            }
        ]

        :type path: str | unicode
        :rtype: list[str | {str, list}]
        """
        # Default implementation using "get_files_list" and "get_directories_list" functions.
        # Override to improve performance.
        items = []
        for directory in self.get_directories_list(path):
            items.append({directory: self.get_dir_tree(os.path.join(path, directory))})
        for filename in self.get_files_list(path):
            items.append(filename)
        return items

    def iter_files_list_nested(self, path):
        """Iterate over all (including files in nested directories) file names inside a path

        :type path: str | unicode
        :rtype: list[str | unicode]
        """
        raise NotImplementedError()

    def iter_dirs_list_nested(self, path):
        """Iterate over all (including nested) directory names inside a path

        :type path: str | unicode
        :rtype: list[str | unicode]
        """
        raise NotImplementedError()

    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
        """
        raise NotImplementedError()

    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
        """
        raise NotImplementedError()

    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]
        """
        raise NotImplementedError()

    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
        """
        raise NotImplementedError()

    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
        """
        raise NotImplementedError()

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

    def is_run_by_built_in_administrator(self):
        """Check that current process executed by built-in administrator
        :rtype: bool
        """
        raise NotImplementedError()

    def get_env(self, name):
        """Get value of specified environment variable

        :param str | unicode name: Name of variable, case-insensitive
        :rtype: str | unicode | None
        """
        for key, value in os.environ.items():
            if key.upper() == name.upper():
                return value
        return None

    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
        """
        raise NotImplementedError()

    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]
        """
        raise NotImplementedError()

    def _log_mssql_query(self, connection_settings, query, args=None):
        if args is None:
            logger.fdebug(
                messages.EXECUTE_MSSQL_QUERY,
                executor_server=self._host_description, server=connection_settings.host,
                query=query
            )
        else:
            logger.fdebug(
                messages.EXECUTE_MSSQL_QUERY_ARGS,
                executor_server=self._host_description, server=connection_settings.host,
                query=query, arguments=repr(args)
            )

    @staticmethod
    def _log_mssql_query_result(result):
        logger.fdebug(messages.EXECUTE_MSSQL_QUERY_RESULT, result=format_sql_results_for_log(result))

    def format_command_for_log(self, cmd, args=None):
        """Same as _format_command, but hides all data that looks like a password, for correct logging

        This command line will be added to log and error messages, so it is expected that you could
        simply copy-paste it from logs or migrator output, and execute.

        :type cmd: str | unicode
        :type args: dict[str | unicode, str | unicode] | None
        :rtype: str | unicode
        """
        return self._format_command(cmd, clean_dict_args_for_log(args))

    def _format_command(self, cmd, args=None):
        """Format command arguments passed for 'execute_command" method into command line string

        :type cmd: str | unicode
        :type args: dict[str | unicode, str | unicode] | None
        :rtype: str | unicode
        """
        raise NotImplementedError()
