import json
import ntpath
import posixpath
from collections import OrderedDict
from xml.etree import ElementTree
import re

from parallels.core.thirdparties import pymysql
from parallels.core.logging import get_logger
from parallels.core import messages
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.utils.common.ip import resolve
from parallels.core.utils.entity import Entity
from parallels.core.utils.plesk_cli_runner import PleskCLIRunnerCLI
from parallels.core.utils.windows_utils import path_join as windows_path_join, get_from_registry
from parallels.core.utils.common import if_not_none, cached, is_empty, safe_format
from parallels.plesk.utils.xml_rpc.plesk.operator import IpOperator
from parallels.plesk.utils.xml_rpc.plesk.operator import ServerOperator

logger = get_logger(__name__)


def get_windows_vhosts_dir(runner):
    vhosts_dir = get_from_registry(
        runner,
        ['HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'],
        'plesk_vhosts'
    )

    if vhosts_dir is None:
        raise Exception(messages.FAILED_DETECT_PLESK_VIRTUAL_HOSTS_DIRECTORY)

    vhosts_dir = vhosts_dir.strip('\\')
    logger.debug(messages.PLESK_VIRTUAL_HOSTS_DIRECTORY_IS_S % vhosts_dir)
    return vhosts_dir


def get_windows_plesk_dir(runner):
    plesk_dir = get_from_registry(
        runner,
        ['HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'],
        'plesk_dir'
    )

    if plesk_dir is None:
        raise Exception(messages.FAILED_DETECT_PLESK_BASE_DIRECTORY)

    plesk_dir = plesk_dir.strip('\\')
    logger.debug(messages.DEBUG_PLESK_BASE_DIRECTORY, plesk_dir)
    return plesk_dir


def get_windows_util_path(runner, util):
    plesk_dir = get_windows_plesk_dir(runner)
    admin_bin64_dir = ntpath.join(plesk_dir, 'admin', 'bin64')
    admin_bin_dir = ntpath.join(plesk_dir, 'admin', 'bin')
    bin_dir = ntpath.join(plesk_dir, 'bin')

    result_path = ntpath.join(admin_bin64_dir, util)
    if not ntpath.exists(result_path):
        result_path = ntpath.join(admin_bin_dir, util)

    if not ntpath.exists(result_path):
        result_path = ntpath.join(bin_dir, util)

    if not ntpath.exists(result_path):
        raise Exception(safe_format(
            messages.UTILITY_NOT_FOUND,
            util=util,
            admin_bin64_dir=admin_bin64_dir,
            admin_bin_dir=admin_bin_dir,
            bin_dir=bin_dir
        ))

    logger.fdebug(messages.DEBUG_PLESK_UTIL_PATH, util=util, path=result_path)
    return result_path


def get_windows_data_dir(runner):
    possible_registry_keys = [
        'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
        'HKLM\SOFTWARE\PLESK\PSA Config\Config'
    ]
    data_dir = get_from_registry(runner, possible_registry_keys, 'PRODUCT_DATA_D')

    if data_dir is None:
        raise Exception(messages.FAILED_DETECT_PLESK_DATA_DIRECTORY)

    data_dir = data_dir.strip('\\')
    logger.debug(messages.DEBUG_PLESK_DATA_DIRECTORY, data_dir)
    return data_dir


def get_windows_psa_database_name(runner):
    """Get name of Plesk panel database ('psa')

    :type runner: parallels.core.runners.base.BaseRunner
    :rtype: str | unicode
    """
    name = get_from_registry(
        runner, [
            'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
            'HKLM\SOFTWARE\PLESK\PSA Config\Config'
        ], 'mySQLDBName'
    )
    if is_empty(name):
        name = 'psa'
    return name


def get_windows_psa_database_login(runner):
    """Get user name to log into Plesk panel database ('psa')

    :type runner: parallels.core.runners.base.BaseRunner
    :rtype: str | unicode
    """
    login = get_from_registry(
        runner, [
            'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
            'HKLM\SOFTWARE\PLESK\PSA Config\Config'
        ], 'PLESK_DATABASE_LOGIN'
    )
    if is_empty(login):
        login = 'admin'
    return login


def get_windows_psa_database_port(runner):
    """Get TCP port to connect to Plesk panel database ('psa')

    :type runner: parallels.core.runners.base.BaseRunner
    :rtype: int
    """
    default_port = 8306
    port = get_from_registry(
        runner, [
            'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
            'HKLM\SOFTWARE\PLESK\PSA Config\Config'
        ], 'MySQL_DB_PORT'
    )
    if is_empty(port):
        port = get_from_registry(
            runner, [
                'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
                'HKLM\SOFTWARE\PLESK\PSA Config\Config'
            ], 'MYSQL_PORT'
        )
    if is_empty(port):
        return default_port

    try:
        return int(port)
    except ValueError:
        return default_port


def get_windows_psa_database_host(runner):
    """Get hostname to connect to Plesk panel database ('psa')

    :type runner: parallels.core.runners.base.BaseRunner
    :rtype: str | unicode
    """
    login = get_from_registry(
        runner, [
            'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
            'HKLM\SOFTWARE\PLESK\PSA Config\Config'
        ], 'MYSQL_DB_HOST'
    )
    if is_empty(login):
        login = 'localhost'
    return login


def get_windows_dump_dir(runner):
    possible_registry_keys = [
        'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
        'HKLM\SOFTWARE\PLESK\PSA Config\Config'
    ]
    dump_dir = get_from_registry(runner, possible_registry_keys, 'DUMP_D')

    dump_dir = dump_dir.strip('\\')
    logger.debug(messages.DEBUG_PLESK_DUMP_DIRECTORY, dump_dir)
    return dump_dir


def get_windows_temp_dump_dir(runner):
    possible_registry_keys = [
        'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
        'HKLM\SOFTWARE\PLESK\PSA Config\Config'
    ]
    dump_dir = get_from_registry(runner, possible_registry_keys, 'DumpTempDir')

    dump_dir = dump_dir.strip('\\')
    logger.debug(messages.DEBUG_PLESK_DUMP_DIRECTORY, dump_dir)
    return dump_dir


def get_windows_vhost_dir(runner, vhost_name):
    vhosts_dir = get_windows_vhosts_dir(runner)

    vhost_name = vhost_name.encode('idna')
    vhost_dir = windows_path_join(vhosts_dir, vhost_name)
    logger.debug(messages.DEBUG_WINDOWS_VHOST_DIRECTORY, vhost_dir)

    return vhost_dir


def get_windows_db_server_credentials(server, db_type, db_host, db_port):
    def _get_credentials(_server, _db_type, _db_host):
        execution_results = _call_php_cli_unchecked(_server, 'cu\database-registrar.php', [
            '--get-credentials', _db_host, '-type', _db_type
        ], log_output=False)
        if execution_results.exit_code != 0:
            return None
        return execution_results.stdout.strip()

    output = None
    if db_type == 'mssql' and db_host.startswith('.\\') and ':' not in db_host:
        # workaround for bug PPP-15779: port 0 specified in Plesk database for local MSSQL server,
        # installer automatically, so first try to check this case
        output = _get_credentials(server, db_type, '%s:0' % db_host)
    if output is None:
        if not is_empty(db_port) and str(db_port) != '0':
            db_host = '%s:%s' % (db_host, db_port)
        output = _get_credentials(server, db_type, db_host)

    if output is None:
        raise Exception(messages.UNABLE_RETRIEVE_CREDENTIALS_FOR_S_DATABASE % (
            db_type, db_host, server.description()
        ))

    xml = ElementTree.XML(output)
    return xml.findtext('username'), xml.findtext('password')


def get_unix_db_server_credentials(server, db_type, db_host, db_port):
    if not is_empty(db_port) and str(db_port) != '0':
        db_host = '%s:%s' % (db_host, db_port)
    output = _call_php_cli(server, 'api-cli/database-registrar.php', [
        '--get-credentials', db_host, '-type', db_type
    ], log_output=False).strip()
    xml = ElementTree.XML(output)
    return xml.findtext('username'), xml.findtext('password')


def check_capability(plesk_server, input_path, output_path):
    """
    Run check that given Plesk server is capable to migrate listed objects and provide report with detected issues
    :type plesk_server: parallels.plesk.connections.target_server.PleskTargetServer
    :param str input_path: path to capability dump
    :param output_path: path to report generated by Plesk capability checker
    """
    _call_php_cli(plesk_server, 'backup/Conflicts/Runner.php', [
        '--check-capability',
        '--session-path=%s' % plesk_server.get_session_dir_path(),
        '--log=conflict-resolve.log',
        '--capability-info=%s' % input_path,
        '--capability-info-out=%s' % output_path,
        '--used-space-coefficient=0.7',
        '--max-transfer-download-time=86400',
        '--min-transfer-download-speed=1.25'
    ])


def convert_wildcard_to_path(domain_name):
    if domain_name.startswith('*'):
        return '_%s' % (domain_name[1:],)
    else:
        return domain_name


def get_plesk_version_unix(runner, plesk_dir):
    """Return Plesk for Unix version string

    :type runner: parallels.core.runners.base.BaseRunner
    :type plesk_dir: str | unicode
    :rtype: str
    """
    version_str = runner.get_file_contents(posixpath.join(plesk_dir, 'version'))
    return version_str.split()[0]


def get_plesk_version_windows(runner, plesk_dir):
    """Return Plesk for Windows version string

    :type runner: parallels.core.runners.base.BaseRunner
    :type plesk_dir: str | unicode
    :rtype: str
    """
    version_str = runner.get_file_contents(ntpath.join(plesk_dir, 'version'))
    return version_str.split()[0]


def get_migrator_root_path(migrator_module):
    """Get path to package root directory."""
    dirs = [p for p in migrator_module.__path__]
    assert all(d == dirs[0] for d in dirs)
    return dirs[0]


def recalculate_mailbox_maildir_size_linux(runner, domain_name, mailname, disk_quota):
    """Recalculate maildir size on single mailbox on Plesk for Linux server

    :type runner: parallels.core.runners.base.BaseRunner
    :type domain_name: str | unicode
    :type mailname: str | unicode
    :type disk_quota: str | unicode | int | None
    """
    jsonquery = json.dumps(
        OrderedDict([
            (
                'domain',
                OrderedDict([
                    ('name', domain_name.encode('idna')),
                    ('dom_aliases', []),
                    ('mails', []),
                    ('maillists', [])
                ])
            ),
            (
                'certificates', []
            )
        ])
    )
    # See JSON schema specified in Plesk GIT, file 'mail_db_data.sj'
    runner.execute_command(
        '/usr/local/psa/admin/bin/mailmng-core --set-mailbox-disk-quota '
        '--domain-name={domain_name} --mailname={mailbox_name} --disk-quota={quota}',
        dict(
            domain_name=domain_name.encode('idna'),
            mailbox_name=mailname,
            quota=str(disk_quota) if disk_quota is not None else '-1',
        ),
        ExecutionOptions(stdin_content=jsonquery.encode('utf-8'))
    )


def update_mail_usage_stats(runner, subscription_name):
    """Update mail usage stats in control panel

    :type runner: parallels.core.runners.base.BaseRunner
    :type subscription_name: str | unicode
    """
    runner.execute_command('plesk bin mail --recalculate-mailboxes-usage {subscription_name}', dict(
        subscription_name=subscription_name
    ))


def set_mime_types(server, vhost_name, mime_types, vdir_name=None):
    """Set MIME types of virtual host on Windows Plesk server

    Arguments:
    - server - instance of Windows target server (should inherit 2 classes:
        - parallels.core.connections.target_servers.TargetServer
        - parallels.core.connections.plesk_server.PleskServer
    )
    - vhost_name - virtual host name (subscription or addon domain)
    - mime_types - dictionary of mime types (key - extension, value - mime type)
    - vdir_name - virtual directory to set mime type on; if None - mime types
        are set on whole server
    """
    vhost_name = vhost_name.encode('idna')

    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        mimetypes_file = None
        if server.plesk_version >= (12, 0):
            mimetypes_file = server.get_session_file_path(
                '%s.mimetypes' % vhost_name
            )
            runner.upload_file_content(
                mimetypes_file,
                _mime_types_as_string(mime_types)
            )
            mimetypes_arg = mimetypes_file
        else:
            mimetypes_arg = _mime_types_as_string(mime_types)

        cmd, args = _get_webservermng_command(
                server.websrvmng_bin,
                'set-mime-types',
                args={
                    'vhost-name': vhost_name,
                    'vdir-name': vdir_name,
                    'mime-types': mimetypes_arg
                }
            )
        runner.execute_command(cmd, args)

        if mimetypes_file is not None:
            runner.remove_file(mimetypes_file)


def get_mime_types(server, vhost_name, vdir_name=None):
    """Get MIME types of virtual host on Windows Plesk server

    Arguments:
    - server - instance of Windows target server (should inherit 2 classes:
        - parallels.core.connections.target_servers.TargetServer
        - parallels.core.connections.plesk_server.PleskServer
    )
    - vhost_name - virtual host name (subscription or addon domain)
    - vdir_name - virtual directory name
    """
    vhost_name = vhost_name.encode('idna')
    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        cmd, args = _get_webservermng_command(
            server.websrvmng_bin,
            'get-mime-types',
            args={
                'vhost-name': vhost_name,
                'vdir-name': vdir_name,
            }
        )
        mime_types_str = runner.execute_command(cmd, args).stdout
    return _parse_mime_types_string(mime_types_str)


def get_error_documents(server, vhost_name, vdir_name=None):
    """Get error documents of virtual host on Windows Plesk server

    Arguments:
    - server - instance of Windows target server (should inherit 2 classes:
        - parallels.core.connections.target_servers.TargetServer
        - parallels.core.connections.plesk_server.PleskServer
    )
    - vhost_name - virtual host name (subscription or addon domain)
    - vdir_name - virtual directory name
    """
    vhost_name = vhost_name.encode('idna')

    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        cmd, args = _get_webservermng_command(
            server.websrvmng_bin,
            'get-error-docs',
            args={
                'vhost-name': vhost_name,
                'vdir-name': vdir_name
            }
        )
        error_documents_str = runner.execute_command(cmd, args).stdout
    return error_documents_str


def get_vdir_info(server, vhost_name):
    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        vdir_info = runner.execute_command('{websrvmng_path} --list-vdirs --vhost-name={vhost_name}', dict(
            websrvmng_path=server.websrvmng_bin, vhost_name=vhost_name.encode('idna')
        )).stdout
    return vdir_info


def _mime_types_as_string(mime_types):
    return u"".join([ 
        u"%s=%s;" % (ext, mime_type) 
        for ext, mime_type in mime_types.items()
    ])


def _parse_mime_types_string(mime_types_string):
    mime_types = {}
    for mime_type_str in mime_types_string.split(';'):
        mime_type_str = mime_type_str.strip()
        if mime_type_str == '':
            continue

        # Old Plesks (8.x, 9.x) return MIME types in a different format than the new ones. For example:
        # - Old Plesk: $mime_types['.AAA']='application/aaa'
        # - New Plesk: .AAA=application/aaa
        match = re.match(r"^\$mime_types\['(.*)'\]='(.*)'$", mime_type_str)
        if match is not None:  # Old Plesk
            ext = match.group(1)
            mime_type = match.group(2)
        else:  # New Plesk
            ext, mime_type = mime_type_str.split('=')
        mime_types[ext] = mime_type
    return mime_types


def _get_webservermng_command(websrvmng_path, action, args):
    result_args = dict(
        websrvmng_path=websrvmng_path,
        action=action
    )

    result_command = '{websrvmng_path} --{action}'
    for key, value in args.items():
        if value is not None:
            result_command += ' --%s={%s}' % (key, key)
            result_args[key] = value

    return result_command, result_args


def convert_wildcard_domain_path(domain_path):
    """Convert wildcard domain path: in domain physical path '*' symbol replaced to '_'.

    Arguments:
    - domain_path - vhost path to domain

    :type domain_path: str | unicode
    """
    return domain_path.replace('*', '_')


def get_apache_restart_interval(server):
    """
    :type server: parallels.core.connections.plesk_server.PleskServer
    """
    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        return int(runner.execute_command(
            '{server_pref_bin} --get-apache-restart-interval',
            dict(
                server_pref_bin=posixpath.join(server.plesk_dir, 'bin/server_pref')
            )
        ).stdout)


def set_apache_restart_interval_value(server, new_value):
    """
    :type server: parallels.core.connections.plesk_server.PleskServer
    :type new_value: str | unicode | None
    """
    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        runner.execute_command(
            '{server_pref_bin} -u -restart-apache {new_value}',
            dict(
                server_pref_bin=posixpath.join(server.plesk_dir, 'bin/server_pref'),
                new_value=new_value
            )
        )


def restart_plesk_apache(plesk_server):
    """
    :type plesk_server: parallels.core.connections.plesk_server.PleskServer
    """
    with plesk_server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        runner.execute_command(
            "{websrvmng_bin} -r", dict(websrvmng_bin=posixpath.join(plesk_server.plesk_dir, 'admin/bin/websrvmng'))
        )


def get_plesk_ips_with_cli(plesk_server):
    """Get information about IP addresses on specified Plesk server with Plesk CLI

    :type plesk_server: parallels.core.connections.plesk_server.PleskServer
    :rtype: list[parallels.core.utils.plesk_utils.IPAddressInfo]
    """
    runner = PleskCLIRunnerCLI(plesk_server)
    out = runner.run('ipmanage', ['--xml-info'])
    xml = ElementTree.XML(
        # we are not interested in non-ascii characters, which could make XML not well-formed
        out.decode('ascii', 'replace').encode('ascii', 'replace')
    )
    ip_addresses = []
    for ip_node in xml.findall('ip'):
        public_ip_address = ip_node.findtext('publicIp')
        if public_ip_address.strip() == '':
            public_ip_address = None
        ip_addresses.append(IPAddressInfo(
            ip_type=ip_node.findtext('type'),
            ip_address=ip_node.findtext('ip_address'),
            public_ip_address=public_ip_address,
            hostings=if_not_none(ip_node.findtext('hostings'), int),
            clients=if_not_none(ip_node.findtext('clients'), int),
            iface=ip_node.findtext('iface'),
            mask=ip_node.findtext('mask')
        ))

    return ip_addresses


def get_plesk_ips_with_api(plesk_server):
    """Get information about IP addresses on specified Plesk server with Plesk API

    :type plesk_server: parallels.core.connections.plesk_server.PleskServer
    :rtype: list[parallels.core.utils.plesk_utils.IPAddressInfo]
    """
    ips = plesk_server.plesk_api().send(IpOperator.Get()).data
    ip_addresses = []

    for ip_response in ips:
        ip_addresses.append(IPAddressInfo(
            ip_type=ip_response.ip_type,
            ip_address=ip_response.ip_address,
            public_ip_address=ip_response.public_ip_address,
            hostings=0  # can't determine with Plesk API, just set to zero
        ))

    return ip_addresses


def refresh_node_components(node):
    """
    :type node: parallels.core.connections.plesk_server.PleskServer
    """
    with node.runner() as runner:
        assert isinstance(runner, BaseRunner)
        runner.execute_command(u'{defpackagemng_bin} --get --force', dict(
            defpackagemng_bin=ntpath.join(node.plesk_dir, u'admin\\bin\\defpackagemng')
        ))


@cached
def get_packages_xml(server):
    """
    :type server: parallels.core.connections.plesk_server.PleskServer
    :rtype: str 
    """
    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        return runner.execute_command(
            u'{defpackagemng_bin} --get',
            dict(defpackagemng_bin=ntpath.join(server.plesk_dir, u'admin\\bin\\defpackagemng')),
        ).stdout


def change_dedicated_iis_app_pool_state(plesk_server, subscription_name, state):
    """
    :param parallels.core.connections.plesk_server.PleskServer plesk_server: Plesk panel server of subscription
    :param unicode subscription_name: subscription (webspace) name
    :param bool state: whether to enable or disaple IIS application poll
    """
    PleskCLIRunnerCLI(plesk_server).run(
        'subscription',
        [
            '--set-iis-app-pool-settings',
            subscription_name.encode('idna'),
            '-iis-app-pool-turned-on',
            'true' if state else 'false'
        ],
        env=dict(PLESK_RESTORE_MODE='1')
    )


class IPAddressInfo(object):
    def __init__(self, ip_type, ip_address, public_ip_address, hostings=0, clients=0, iface=None, mask=None):
        self.ip_type = ip_type
        self.ip_address = ip_address
        self.public_ip_address = public_ip_address if public_ip_address is not None else ip_address
        self.iface = iface
        self.mask = mask

        if hostings is not None:
            self.hostings = hostings
        else:
            self.hostings = 0

        if clients is not None:
            self.clients = clients
        else:
            self.clients = 0

    @property
    def is_completely_free(self):
        """Check if IP address is completely free - not used by anyone

        :rtype: bool
        """
        return self.clients == 0 and self.hostings == 0

    def __repr__(self):
        return "IPAddressInfo(ip_type=%r, ip_address=%r, public_ip_address=%r, hostings=%r, clients=%r, iface=%r)" % (
            self.ip_type, self.ip_address, self.public_ip_address, self.hostings, self.clients, self.iface
        )


@cached
def get_databases_of_server(panel_server, db_type, db_host):
    """Get name of subscription which owns specified database. If database does not exist - return None

    Returns list of dictionaries with keys - 'subscription_name', 'database_name'.

    :type panel_server: parallels.core.connections.plesk_server.PleskServer
    :type db_type: str | unicode
    :type db_host: str | unicode
    :rtype: list[dict]
    """
    query = """
        SELECT domains.name AS subscription_name, data_bases.name AS database_name FROM data_bases
            JOIN DatabaseServers ON data_bases.db_server_id = DatabaseServers.id
            JOIN domains on data_bases.dom_id = domains.id
            WHERE
                DatabaseServers.host = %(db_host)s AND
                DatabaseServers.type = %(db_type)s
    """
    return panel_server.query_panel_db(query, dict(db_type=db_type, db_host=db_host))


@cached
def get_database_users_of_server(panel_server, db_type, db_host):
    """Get name of subscription which owns specified database user. If database user does not exist - return None.

    Returns list of dictionaries with keys - 'subscription_name', 'user_name'.

    :type panel_server: parallels.core.connections.plesk_server.PleskServer
    :type db_type: str | unicode
    :type db_host: str | unicode
    :rtype: dict
    """
    query = """
        SELECT domains.name AS subscription_name, db_users.login AS user_name FROM db_users
            JOIN DatabaseServers ON db_users.db_server_id = DatabaseServers.id
            JOIN domains on db_users.dom_id = domains.id
            WHERE
                DatabaseServers.host= %(db_host)s AND
                DatabaseServers.type = %(db_type)s
    """
    return panel_server.query_panel_db(query, dict(db_type=db_type, db_host=db_host))


def get_domain_dot_net_version(panel_server, domain_name):
    """Get ASP.NET version used by domain

    :type panel_server: parallels.core.connections.plesk_server.PleskServer
    :type domain_name: str | unicode
    :rtype: tuple[int] | None
    """
    query = u"""
        SELECT hosting.managed_runtime_version AS version FROM domains
            JOIN hosting ON domains.id = hosting.dom_id
            WHERE name = '{domain_name}'
    """.format(domain_name=domain_name)
    results = panel_server.query_panel_db(query)
    if len(results) == 0:
        return None
    else:
        if results[0]['version'] is not None and results[0]['version'].strip() != '':
            return tuple(int(v) for v in results[0]['version'].split("."))
        else:
            return None


@cached
def get_server_info_with_key(panel_server):
    """Get information about Plesk server and license key, with the help of Plesk API

    :type panel_server: parallels.core.connections.plesk_server.PleskServer
    :rtype: parallels.plesk.utils.xml_rpc.plesk.operator.server.ServerInfo
    """
    plesk_api = panel_server.plesk_api()
    return plesk_api.send(ServerOperator.Get([
        ServerOperator.Dataset.GEN_INFO, ServerOperator.Dataset.STAT, ServerOperator.Dataset.KEY
    ])).data


def count_subscriptions_and_addon_domains(panel_server):
    """Get number of subscriptions and addon domains on the server. Subdomains and aliases are not counted

    :type panel_server: parallels.core.connections.plesk_server.PleskServer
    :rtype: int
    """
    result_set = panel_server.query_panel_db("SELECT COUNT(*) AS domains_count FROM domains WHERE parentDomainId = 0")
    return int(result_set[0]["domains_count"])


class PHPHandler(Entity):
    def __init__(self, handler_id, version, full_version, handler_type):
        self._handler_id = handler_id
        self._version = version
        self._handler_type = handler_type
        self._full_version = full_version

    @property
    def handler_id(self):
        """Plesk PHP handler ID. Examples: 'plesk-php53-fastcgi', 'plesk-php55-fpm', 'cgi', etc.
        :rtype: str
        """
        return self._handler_id

    @property
    def version(self):
        """Short version, as a string. Examples: '5', '5.3', '5.4'

        :rtype: str | None
        """
        return self._version

    @property
    def full_version(self):
        """Full version, as a string. Examples: '5.3.29', '5.4.44', '5.5.29'
        :rtype: str | None
        """
        return self._full_version

    @property
    def handler_type(self):
        """Plesk PHP handler type (mode in which Plesk PHP is running). Allowed value: 'cgi', 'fastcgi', 'module', 'fpm'

        :rtype: str
        """
        return self._handler_type

    def pretty_str(self):
        """User-friendly string representation of this object. Use in text messages provided to customer.

        :rtype: unicode
        """
        if self.handler_id in ('cgi', 'fastcgi', 'module'):
            title = messages.PHP_HANDLER_TYPE_DEFAULT_OS
        elif self.handler_id.startswith('plesk-'):
            title = messages.PHP_HANDLER_TYPE_DEFAULT_PLESK
        else:
            title = messages.PHP_HANDLER_TYPE_CUSTOM % self.handler_id

        version = self.full_version
        if is_empty(version):
            version = self.version
        if is_empty(version):
            version = messages.PHP_UNKNOWN_VERSION

        mode = {
            'fastcgi': messages.PHP_MODE_FASTCGI,
            'cgi': messages.PHP_MODE_CGI,
            'fpm': messages.PHP_MODE_FPM,
            'module': messages.PHP_MODE_APACHE_MODULE
        }.get(self.handler_type)
        if mode is None:
            if self.handler_type is None:
                return messages.PHP_MODE_UNKNOWN
            else:
                return messages.PHP_MODE_ID_STRING % self.handler_type

        return messages.PHP_DESCRIPTION.format(
            title=title, version=version, mode=mode
        )


def get_php_handlers(server):
    """List PHP handlers available on specified Plesk server

    :type server: parallels.core.connections.plesk_server.PleskServer
    :rtype: list[parallels.core.utils.plesk_utils.PHPHandler]
    """
    runner = PleskCLIRunnerCLI(server)
    raw_handlers = json.loads(runner.run('php_handler', ['--list-json']).decode('utf-8'))
    handlers = []
    for raw_handler in raw_handlers:
        if raw_handler.get('status') == 'broken':
            continue
        handlers.append(PHPHandler(
            handler_id=raw_handler.get('id'),
            version=raw_handler.get('version'),
            full_version=raw_handler.get('fullVersion'),
            handler_type=raw_handler.get('type')
        ))
    return handlers


def is_auxiliary_user_role_exists(server, owner, role):
    """Check whether specified auxiliary user role exists on the server

    :type server: parallels.core.connections.plesk_server.PleskServer
    :type owner: str | unicode | None
    :type role: str | unicode
    :rtype: bool | None
    """
    result_set = server.query_panel_db("""
        SELECT COUNT(*) AS roles_count FROM smb_roles
            JOIN clients ON smb_roles.ownerId = clients.id
            WHERE smb_roles.name = %(role)s and clients.login = %(client)s
    """, dict(role=role, client=owner if owner is not None else 'admin'))
    if len(result_set) == 0:
        return None
    else:
        return int(result_set[0]["roles_count"]) >= 1


def get_subscription_count(server):
    """Get count of subscriptions on specified Plesk server

    :type server: parallels.core.connections.plesk_server.PleskServer
    :rtype: int
    """
    lines = server.query_panel_db("SELECT COUNT(*) AS domains_count FROM domains WHERE webspace_id = 0")
    return int(lines[0]["domains_count"])


def get_client_count(server):
    """Get count of clients on specified Plesk server

    :type server: parallels.core.connections.plesk_server.PleskServer
    :rtype: int
    """
    lines = server.query_panel_db("SELECT COUNT(*) AS clients_count FROM clients WHERE type = 'client'")
    return int(lines[0]["clients_count"])


@cached
def get_plesk_db_connection_args(plesk_server):
    """Retrieve arguments required to connect to given Plesk database

    :type plesk_server: parallels.core.connections.plesk_server.PleskServer
    :rtype: dict
    """
    if plesk_server.is_windows():
        with plesk_server.runner() as runner:
            return dict(
                user=get_windows_psa_database_login(runner),
                database=get_windows_psa_database_name(runner),
                charset='utf8',
                host=get_windows_psa_database_host(runner),
                port=get_windows_psa_database_port(runner),
                password=plesk_server.panel_admin_password,
                cursorclass=pymysql.cursors.Cursor
            )
    else:
        return dict(
            user='admin',
            database='psa',
            charset='utf8',
            host='localhost',
            password=_read_unix_password(plesk_server),
            # Need to specify socket file in explicit way: the library does
            # not detect it automatically, and has problems parsing default my.cnf
            # file on CentOS 7. So, we use "mysql" command line client to detect
            # path of the socket file.
            unix_socket=_get_mysql_socket(plesk_server),
            cursorclass=pymysql.cursors.Cursor
        )


def is_valid_protected_directory_name(is_windows, directory_name):
    """Check if specified protected directory name (path) is valid for Plesk

    Directory name validation taken from Plesk code:
    common/php/plib/CommonPanel/Validate/ProtectedDirectory/Name.php

    :type is_windows: bool
    :type directory_name: str | unicode
    :rtype: bool
    """
    if is_windows:
        allowed_pattern = r"^[0-9a-zA-Z_.\/\-~@!\+=\^\(\)\[\]\{\}, ]+$"
        # forbid // and ^. and /. and .$ and ./
        denied_pattern = r"(\/\/|^\.|\.$|\/\.|\.\/|\/ | \/)"
    else:
        allowed_pattern = r"^[0-9a-zA-Z_.\/\-]+$"
        # forbid /../ and // and ^./ and ^../ and /./ and /..$ and /.$ and ^.$
        denied_pattern = r"(\/\.\.\/|\/\/|^\.\/|^\.\.\/|\/\.\/|\/\.\.$|\/\.$|^\.$)"

    filename = directory_name.strip('/').replace('/', '..')

    return (
        re.match(allowed_pattern, directory_name) is not None and
        re.match(denied_pattern, directory_name) is None and
        not len(filename) > 247
    )


def parse_secret_keys(data):
    """Parse output of secret_key --list Plesk utility and return list of secret keys details

    :type data: str
    :rtype: list[dict]
    """
    secret_keys = []
    storage = {}
    for line in data.splitlines():
        # each line of data presented by 'key: value' pair of secret key details
        line_parts = line.split(b':', 1)
        if len(line_parts) < 2:
            # lines of data related to different secret keys separated by empty line, so at this moment
            # all available details related to current secret key was collected
            if 'key' in storage and 'ip' in storage and 'description' in storage:
                # we have enough secret key details
                secret_keys.append(storage.copy())
            # clear secret key details storage and proceed to next line of data
            storage = {}
            continue
        key = line_parts[0].strip().lower()
        value = line_parts[1].strip()
        # store secret key details
        storage[key] = value if value else None
    return secret_keys


def get_remote_smarter_mail_ip(server):
    """Get IP address of remote SmarterMail server. If there is no remote SmarterMail - None is returned.

    :type server: parallels.core.connections.plesk_server.PleskServer
    :rtype: str | unicode | None
    """
    if not server.is_windows() or server.plesk_version < (17, 0):
        return None

    mail_package_table = server.query_panel_db(
        "SELECT value FROM ServiceNodeEnvironment WHERE section = 'mail' AND name = 'SMTP_Server_package'"
    )
    if len(mail_package_table) == 0:
        return None
    mail_package = mail_package_table[0]['value']
    if mail_package != 'smartermail_remote':
        return None

    smarter_mail_host_table = server.query_panel_db(
        "SELECT val FROM misc WHERE param = 'smartermail_remote_host'"
    )
    if len(smarter_mail_host_table) == 0:
        return None

    smarter_mail_host = smarter_mail_host_table[0]['val']
    smarter_mail_ip = resolve(smarter_mail_host)

    return smarter_mail_ip


@cached
def _read_unix_password(server):
    """Read plesk database password on Unix

    :type server: parallels.core.connections.plesk_server.PleskServer
    :rtype: str | unicode
    """
    with server.runner() as runner:
        return runner.get_file_contents('/etc/psa/.psa.shadow').strip()


@cached
def _get_mysql_socket(server):
    """Detect path to Unix socket for connection to Plesk database

    :type server: parallels.core.connections.plesk_server.PleskServer
    :rtype: str | unicode
    """
    default_socket = '/var/lib/mysql/mysql.sock'
    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        query_result = runner.execute_command(
            u'MYSQL_PWD="$(cat /etc/psa/.psa.shadow)" mysql --silent --skip-column-names -u admin psa -e {query}',
            dict(query="SHOW VARIABLES LIKE 'socket'")
        ).stdout
    parts = query_result.split(None, 1)
    if len(parts) == 2:
        return parts[1].strip()
    else:
        # if MySQL client returned something strange - fallback to default, which should work at
        # least on CentOS
        return default_socket


def _call_php_cli(plesk_server, path, args, log_output=True):
    """
    :rtype: str 
    """
    with plesk_server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        command_str, command_args = _get_php_cli_command(plesk_server, path, args)
        return runner.execute_command(command_str, command_args, ExecutionOptions(log_output=log_output)).stdout


def _call_php_cli_unchecked(plesk_server, path, args, log_output=True):
    """
    :rtype: parallels.core.runners.entities.ExecutionResults 
    """
    with plesk_server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        command_str, command_args = _get_php_cli_command(plesk_server, path, args)
        return runner.execute_command(
            command_str, command_args, ExecutionOptions(log_output=log_output, ignore_exit_code=True)
        )


def _get_php_cli_command(plesk_server, path, args):
    if plesk_server.is_windows():
        result_command = u'{php_bin} -dauto_prepend_file="" {script}'
        result_args = dict(
            php_bin=ntpath.join(plesk_server.plesk_dir, u'admin\\bin\\php'),
            script=ntpath.join(plesk_server.plesk_dir, u'admin\\plib', path)
        )
    else:
        result_command = u'{sw_engine_pleskrun_bin} {script}'
        result_args = dict(
            sw_engine_pleskrun_bin=posixpath.join(plesk_server.plesk_dir, u'bin/sw-engine-pleskrun'),
            script=posixpath.join(plesk_server.plesk_dir, u'admin/plib', path)
        )

    for arg_num, arg in enumerate(args):
        arg_key = "arg_%s" % arg_num
        result_command += " {%s}" % arg_key
        result_args[arg_key] = arg

    return result_command, result_args
