import itertools
import ssl
import sys
import urllib
from xml.etree import ElementTree

from parallels.core import messages
from parallels.core.logging import get_logger
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.runners.exceptions.non_zero_exit_code import NonZeroExitCodeException
from parallels.core.utils import unix_utils, windows_utils
from parallels.core.utils.clean_logging import clean_list_args_for_log, clean_dict_args_for_log, dummy_text
from parallels.core.utils.common import safe_string_repr, default, unused
from parallels.core.utils.common_constants import PLESK_PANEL_DEFAULT_PORT, PLESK_PANEL_DEFAULT_PROTOCOL
from parallels.core.utils.http_client import perform_http_request_response_text

logger = get_logger(__name__)


class CLIGateAuthPassword(object):
    def __init__(self, password):
        self.password = password


class CLIGateAuthSecretKey(object):
    def __init__(self, secret_key):
        self.secret_key = secret_key


class PleskCLIRunnerBase(object):
    """Base class to run Plesk utilities"""

    def run(self, utility_name, arguments, stdin_content=None, env=None, is_private=False):
        raise NotImplementedError()

    def run_unchecked(self, utility_name, arguments, stdin_content=None, env=None, is_private=False):
        raise NotImplementedError()

    @staticmethod
    def _get_cmd_env(env):
        cmd_env = {
            'LANG': 'en_US.UTF-8',
            'ALLOW_WEAK_PASSWORDS': '1',
            'PLESK_RESTORE_MODE': '1',
        }
        if env is not None:
            cmd_env.update(env)
        return cmd_env


class PleskCLIRunnerCLI(PleskCLIRunnerBase):
    """Run Plesk utilities with runner provided by server object"""

    def __init__(self, plesk_server):
        self.plesk_server = plesk_server

    def run(self, utility_name, arguments, stdin_content=None, env=None, is_private=False):
        """Run Plesk command line utility; raise exception if exit code is not null

        :type utility_name: str|unicode
        :type arguments: list[str|unicode]
        :type stdin_content: str|None
        :type env: dict[str|unicode, str|unicode]|None
        :type is_private: bool
        :rtype: str
        """
        env = self._get_cmd_env(env)
        with self.plesk_server.runner() as runner:
            assert isinstance(runner, BaseRunner)

            cmd, args = self._format_command(utility_name, arguments, is_private)
            return runner.execute_command(
                cmd, args,
                ExecutionOptions(
                    stdin_content=stdin_content,
                    environment_variables=env,
                    # Define umask to overwrite the default mask, used by migrator.
                    # For security reasons migrator can use special umask.
                    # Calling internal commands with the default umask can create files with incorrect permissions.
                    umask=0o22
                )
            ).stdout

    def run_unchecked(self, utility_name, arguments, stdin_content=None, env=None, is_private=False):
        """Run Plesk command line utility

        :type utility_name: str|unicode
        :type arguments: list[str|unicode]
        :type stdin_content: str|None
        :type env: dict[str|unicode, str|unicode]|None
        :type is_private: bool
        :rtype: str
        """
        env = self._get_cmd_env(env)
        with self.plesk_server.runner() as runner:
            assert isinstance(runner, BaseRunner)

            cmd, args = self._format_command(utility_name, arguments, is_private)
            results = runner.execute_command(
                cmd, args,
                ExecutionOptions(
                    stdin_content=stdin_content,
                    environment_variables=env,
                    # Define umask to overwrite the default mask, used by migrator.
                    # For security reasons migrator can use special umask.
                    # Calling internal commands with the default umask can create files with incorrect permissions.
                    umask=0o22,
                    ignore_exit_code=True
                )
            )
            return results.exit_code, results.stdout, results.stderr

    def _format_command(self, utility_name, arguments, is_private=False):
        cmd = "{utility_bin}"
        args = dict(
            utility_bin=self._get_path(utility_name, is_private)
        )
        for arg_num, arg_value in enumerate(arguments):
            var_name = 'arg%s' % arg_num
            cmd += ' {%s}' % var_name
            args[var_name] = arg_value

        return cmd, args

    def _get_path(self, utility_name, is_private):
        if is_private:
            return self.plesk_server.get_admin_bin_util_path(utility_name)
        return self.plesk_server.get_bin_util_path(utility_name)


class PleskCLIRunnerLocalCLIGate(PleskCLIRunnerBase):
    """Run Plesk utilities with the help of CLIGate"""

    def __init__(self, cligate_auth, is_windows, ip, host=None, protocol=None, port=None):
        """Class constructor

        :param cligate_auth: type of CLIGate auth, allowed value: CLIGateAuthPassword
        """
        self._cligate_auth = cligate_auth
        self._is_windows = is_windows
        self._ip = ip
        self._host = host
        self._protocol = default(protocol, PLESK_PANEL_DEFAULT_PROTOCOL)
        self._port = default(port, PLESK_PANEL_DEFAULT_PORT)

    def run_unchecked(self, utility_name, arguments, stdin_content=None, env=None, is_private=False):
        # for cligate runner no matter public or private utility should be executed
        unused(is_private)

        log_query_arguments = clean_list_args_for_log(
            arguments,
            # use '-' as fill for hidden passwords, as usual '*' will be quited in HTTP query which looks a bit ugly
            dummy_fill='-'
        )
        log_command_arguments = clean_list_args_for_log(arguments)

        query = []
        log_query = []
        headers = {}

        for argument in itertools.chain([utility_name], arguments):
            query.append(('ARG[]', argument.encode('utf-8')))
        for log_argument in itertools.chain([utility_name], log_query_arguments):
            log_query.append(('ARG[]', log_argument.encode('utf-8')))

        if stdin_content is not None:
            query.append(('STDIN', stdin_content.encode('utf-8')))
            log_query.append(('STDIN', stdin_content.encode('utf-8')))

        cmd_env = self._get_cmd_env(env)
        log_cmd_env = clean_dict_args_for_log(cmd_env)
        for key, value in cmd_env.items():
            query.append(('ENV[%s]' % key, value.encode('utf-8')))
        for log_key, log_value in log_cmd_env.items():
            log_query.append(('ENV[%s]' % log_key, log_value.encode('utf-8')))

        if isinstance(self._cligate_auth, CLIGateAuthPassword):
            query.append(('PSW', self._cligate_auth.password.encode('utf-8')))
            log_query.append(('PSW', dummy_text('-')))
        elif isinstance(self._cligate_auth, CLIGateAuthSecretKey):
            headers['HTTP_AUTH_SECRET_KEY'] = self._cligate_auth.secret_key
        else:
            raise NotImplementedError(messages.PROVIDED_CLIGATE_AUTH_IS_NOT_SUPPORTED)

        query_str = urllib.urlencode(query)
        log_query_str = urllib.urlencode(log_query)

        url_str = "%s://%s:%s/cligate.php" % (self._protocol, self._ip, self._port)
        cmd_str = self._format_command(utility_name, log_command_arguments, log_cmd_env)
        logger.fdebug(messages.DEBUG_CLIGATE_REQUEST, url=url_str, query=log_query_str, command=cmd_str)

        result = perform_http_request_response_text(
            url_str, query_str,
            headers=headers,
            hostname=self._host,
            # CLIGate request must always come from 127.0.0.1,
            # as Plesk forbids requests from other addresses by default
            ip_address='127.0.0.1',
            verify_certificates=False
        )

        result_xml = ElementTree.fromstring(result)
        exit_code = int(result_xml.findtext('./return'))
        stdout = result_xml.findtext('./stdout')
        stderr = result_xml.findtext('./stderr')
        if stderr is None:
            stderr = ''

        return exit_code, stdout, stderr

    def run(self, utility_name, arguments, stdin_content=None, env=None, is_private=False):
        exit_code, stdout, stderr = self.run_unchecked(utility_name, arguments, stdin_content, env, is_private)
        if exit_code != 0:
            cmd_str = self._format_command(
                utility_name,
                clean_list_args_for_log(arguments),
                clean_dict_args_for_log(self._get_cmd_env(env))
            )
            raise NonZeroExitCodeException(
                messages.CLIGATE_COMMAND_FAILED % (
                    cmd_str, exit_code, safe_string_repr(stdout), safe_string_repr(stderr)
                ), stdout=stdout, stderr=stderr, exit_code=exit_code
            )
        return stdout

    def _format_command(self, utility_name, arguments, env=None):
        """
        :type utility_name: str|unicode
        :type arguments: list[str|unicode]
        :type env: dict[str|unicode, str|unicode]|None
        :rtype: str|unicode
        """
        command = []
        if env:
            command.append(' '.join('%s=%s' % (name, value) for name, value in env.items()))
        if self._is_windows:
            command.append(windows_utils.format_command_list(utility_name, arguments))
        else:
            command.append(unix_utils.format_command_list(utility_name, arguments))
        return ' '.join(command)

    @staticmethod
    def _prepare_context():
        if (sys.version_info.major, sys.version_info.minor, sys.version_info.micro) < (2, 7, 9):
            return None
        context = ssl.create_default_context()
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        return context
