import errno
import ntpath
import os
import shutil
from contextlib import contextmanager

from parallels.core import local_command_exec, messages
from parallels.core.logging import get_logger
from parallels.core.runners.entities import ExecutionResults
from parallels.core.runners.exceptions.mssql import MSSQLException
from parallels.core.runners.windows.base import WindowsRunner
from parallels.core.utils.common import cached, mkdir_p, default, open_no_inherit, safe_string_repr
from parallels.core.utils.common.ip import resolve, resolve_all
from parallels.core.utils.file_utils import iter_full_file_paths, iter_full_dir_paths, \
    get_dir_tree as file_utils_get_dir_tree
from parallels.core.utils.steps_profiler import get_default_steps_profiler
from parallels.core.utils.windows_utils import add_windows_long_path_prefix, convert_windows_encoding_to_python

logger = get_logger(__name__)
profiler = get_default_steps_profiler()


class LocalWindowsRunner(WindowsRunner):
    """Execute commands on local Windows server"""

    def __init__(self, disable_profiling=False):
        super(LocalWindowsRunner, self).__init__(host_description='the local server', hostname=None)
        self._disable_profiling = disable_profiling

    @staticmethod
    @cached
    def get_instance(disable_profiling=False):
        return LocalWindowsRunner(disable_profiling)

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

        :rtype: str
        """
        try:
            stdout = self.execute_command('cmd.exe /c chcp').stdout
            output_encoding = stdout.split()[-1].strip('.')
            return convert_windows_encoding_to_python(output_encoding)
        except:
            logger.exception()
            return u'utf-8'

    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 
        """
        if execution_options.redirect_output_file is not None:
            raise NotImplementedError()

        if execution_options.controlled_interruption is not None:
            stop_mark = execution_options.controlled_interruption.stop_mark
            stop_poll_interval = execution_options.controlled_interruption.poll_interval
        else:
            stop_mark = None
            stop_poll_interval = None

        command_str = self._format_command(command, arguments)

        exit_code, stdout, stderr = local_command_exec(
            command_str, execution_options.stdin_content, None, None,
            execution_options.environment_variables, execution_options.working_dir,
            stop_mark=stop_mark, stop_poll_interval=stop_poll_interval
        )
        return ExecutionResults(exit_code, stdout, stderr)

    def upload_file(self, local_filename, remote_filename):
        shutil.copy2(local_filename, remote_filename)

    def upload_file_content(self, filename, content):
        with open_no_inherit(filename, "wb") as fp:
            fp.write(content)

    def get_file(self, remote_filename, local_filename):
        if local_filename != remote_filename:
            logger.debug(messages.DEBUG_COPY_LOCAL_FILE, remote_filename, local_filename)
            shutil.copy2(remote_filename, local_filename)

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

        :type remote_filename: str | unicode
        :rtype: str | unicode
        """
        with open_no_inherit(remote_filename, 'rb') as fp:
            return fp.read()

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

        :type remote_filename: str | unicode
        :rtype: int
        """
        return os.path.getsize(remote_filename)

    def move(self, src_path, dst_path):
        shutil.move(src_path, dst_path)

    def get_files_list(self, path):
        return os.listdir(path)

    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'
                ]
            }
        ]

        Symbolic links are ignored.

        :type path: str | unicode
        :rtype: list[str | {str, list}]
        """
        return file_utils_get_dir_tree(path)

    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]
        """
        return iter_full_file_paths(path)

    def iter_dirs_list_nested(self, path):
        return iter_full_dir_paths(path)

    def mkdir(self, dirname):
        mkdir_p(dirname)

    def create_file(self, filename):
        with open(filename, 'wb'):
            pass

    def append_file_content(self, filename, content):
        with open(filename, 'ab') as fp:
            fp.write(content)

    def remove_directory(self, directory, is_remove_root=True):
        for root, dirs, files in os.walk(directory):
            for name in files:
                self.remove_file(ntpath.join(directory, name))
            for name in dirs:
                self.remove_directory(ntpath.join(directory, name))
            if is_remove_root:
                os.rmdir(add_windows_long_path_prefix(root))

    def remove_file(self, filename):
        try:
            os.remove(add_windows_long_path_prefix(filename))
        except OSError as e:
            # if file does not exist - just skip
            if e.errno != errno.ENOENT:
                raise

    def check(self, server_description):
        # No need to check local execution, it should never fail
        pass

    def file_exists(self, filename):
        return os.path.exists(filename)

    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
        """
        import pymssql
        self._log_mssql_query(connection_settings, query, args)
        with self._wrap_pymssql_error(connection_settings, query, args):
            host, port = self._split_mssql_host_port(connection_settings.host)
            with pymssql.connect(
                host, connection_settings.user, connection_settings.password,
                default(connection_settings.database, ''), port=port
            ) as conn:
                # Disable autocommit, as some commands like backup do not work when it is enabled
                conn.autocommit(True)
                try:
                    with conn.cursor() as cursor:
                        cursor.execute(query, args)
                finally:
                    conn.autocommit(False)

    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]
        """
        import pymssql
        self._log_mssql_query(connection_settings, query, args)
        with self._wrap_pymssql_error(connection_settings, query, args):
            host, port = self._split_mssql_host_port(connection_settings.host)
            with pymssql.connect(
                host, connection_settings.user, connection_settings.password,
                default(connection_settings.database, ''), port=port
            ) as conn:
                with conn.cursor(as_dict=True) as cursor:
                    cursor.execute(query, args)
                    result = cursor.fetchall()
                    self._log_mssql_query_result(result)
                    return result

    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 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 resolve_all(address)

    @contextmanager
    def _wrap_pymssql_error(self, connection_settings, query, args):
        """Context manager, within its scope all pymssql errors will be wrapped with MSSQLException

        :type connection_settings: parallels.core.runners.entities.MSSQLConnectionSettings
        :type query: str | unicode
        :type args: dict | None
        """
        import pymssql
        try:
            yield
        except pymssql.Error as e:
            logger.exception()
            if (
                isinstance(e.args, (list, tuple)) and
                len(e.args) > 0 and
                isinstance(e.args[0], basestring)
            ):
                raise MSSQLException(
                    code=None, message=e.args[0],
                    connection_settings=connection_settings, query=query, args=args,
                    connected_from=self.host_description
                )
            elif (
                isinstance(e.args, (list, tuple)) and
                len(e.args) == 2 and
                isinstance(e.args[0], int) and
                isinstance(e.args[1], basestring)
            ):
                raise MSSQLException(
                    code=e.args[0], message=e.args[1],
                    connection_settings=connection_settings, query=query, args=args,
                    connected_from=self.host_description
                )
            elif (
                isinstance(e.args, (list, tuple)) and
                len(e.args) > 0 and
                isinstance(e.args[0], (list, tuple)) and
                len(e.args[0]) > 1
            ):
                raise MSSQLException(
                    code=e.args[0][0], message=e.args[0][1],
                    connection_settings=connection_settings, query=query, args=args,
                    connected_from=self.host_description
                )
            else:
                raise MSSQLException(
                    code=None, message=safe_string_repr(e),
                    connection_settings=connection_settings, query=query, args=args,
                    connected_from=self.host_description
                )

    @staticmethod
    def _split_mssql_host_port(hostname):
        """Split MSSQL hostname into host and port parts

        For 'localhost,1435' returns ('localhost', '1435').
        For 'localhost' returns ('localhost', None).

        :type hostname: str | unicode
        :rtype: tuple
        """
        parts = hostname.split(',', 1)
        if len(parts) == 2:
            return parts[0], parts[1]
        else:
            return hostname, '1433'
