"""Utility functions to transfer settings of virtual directories"""

from xml.etree import ElementTree

from parallels.core.utils.common.xml import elem
from parallels.core.utils.common.xml import replace_root_node_if_name_matches


class VirtualDirectoryXMLConverter(object):
    """Converts virtual directory XML file to get ready for import

    Could change paths in XML according to target layout,
    remove reserved virtual directories
    (virtual directories that should not be transferred)
    and replace "<vhost>" node with "<vdir>" node so XML
    could be passed to Plesk CLI utilities
    """
    def __init__(self):
        self.reserved_vdirs = None
        self.path_convert_function = None

    def set_reserved_vdirs(self, reserved_vdirs):
        self.reserved_vdirs = reserved_vdirs

    def set_path_convert_function(self, path_convert_function):
        self.path_convert_function = path_convert_function

    def convert(self, subscription, site, xml_str):
        vdir_xml = ElementTree.XML(xml_str)
        vdir_xml = self._replace_vhost_with_vdir(vdir_xml)

        if self.path_convert_function is not None:
            self._convert_paths(subscription, site, vdir_xml, self.path_convert_function)
        if self.reserved_vdirs is not None:
            self._remove_reserved_vdirs(vdir_xml)
        self._remove_duplicate_index_docs(vdir_xml)

        return ElementTree.tostring(vdir_xml, 'utf-8', 'xml')

    def _remove_duplicate_index_docs(self, vdir_xml):
        """Remove duplicate index documents, for example "Index.cnf" and "index.cnf".

        Such documents make no sense and may cause issue when restoring virtual directories.
        Error message looks like:
        Cannot add duplicate collection entry of type 'add' with unique key attribute 'value' set to 'index.cfm'
        """
        def remove_duplicate_recursive(node):
            if node.tag in ['vdir', 'vhost'] and 'defaultDoc' in node.attrib:
                node.attrib['defaultDoc'] = ",".join(
                    self._remove_list_duplicates_case_insensitive(
                        [item.strip() for item in node.attrib['defaultDoc'].split(",")]
                    )
                )
            for child in node:
                remove_duplicate_recursive(child)

        remove_duplicate_recursive(vdir_xml)

    @staticmethod
    def _remove_list_duplicates_case_insensitive(lst):
        """Take a list and make its elements unique, case insensitive, preserving order.
        The first one duplicate item is left, all the others are removed.

        :param lst: list[str | unicode]
        :return: list[str | unicode]
        """
        result = []
        added_set = set()
        for item in lst:
            if item.lower() not in added_set:
                result.append(item)
                added_set.add(item.lower())

        return result

    @staticmethod
    def _convert_paths(subscription, site, xml, path_convert_function):
        """

        Arguments:
        - path_convert_function - function with signature path_convert_function(subscription, site, vdir_path),
        where vdir_path is a path of a virtual directory in source virtual directory XML. The function should return
        corresponding path to the provided directory on the target server.
        """

        # logic of XML conversion is taken from Plesk's RestoreVirtualDirectories function at
        # windows/binary/plesk-utils/PMM/psadumpagent/RestoreEnv.cs
        # considering some things in Plesk's code are already not necessary or look buggy

        def fix_path_recursive(node):
            if node.tag in ['vdir', 'vhost'] and 'path' in node.attrib:
                node.attrib['path'] = path_convert_function(
                    subscription,
                    site,
                    node.attrib['path'],
                )
            for child in node:
                fix_path_recursive(child)

        fix_path_recursive(xml)

    def _remove_reserved_vdirs(self, vdir_xml):
        """Remove information about auto-generated virtual directories"""
        vdirs_node = vdir_xml.find('vdirs')
        if vdirs_node is not None:
            for reserved_vdir in self.reserved_vdirs:
                nodes_to_remove = list(vdirs_node.findall('vdir[@name="%s"]' % (reserved_vdir,)))
                for node_to_remove in nodes_to_remove:
                    vdirs_node.remove(node_to_remove)

    @staticmethod
    def _replace_vhost_with_vdir(vdir_xml):
        """In the vhost description xml file, replace vhost element with vdir element.
        """
        root_vdir = elem('vdir')
        for k, v in vdir_xml.attrib.items():
            root_vdir.attrib[k] = v
        # name of the root vdir node should be an empty string - that is a requirement of Plesk
        root_vdir.attrib['name'] = ''

        return replace_root_node_if_name_matches(vdir_xml, 'vhost', root_vdir)


def get_vdir_to_path(vdirs_xml_str):
    """Read virtual directories XML, compose dictionary: virtual directory name -> path

    :type vdirs_xml_str: str | unicode
    :rtype: dict[str | unicode, str | unicode]
    """
    vdir_xml = ElementTree.XML(vdirs_xml_str.encode('utf-8'))
    vdirs_dict = {}
    for node in vdir_xml:
        vdirs_dict.update(
            _get_node_vdir_to_path(node, None)
        )
    return vdirs_dict


def _get_node_vdir_to_path(node, parent_name):
    """Read XML node from virtual directories XML, and compose dictionary: virtual directory name -> path

    :type parent_name: str | unicode | None
    :rtype: dict[str | unicode, str | unicode]
    """
    vdirs_dict = {}
    if node.tag == 'vdir' and 'name' in node.attrib:
        if parent_name is not None:
            vdir_name = parent_name + '\\' + node.attrib['name']
        else:
            vdir_name = node.attrib['name']
        vdirs_dict[vdir_name] = node.attrib.get('path')
    else:
        vdir_name = parent_name

    for child in node:
        vdirs_dict.update(
            _get_node_vdir_to_path(child, vdir_name)
        )

    return vdirs_dict

