from xml.etree import ElementTree

import parallels.core.migrator
import parallels.plesk.source.helm3
from parallels.core.logging import get_logger
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.plesk.source.helm3 import messages
from parallels.core.utils.common import cached, default
from parallels.core.utils.migrator_utils import get_package_extras_file_path
from parallels.core import MigrationError

logger = get_logger(__name__)


class Helm3Agent(object):

    def __init__(self, server):
        self.server = server
        self._is_deployed_helm3_agent = False

    def run(self, args=None, redirect_output_file=None):
        args = default(args, [])
        self._deploy_helm3_agent_lazy()
        with self.server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            command_args = dict(
                agent_bin=self.server.get_session_file_path("Helm3MigrationAgent.exe")
            )
            command_str = '{agent_bin}'
            for i, arg in enumerate(args):
                command_str += ' {arg%i}' % i
                command_args['arg%i' % i] = arg

            return runner.execute_command(
                command_str, command_args, ExecutionOptions(redirect_output_file=redirect_output_file)
            ).stdout.strip()

    def create_shallow_dump(self, filename):
        """Create shallow dump and put it to specified file

        :type filename: str | unicode
        """
        remote_shallow_dump_filename = self.server.get_session_file_path('shallow-dump.xml')
        self.run(['--create-shallow-dump', remote_shallow_dump_filename])
        with self.server.runner() as runner:
            runner.get_file(remote_shallow_dump_filename, filename)

    @cached
    def get_servers_info(self):
        """Get information about servers in Helm cluster

        :rtype: parallels.plesk.source.helm3.helm3_agent.SourceServersInfo
        """
        try:
            result_xml = self.run(["--get-servers-info"])
            return SourceServersInfo(result_xml.strip())
        except Exception:
            logger.exception()
            raise MigrationError(messages.CANNOT_GET_INFO_ABOUT_SERVERS)

    @cached
    def get_domain_diskspace_usage(self, subscription_name, service_type):
        """Get disk usage of subscription's service from Helm database

        :type subscription_name: str | unicode
        :type service_type: str | unicode
        :rtype: float
        """
        try:
            return float(
                self.run(["--get-diskspace-usage", subscription_name, service_type])
            )
        except Exception:
            logger.exception()
            raise MigrationError(
                messages.CANNOT_GET_DISK_USAGE_FOR_SUBSCRIPTION % subscription_name
            )

    @cached
    def is_fake_domain(self, subscription_name):
        """Check whether subscription is fake (does not actually exist, needed for migration only

        :type subscription_name: str | unicode
        :rtype: bool
        """
        try:
            return self.run(["--is-fake-domain", subscription_name]) == "True"
        except Exception:
            logger.exception()
            raise MigrationError(
                messages.CANNOT_DETECT_IF_SUBSCRIPTION_IS_FAKE % subscription_name
            )

    def _get_helm_agent_executable(self):
        """
        :rtype: str | unicode
        """
        self._deploy_helm3_agent_lazy()
        return self.server.get_session_file_path("Helm3MigrationAgent.exe")

    @cached
    def get_service_ips(self, domain_name, service_type_id):
        """Return all IP addresses of a server where specified service of domain is located

        :type domain_name: str | unicode
        :type service_type_id: str | unicode
        :rtype: list[str | unicode]
        """
        try:
            service_ips = []

            with self.server.runner() as runner:
                assert isinstance(runner, BaseRunner)
                get_service_ips_result = runner.execute_command(
                    "{helm3_agent_executable} --get-service-ips {domain_name} {service_type_id}",
                    dict(
                        helm3_agent_executable=self._get_helm_agent_executable(),
                        domain_name=domain_name,
                        service_type_id=service_type_id
                    ),
                    ExecutionOptions(ignore_exit_code=True)
                )

            if get_service_ips_result.exit_code == 1:
                return []
            elif get_service_ips_result.exit_code != 0:
                raise MigrationError(
                    messages.CANNOT_GET_IP_ADDRESSES_FOR_SUBSCRIPTION_SERVICE_IPS_COMMAND % (
                        domain_name, get_service_ips_result.exit_code
                    )
                )
            xml_service_ips = ElementTree.fromstring(get_service_ips_result.stdout.strip())
            for ip in xml_service_ips.findall("ip-address"):
                service_ips.append(ip.attrib['ip'])

            return service_ips
        except NoServiceIpHelmDatabase:
            raise
        except Exception:
            logger.exception()
            raise MigrationError(
                messages.CANNOT_GET_IP_ADDRESSES_FOR_SUBSCRIPTION % domain_name
            )

    @cached
    def get_vhosts_dir_source(self, server):
        """Directory where virtual hosts reside on source"""
        try:
            vhost_dir = self.run(["--get-vhost-dir", server.ip()])
            if vhost_dir == "":
                return None
            else:
                return vhost_dir
        except Exception:
            logger.exception()
            raise MigrationError(
                messages.CANNOT_DETECT_SOURCE_VHOST_DIR % server.ip()
            )

    def _deploy_helm3_agent_lazy(self):
        """Deploy Helm 3 migration agent to the source server, if it was not deployed yet in the current execution

        :rtype: None
        """
        if not self._is_deployed_helm3_agent:
            with self.server.runner() as runner:
                helm_scripts = [
                    "Helm3MigrationAgent.exe", "Helm3Agent.dll",
                    "Helm3CompatibilityChecker.exe", "Helm3CompatibilityChecker.html",
                    "ForeignMigratorCoreCommon.dll", "ForeignMigratorCoreInterfaces.dll",
                    "psa.idna.dll",    "psabackupcommon.dll", "psadumpagent.dll",
                    "psadumpschema.dll", "PsaFactory.dll", "adodb.dll"
                ]
                for script_name in helm_scripts:
                    remote_script_path = self.server.get_session_file_path(script_name)
                    runner.upload_file(
                        get_package_extras_file_path(parallels.plesk.source.helm3, script_name),
                        remote_script_path
                    )
                self._is_deployed_helm3_agent = True


class SourceServersInfo(object):
    """Information about servers registered in Helm"""

    def __init__(self, servers_info_xml):
        """Class constructor

        Servers info XML is a string with XML returned by "get-servers-info" command of Helm 3 agent.

        :type servers_info_xml: str | unicode
        """
        self._xml = ElementTree.fromstring(servers_info_xml)

    def get_server_ips_by_host_or_ip(self, host_or_ip):
        """Get all IP addresses of a server by its hostname or any of its IP addresses

        If no such server is found, function returns None.

        :type host_or_ip: str | unicode
        :rtype: list[str | unicode]
        """
        for server_node in self._xml.findall('server'):
            ips = []
            for ip_node in server_node.findall('ip-addresses/ip-address'):
                ips.append(ip_node.text)

            if server_node.findtext('name') == host_or_ip:
                return ips

            if host_or_ip in ips:
                return ips

        return None

    def get_server_name_by_ip(self, ip):
        """Get server name (as it is displayed in Helm panel by any of its IP addresses, registered in Helm

        If no such server is found, function returns None.

        :type ip: str | unicode
        :rtype: str | unicode | None
        """
        for server_node in self._xml.findall('server'):
            for ip_node in server_node.findall('ip-addresses/ip-address'):
                if ip_node.text == ip:
                    return server_node.findtext('name')

        return None

    def get_all_server_ips_by_ip(self, ip):
        """Get all IP addresses of Helm server by one IP address

        If no such server is found, function returns None.

        :type ip: str | unicode
        :rtype: list[str | unicode] | None
        """
        for server_node in self._xml.findall('server'):
            server_ips = []
            for ip_node in server_node.findall('ip-addresses/ip-address'):
                server_ips.append(ip_node.text)
            if ip in server_ips:
                return server_ips

        return None


class NoServiceIpHelmDatabase(MigrationError):
    pass
