import os
import time
import subprocess
import threading
from pipes import quote

from parallels.core import messages
from parallels.core.logging import get_logger
from parallels.core.utils.common import safe_format, popen_no_inherit, is_run_on_windows
from parallels.core.utils.common.string_utils import safe_utf8_encode
from plesk_migrator.compatibility.utils import stringio

logger = get_logger(__name__)


class MigrationError(Exception):
    def __init__(self, message, error_id=None, details=None):
        """Class constructor

        :type message: str | unicode
        :type error_id: str | unicode
        """
        super(Exception, self).__init__(message)
        self._error_id = error_id
        self._details = details

    @property
    def error_id(self):
        """Error ID - a string identifying the error for other components"""
        return self._error_id

    @property
    def details(self):
        """
        :rtype: str | unicode | None
        """
        return self._details


class ParallelExecutionError(MigrationError):
    def __init__(self):
        super(MigrationError, self).__init__(messages.EXCEPTION_PARALLEL_EXECUTION)


class CommandExecutionError(MigrationError):
    """Exception occurred when executing some command on some host"""

    def __init__(self, message, stdout='', stderr='', host_description='', cause=''):
        super(MigrationError, self).__init__(message)
        self._stdout = stdout
        self._stderr = stderr
        self._host_description = host_description
        self._cause = cause

    @property
    def stdout(self):
        """stdout of the command"""
        return self._stdout

    @property
    def stderr(self):
        """stderr of the command"""
        return self._stderr

    @property
    def host_description(self):
        """Description of the host where the command was executed"""
        return self._host_description

    @property
    def cause(self):
        """Root cause of the error - exception caused command execution to fail"""
        return self._cause


class APIError(MigrationError):
    """Exception occurred when executing some request through external API"""

    @property
    def how_to_reproduce(self):
        """User-friendly description on how to reproduce API request"""
        raise NotImplementedError()


class MigrationConfigurationFileError(MigrationError):
    pass


class MigrationNoContextError(MigrationError):
    pass


class InvalidCredentials(MigrationNoContextError):
    pass


class MigratorInterruptException(MigrationError):
    def __init__(self):
        super(MigratorInterruptException, self).__init__(messages.INTERRUPT_EXCEPTION)


class MigrationNoRepeatError(MigrationError):
    """Exception explicitly stating that there is no sense to repeat failed operation for the object

    For example, if subscription creation failed because such subscription already exists,
    there is no sense to try to create subscription any more times.
    """
    pass


class MigrationStopError(MigrationError):
    """Exception for situations when migration should be stopped, for example failed essential pre-migration checks"""
    pass


def local_command_exec(
    command, stdin_content=None, output_codepage='utf-8', error_policy='strict', env=None, working_dir=None,
    umask=None, stop_mark=None, stop_poll_interval=None
):
    if type(command) is list or type(command) is tuple:
        command_str = ' '.join([quote(c) for c in command])
        command_encoded = [safe_utf8_encode(c) for c in command]
    else:
        command_str = command
        command_encoded = safe_utf8_encode(command)
    command_env = os.environ.copy()
    command_env['LANG'] = 'en_US.utf-8'
    if env is not None:
        command_env.update(env)

    try:
        def preexec_fn():
            if umask is not None:
                os.umask(umask)
            if stop_mark is not None:
                os.setsid()

        p = popen_no_inherit(
            command_encoded,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env=command_env,
            cwd=working_dir,
            # preexec_fn is not supported on Windows
            preexec_fn=preexec_fn if not is_run_on_windows() else None
        )

        if stop_mark is not None:
            if stdin_content is not None:
                p.stdin.write(stdin_content)
            p.stdin.close()

            stdout_data = stringio(is_unicode=False)
            stderr_data = stringio(is_unicode=False)

            read_buffer_size = 1024

            def consume_stdout():
                while p.returncode is None:
                    p.poll()
                    stdout_part = p.stdout.read(read_buffer_size)
                    stdout_data.write(stdout_part)

            def consume_stderr():
                while p.returncode is None:
                    p.poll()
                    stderr_part = p.stderr.read(read_buffer_size)
                    stderr_data.write(stderr_part)

            thread_stdout = threading.Thread(target=consume_stdout)
            thread_stdout.start()

            thread_stderr = threading.Thread(target=consume_stderr)
            thread_stderr.start()

            while True:
                if p.returncode is not None:
                    thread_stdout.join()
                    thread_stderr.join()

                    stdout_last_part = p.stdout.read()
                    stdout_data.write(stdout_last_part)
                    stderr_last_part = p.stderr.read()
                    stderr_data.write(stderr_last_part)

                    stdout_str = stdout_data.getvalue()
                    if output_codepage is not None:
                        stdout_str = stdout_str.decode(output_codepage, error_policy)
                    stderr_str = stderr_data.getvalue()
                    if output_codepage is not None:
                        stderr_str = stderr_str.decode(output_codepage, error_policy)
                    return p.returncode, stdout_str, stderr_str

                if stop_mark.is_set():
                    if is_run_on_windows():
                        subprocess.call(
                            ['taskkill', '/F', '/T', '/PID', str(p.pid)],
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE
                        )
                        p.kill()
                    else:
                        import signal
                        os.killpg(os.getpgid(p.pid), signal.SIGTERM)
                        p.wait()

                    raise MigratorInterruptException()

                time.sleep(stop_poll_interval)
        else:
            stdout_str, stderr_str = p.communicate(stdin_content)
            if output_codepage is not None:
                stdout_str = stdout_str.decode(output_codepage, error_policy)
            if output_codepage is not None:
                stderr_str = stderr_str.decode(output_codepage, error_policy)
            return p.returncode, stdout_str, stderr_str
    except MigratorInterruptException:
        raise
    except Exception as e:
        logger.exception()
        raise Exception(safe_format(messages.FAILED_TO_EXECUTE_LOCAL_COMMAND, command=command_str, reason=e))


def version_tuple(version):
    """Convert string product version to a tuple of ints"""
    return tuple(map(int, version.split('.')))
