import sys
from contextlib import contextmanager
import ntpath
import posixpath

from parallels.core.logging import get_logger
from parallels.core import messages
from parallels.core.connections.unix_server import UnixServer
from parallels.core.registry import Registry
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.utils.common_constants import PLESK_SECRET_KEY_DESCRIPTION
from parallels.core.utils.windows_utils import get_from_registry
from parallels.core.utils.common import cached
from parallels.core.utils import plesk_utils
from parallels.core.utils import migrator_utils
from parallels.core.utils import windows_utils
from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.core.utils.plesk_utils import parse_secret_keys, get_plesk_ips_with_cli, get_plesk_ips_with_api, \
    get_server_info_with_key

logger = get_logger(__name__)


class PleskServer(UnixServer):

    def description(self):
        raise NotImplementedError()

    def is_windows(self):
        raise NotImplementedError()

    def plesk_api(self):
        raise NotImplementedError()

    @contextmanager
    def runner(self):
        """Get runner object to execute commands on Plesk server"""
        raise NotImplementedError()

    def cli_runner(self):
        """Get runner object to execute Plesk utilities"""
        raise NotImplementedError()

    def cligate_runner(self):
        """Get runner object to execute Plesk utilities with CLIGate"""
        raise NotImplementedError()

    @property
    def is_power_user_mode(self):
        """Check if Plesk is in Power-User mode"""
        return self._get_server_info().gen_info.mode == 'poweruser'

    @property
    def can_manage_resellers(self):
        """Check if resellers could be managed by Plesk (e.g. WebPro edition does not allow that)

        :rtype: bool
        """
        return self._get_server_info().key.properties.get('can-manage-resellers', '1') != '0'

    def run_test_api_request(self):
        """Run some Plesk API query to test connection"""
        self._get_server_info()

    @cached
    def _get_server_info(self):
        return get_server_info_with_key(self)

    @property
    @cached
    def panel_admin_password(self):
        with self.runner() as runner:
            assert isinstance(runner, BaseRunner)
            if self.is_windows():
                if (10, 4) <= self.get_plesk_version() < (17, 0):
                    return runner.execute_command(
                        u'{admin_bin} --show-password',
                        dict(admin_bin=ntpath.join(self.plesk_dir, u'bin\\admin')),
                        ExecutionOptions(
                            log_output=False
                        )
                    ).stdout.rstrip(b'\n')
                elif (17, 0) <= self.get_plesk_version() < (17, 8):
                    stdout = runner.execute_command(
                        u'{plesksrvclient_bin} -get -nogui -noclipboard',
                        dict(plesksrvclient_bin=ntpath.join(self.plesk_dir, u'admin\\bin\\plesksrvclient')),
                        ExecutionOptions(
                            log_output=False
                        )
                    ).stdout
                    return stdout[stdout.find(b': ') + 2:]
                else:
                    return runner.execute_command(
                        u'{psadb_bin} --get-admin-password',
                        dict(psadb_bin=ntpath.join(self.plesk_dir, u'admin\\bin64\\psadb.exe')),
                        ExecutionOptions(
                            log_output=False
                        )
                    ).stdout.strip()
            else:
                if (10, 4) <= self.get_plesk_version() < (17, 0):
                    return runner.execute_command(
                        u'{admin_bin} --show-password',
                        dict(admin_bin=posixpath.join(self.plesk_dir, u'bin/admin')),
                        ExecutionOptions(
                            log_output=False
                        )
                    ).stdout.rstrip(b'\n')
                else:
                    return runner.get_file_contents('/etc/psa/.psa.shadow').rstrip(b'\n')

    @property
    @cached
    def panel_secret_key(self):
        # assume that Plesk Migrator installed on the target Plesk server
        ip_address = Registry.get_instance().get_context().conn.target.main_node_ip
        return self._get_secret_key(ip_address)

    @property
    @cached
    def panel_secret_key_local(self):
        ip_address = Registry.get_instance().get_context().conn.target.main_node_ip
        if ip_address == '127.0.0.1':
            return self.panel_secret_key
        else:
            return self._get_secret_key('127.0.0.1')

    def _get_secret_key(self, ip_address):
        if self.get_plesk_version() < (12, 5):
            # do not use secret keys for interaction with legacy Plesk servers to avoid possible issues
            return None
        with self.runner() as runner:
            assert isinstance(runner, BaseRunner)
            if self.is_windows():
                secret_key_bin = ntpath.join(self.plesk_dir, u'bin\\secret_key')
            else:
                secret_key_bin = posixpath.join(self.plesk_dir, u'bin/secret_key')
            # list available secret keys
            stdout = runner.execute_command(
                u'{secret_key_bin} --list', dict(secret_key_bin=secret_key_bin),
                ExecutionOptions(log_output=False)
            ).stdout
            # try to find secret key created earlier
            for secret_key in parse_secret_keys(stdout):
                if secret_key['ip'] == ip_address and secret_key['description'] == PLESK_SECRET_KEY_DESCRIPTION:
                    try:
                        # added from PPP-51310, Plesk 18.0.33+ now shows keys only when they are created
                        int(secret_key['key'])
                        logger.fdebug(messages.DELETE_SECURITY_KEY_IF_LISTED_IS_INVALID)
                        runner.execute_command(
                            u'{secret_key_bin} --delete -key {key}',
                            dict(secret_key_bin=secret_key_bin, key=secret_key['key']),
                            ExecutionOptions(log_output=False)
                        )
                    except ValueError:
                        return secret_key['key']

            # create new secret_key
            return runner.execute_command(
                u'{secret_key_bin} --create -ip-address {ip_address} -description {description}',
                dict(
                    secret_key_bin=secret_key_bin,
                    ip_address=ip_address,
                    description=PLESK_SECRET_KEY_DESCRIPTION
                ),
                ExecutionOptions(
                    log_output=False
                )
            ).stdout

    @property
    @cached
    def vhosts_dir(self):
        if self.is_windows():
            with self.runner() as runner:
                return plesk_utils.get_windows_vhosts_dir(runner)
        else:
            return self._psa_conf_reader.get_vhosts_dir()

    def get_vhost_dir(self, domain):
        """Get path to virtual host's directory of domain"""
        vhost_name = domain.encode('idna')
        if self.is_windows():
            return windows_utils.path_join(self.vhosts_dir, vhost_name)
        else:
            return posixpath.join(self.vhosts_dir, vhost_name)

    def get_vhost_system_dir(self, domain):
        """Get path to system directory directory of domain"""
        vhost_name = domain.encode('idna')
        if self.is_windows():
            return windows_utils.path_join(self.vhosts_dir, 'system', vhost_name)
        else:
            return posixpath.join(self.vhosts_dir, 'system', vhost_name)

    @property
    @cached
    def plesk_dir(self):
        if self.is_windows():
            with self.runner() as runner:
                return plesk_utils.get_windows_plesk_dir(runner)
        else:
            return self._psa_conf_reader.get_product_root_dir()

    @property
    @cached
    def websrvmng_bin(self):
        """Get full path to websrvmng Plesk utility

        :rtype: unicode
        """
        if not self.is_windows():
            raise NotImplementedError()

        if self.get_plesk_version() >= (12, 0) and self.is_64_bit_os:
            return u'{plesk_dir}\\admin\\bin64\\websrvmng'.format(plesk_dir=self.plesk_dir)
        else:
            return u'{plesk_dir}\\admin\\bin\\websrvmng'.format(plesk_dir=self.plesk_dir)

    @property
    @cached
    def is_64_bit_os(self):
        """Check if this is 64-bit OS or 32-bit one

        :rtype: bool
        """
        if not self.is_windows():
            raise NotImplementedError()

        try:
            with self.runner() as runner:
                # Get architecture from registry.
                # Don't try to get it from environment variable directly, as migrator is running 32-bit version
                # of Python, and Windows sets environment variable accordingly, and you always get 'x86'.
                # Also, avoid using 'wmic os get osarchitecture', as it does not work on Windows 2003.
                return '64' in get_from_registry(
                    runner, [r'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'],
                    'PROCESSOR_ARCHITECTURE'
                )
        except Exception as e:
            logger.exception()
            logger.ferror(messages.FAILED_TO_DETECT_ARCH, server=self.description(), reason=e)
            return True

    @property
    @cached
    def data_dir(self):
        if self.is_windows():
            with self.runner() as runner:
                return plesk_utils.get_windows_data_dir(runner)
        else:
            return self._psa_conf_reader.get_product_root_dir()

    @property
    @cached
    def mail_dir(self):
        if self.is_windows():
            raise NotImplementedError()
        else:
            return self._psa_conf_reader.get_mailnames_dir()

    @property
    @cached
    def dump_dir(self):
        if self.is_windows():
            with self.runner() as runner:
                dump_dir = plesk_utils.get_windows_dump_dir(runner)
                if not dump_dir:
                    # Fallback to %plesk_dir%\Backup
                    dump_dir = ntpath.join(self.plesk_dir, u'Backup')
                return dump_dir
        else:
            return self._psa_conf_reader.get_dump_dir()

    @property
    @cached
    def mailman_root_dir(self):
        if self.is_windows():
            raise NotImplementedError()
        else:
            return self._psa_conf_reader.get_mailman_root_dir()

    @property
    @cached
    def tomcat_dir(self):
        if self.is_windows():
            with self.runner() as runner:
                assert isinstance(runner, BaseRunner)
                # The most recent Plesk versions have applications at %PLESK_DIR%\var\tomcat
                path = ntpath.join(self.plesk_dir, u'var\\tomcat')
                if runner.file_exists(path):
                    return path
                # Previous Plesk versions (e.g. 10.4) have applications at %PLESK_DIR\Additional\Tomcat.
                return ntpath.join(self.plesk_dir, u'Additional\\Tomcat')
        else:
            return self._psa_conf_reader.get_tomcat_dir()

    @cached
    def get_plesk_version(self):
        """Return a tuple with Plesk version

        :rtype: tuple[int]
        """
        with self.runner() as runner:
            if self.is_windows():
                plesk_version = plesk_utils.get_plesk_version_windows(runner, self.plesk_dir)
            else:
                plesk_version = plesk_utils.get_plesk_version_unix(runner, self.plesk_dir)
        return migrator_utils.version_to_tuple(plesk_version)

    @property
    def plesk_version(self):
        """Plesk version as a tuple

        :rtype: tuple[int]
        """
        return self.get_plesk_version()

    @property
    def plesk_version_str(self):
        """Plesk version as a string

        :rtype: str
        """
        return '.'.join([str(x) for x in self.plesk_version])

    @property
    @cached
    def rsync_bin(self):
        with self.runner() as runner:
            return migrator_utils.detect_rsync_path(
                runner, self.description()
            )

    @property
    @cached
    def is_mailserver_qmail(self):
        if self.is_windows():
            return False
        else:
            with self.runner() as runner:
                assert isinstance(runner, BaseRunner)
                return 'qmail' in runner.execute_command(
                    'plesk sbin mailmng --features | grep SMTP_Server'
                ).stdout.lower()

    def join_path(self, *p):
        if self.is_windows():
            return ntpath.join(*p)
        return posixpath.join(*p)

    def get_extensions_var_dir(self):
        return self.join_path(self.data_dir, 'var', 'modules')

    def get_extension_var_dir(self, extension_name):
        """Retrieve absolute path to var directory of given extension

        :type extension_name: str
        :rtype: str
        """
        return self.join_path(self.get_extensions_var_dir(), extension_name)

    def get_bin_util_path(self, name):
        if self.is_windows():
            return ntpath.join(self.plesk_dir, 'bin', name)
        else:
            return posixpath.join(self.plesk_dir, 'bin', name)

    def get_admin_bin_util_path(self, name):
        if self.is_windows():
            return ntpath.join(self.plesk_dir, 'admin', 'bin', name)
        else:
            return posixpath.join(self.plesk_dir, 'admin', 'bin', name)

    @cached
    def get_db_servers(self):
        return {
            result.data.id: result.data
            for result in self.plesk_api().send(plesk_ops.DbServerOperator.Get(plesk_ops.DbServerOperator.FilterAll()))
        }

    def has_database_server(self, server_host, server_type):
        for database_server in self.get_db_servers().values():
            if database_server.host == server_host and database_server.dbtype == server_type:
                return True
        return False

    def get_database_server(self, server_host, server_type):
        for database_server in self.get_db_servers().values():
            if database_server.host == server_host and database_server.dbtype == server_type:
                return database_server
        return None

    def get_default_database_server(self, server_type):
        for database_server in self.get_db_servers().values():
            if database_server.dbtype == server_type and database_server.default:
                return database_server
        return None

    @cached
    def get_all_ips(self, global_context):
        """Get list of all IP addresses of Plesk server

        Information includes IP address, its type and corresponding public IP address.
        """
        if self.get_plesk_version() >= (12, 0):
            return get_plesk_ips_with_cli(self)
        else:
            # on old Plesk versions, 'ipmanage' utility returns incomplete set of IP addresses,
            # fallback to using Plesk API
            return get_plesk_ips_with_api(self)

    def query_panel_db(self, query_str, query_args=None):
        """Execute SQL query on Plesk database ('psa'), return results as list of dictionaries

        :type query_str: str | unicode
        :type query_args: dict | None
        :rtype: list[dict]
        """
        raise NotImplementedError()

    @property
    @cached
    def _psa_conf_reader(self):
        return PSAConfReader(self)


class PSAConfReader(object):
    def __init__(self, server):
        """
        :type server: parallels.core.connections.plesk_server.PleskServer 
        """
        self._server = server

    def get_vhosts_dir(self):
        """Get directory with virtual hosts on Plesk for Unix server

        :rtype: str | unicode
        """
        return self._get_var(
            ('HTTPD_VHOSTS_D',), messages.DEBUG_UNIX_VHOSTS_DIRECTORY
        )

    def get_mailnames_dir(self):
        """Get directory with mail messages on Plesk for Unix server

        :rtype: str | unicode
        """
        return self._get_var(
            ('PLESK_MAILNAMES_D', 'QMAIL_MAILNAMES_D'), messages.DEBUG_MAIL_MESSAGES_DIRECTORY
        )

    def get_dump_dir(self):
        """Get directory where Plesk stores backups on Plesk for Unix server

        :rtype: str | unicode
        """
        return self._get_var(
            ('DUMP_D',), messages.DEBUG_BACKUP_DUMPS_DIRECTORY
        )

    def get_product_root_dir(self):
        """Get directory where Plesk for Unix is installed

        :rtype: str | unicode
        """
        return self._get_var(
            ('PRODUCT_ROOT_D',), messages.DEBUG_UNIX_PRODUCT_ROOT_DIRECTORY
        )

    def get_mailman_root_dir(self):
        """
        :rtype: str | unicode
        """
        return self._get_var(
            ('MAILMAN_ROOT_D',), messages.DEBUG_UNIX_MAILMAN_ROOT_DIRECTORY
        )

    def get_tomcat_dir(self):
        """
        :rtype: str | unicode
        """
        return self._get_var(
            ('CATALINA_HOME',), messages.DEBUG_TOMCAT_DIRECTORY
        )

    @cached
    def _get_var(self, var_names, description=None):
        conf_variables = self._get_psa_conf_vars()
        for var_name in var_names:
            if var_name in conf_variables:
                var_value = conf_variables[var_name]

                if description is not None:
                    logger.fdebug(
                        '{description} on {server}: {value}',
                        description=description, server=self._server.description(), value=var_value
                    )

                return var_value

        return ''

    @cached
    def _get_psa_conf_vars(self):
        conf_variables = {}

        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            stdout = runner.execute_command(
                '/bin/cat /etc/psa/psa.conf', execution_options=ExecutionOptions(log_output=False)
            ).stdout

        for line in stdout.split(b'\n'):
            line = line.strip()

            if line == '':
                continue

            if line.startswith(b'#'):
                continue

            name, _, value = line.replace(b'\t', b' ').partition(b' ')

            name = name.strip()
            value = value.strip().rstrip(b'/')

            if sys.version_info.major >= 3:
                name = name.decode('utf-8')
                value = value.decode('utf-8')
            conf_variables[name] = value

        return conf_variables
