"""
Copyright 2016 VMware, Inc.  All rights reserved. -- VMware Confidential
"""

import os
import sys

HOME_DIR = os.path.join(os.sep, 'usr', 'lib', 'vmware-updatemgr')

CONFIG_VERSION_FILE = os.path.join(HOME_DIR, 'config', 'version.txt')

BIN_DIR = os.path.join(HOME_DIR, 'bin')
CONFIG_DIR = BIN_DIR
EXTENTION_XML = os.path.join(BIN_DIR, 'extension.xml')
INTEGRITY_XML = os.path.join(BIN_DIR, "vci-integrity.xml")
INSTALLUTILS_CONFIG_XML = os.path.join(BIN_DIR, "vciInstallUtils_config.xml")
INSTALLUTIL = os.path.join(BIN_DIR, "vmware-vciInstallUtils")
VUM_CONFIG_TXT = os.path.join(BIN_DIR, "configvalues.txt")
JETTY_VUM_SSL_XML = os.path.join(BIN_DIR, "jetty-vum-ssl.xml")

RPMNEW_FILE_POSTFIX = ".rpmnew"
UPDATED_EXTENTION_XML = EXTENTION_XML + RPMNEW_FILE_POSTFIX
UPDATED_INTEGRITY_XML = INTEGRITY_XML + RPMNEW_FILE_POSTFIX

CONFIG_DB_USERNAME_KEY = 'db_user'
CONFIG_DB_PASSWORD_KEY = 'db_password'
CONFIG_DB_TYPE_KEY = 'db_type'
CONFIG_DSN_NAME_KEY = 'db_dsn'

VERSION_SEPARATOR = "."
PATH_SEPARATOR = "/"
PROTOCOLS_TAG = "protocols"
SSL_PROTOCOLS_TAG_PATH = "./vmacore/ssl/protocols"
SSL_TAG_PATH = "./vmacore/ssl"

class Version:
    def __init__(self, versionStr):
        intermediateResult = versionStr.split(VERSION_SEPARATOR)
        self.__versionNums = list(map(int, intermediateResult))

    def __lt__(self, other):
        result = False
        for index, verNo in enumerate(self.__versionNums):
            if verNo < other.__versionNums[index]:
                result = True
                break
            if verNo > other.__versionNums[index]:
                break
        return result

    def __str__(self):
        return VERSION_SEPARATOR.join(str(verNo) for verNo in self.__versionNums)


class XmlMerger:
    """
    The class provides a framework to merge config from one XML file to the other
    """
    def __init__(self, oldFilePath, newFilePath):
        """
        Initialize XmlMerger with old and new XML files

        @param oldFilePath: Path to old XML file
        @param newFilePath: Path to new XML file
        """
        import lxml.etree as etree
        parser = etree.XMLParser(remove_blank_text=True)
        self.__oldFilePath = oldFilePath
        self.__newFilePath = newFilePath
        self.__oldTree = etree.parse(oldFilePath, parser)
        self.__newTree = etree.parse(newFilePath, parser)
        self.__etree = etree

    def incrementVersion(self, tagPath):
        """
        Update the element of given tag from the new file to the old file,
        where the element's text should be a version existing in both files
        and the new version should be greater than the old one.

        @param tagPath: Path of tags to the version element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return
        oldVersion = self.__oldTree.find(tagPath)
        newVersion = self.__newTree.find(tagPath)
        if (oldVersion is not None and newVersion is not None
            and Version(oldVersion.text) < Version(newVersion.text)):
            oldVersion.text = newVersion.text

    def update(self, tagPath):
        """
        Update the element of given tag from the new file to the old file,
        where the element should exist in both files.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return
        oldElem = self.__oldTree.find(tagPath)
        newElem = self.__newTree.find(tagPath)
        if oldElem is not None and newElem is not None:
            oldElem.text = newElem.text

    def getParentPath(self, tagPath):
        """
        Retrieve the path to parent element by removing the last "/" and
        the following word. If no "/", then the parent path string is empty.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        words = tagPath.split(PATH_SEPARATOR)
        # Remove the last word and its preceding "/"
        parentPath = PATH_SEPARATOR.join(words[0:-1])
        return parentPath

    def getTag(self, tagPath):
        """
        Retrieve the name of a tag (i.e., the last word) given the path to it.
        If no word found in the path, then the tag string is empty.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        words = tagPath.split(PATH_SEPARATOR)
        # Return the last word
        return words[-1]

    def replace(self, tagPath):
        """
        Copy all instances of the element (including all of its subelements)
        of given tag from the new file to the old file,
        where at least one instance of the elemet should exist in both files.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return

        oldParent = self.__oldTree.getroot()
        parentPath = self.getParentPath(tagPath)
        if len(parentPath) != 0:
            oldParent = self.__oldTree.find(parentPath)

        oldElems = self.__oldTree.findall(tagPath)
        newElems = self.__newTree.findall(tagPath)
        if len(oldElems) != 0 and len(newElems) != 0:
            for elem in oldElems:
                oldParent.remove(elem)
            for elem in newElems:
                oldParent.append(elem)

    def add(self, tagPath):
        """
        Add a new element (including all of its subelements)
        of given tag from the new file to the old file,
        where the elemet should exist in the new file only.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return

        oldParent = self.__oldTree.getroot()
        parentPath = self.getParentPath(tagPath)
        if len(parentPath) != 0:
            oldParent = self.__oldTree.find(parentPath)

        oldElem = self.__oldTree.find(tagPath)
        newElem = self.__newTree.find(tagPath)
        if oldElem is None and newElem is not None:
            oldParent.append(newElem)

    def set(self, tagPath, text):
        """
        Set the text of an element of given tag in the old file only.
        If the element does not exist in the old file, then create it first.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or tagPath.startswith(".") or text is None):
            return

        oldParent = self.__oldTree.getroot()
        parentPath = self.getParentPath(tagPath)
        if len(parentPath) != 0:
            oldParent = self.__oldTree.find(parentPath)

        tag = self.getTag(tagPath)
        if len(tag) == 0:
            return

        oldElem = self.__oldTree.find(tagPath)
        if oldElem is None:
            oldElem = self.__etree.SubElement(oldParent, tag)
        oldElem.text = text

    def delete(self, tagPath, force=False):
        """
        Delete a element (including all of its subelements)
        of given tag from the old file,
        where the elemet should exist in the old file only.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return

        oldParent = self.__oldTree.getroot()
        parentPath = self.getParentPath(tagPath)
        if len(parentPath) != 0:
            oldParent = self.__oldTree.find(parentPath)

        oldElem = self.__oldTree.find(tagPath)
        newElem = self.__newTree.find(tagPath)
        if oldElem is not None and (newElem is None or force):
            oldParent.remove(oldElem)

    def saveToFile(self):
        """
        Save changes into the old XML file
        """
        self.__oldTree.write(self.__oldFilePath, encoding="utf-8", xml_declaration=True, pretty_print=True)


def GetConfigVersion(filePath):
    """
    Gets current config version number from filePath.

    :return: Version of Config files
    """
    # Read version from filePath
    if os.path.exists(filePath):
        with open(filePath) as fp:
            version = Version(fp.read())
    else:
        version = Version("0.0")

    return version


def SetConfigVersion(filePath, versionNo):
    """
    Persists the latest applied patch version.

    @param filePath: Path to the component version file.

    :return: None
    """
    with open(filePath, "w") as fp:
        fp.write(str(versionNo))


def LoadConfigFile():
    """
    Load data from config file
    """
    configDict = {}
    with open(VUM_CONFIG_TXT, 'r') as f:
        lines = [line.rstrip('\n') for line in f]
        for line in lines:
            key = line[0:line.find('=')]
            val = line[line.find('=') + 1:]
            configDict[key] = val
    return configDict

def CreateUpgradeDataBaseSchemaCmd(logDir):
    """
    Create command to run install util for DB upgrade

    @param logDir: directory for log file
    """
    configDict = LoadConfigFile()
    command = [INSTALLUTIL,
        '-C', CONFIG_DIR,
        '-L', logDir,
        '-I', BIN_DIR,
        '-D', configDict[CONFIG_DSN_NAME_KEY],
        '-U', configDict[CONFIG_DB_USERNAME_KEY],
        '-P', configDict[CONFIG_DB_PASSWORD_KEY],
        '-T', configDict[CONFIG_DB_TYPE_KEY],
        '-O', 'dbupgrade']
    return command

def upgradeVUMTLSConfig():
    """
    This function will modify vci-integrity.xml and jetty-vum-ssl.xml to support
    most secure TLS protocols configuration (as per security best practice doc.).
    TODO: Remove the code related to jetty-vum-ssl.xml config file, once we
          decide replace file upload use-case with VAPI
    """
    import lxml.etree as etree
    parser = etree.XMLParser(remove_blank_text=True)
    # Update vci-integrity.
    # Remove unsecure tls/ssl protocols as part of b2b upgrade.
    integrityXml = etree.parse(INTEGRITY_XML, parser)
    protocols = integrityXml.find(SSL_PROTOCOLS_TAG_PATH)
    if protocols is None:
       # If protocols tag is missing in config file due any reason, add it.
       ssl = integrityXml.find(SSL_TAG_PATH)
       protocols = etree.SubElement(ssl, PROTOCOLS_TAG)

    # By default, VUM should be configured to have only TLSv1.2 support.
    protocols.text = 'tls1.2'
    with open(INTEGRITY_XML, 'wb') as outfile:
        outfile.write(etree.tostring(integrityXml, pretty_print=True))

    # Update jetty-vum-ssl.
    # Add unsecure ssl/tls protocols to "excluded protocols" list.
    # TODO: remove this code once the jetty file upload use-case is replaced with VAPI
    jettyConfigTree = etree.parse(JETTY_VUM_SSL_XML, parser)
    array = jettyConfigTree.getroot().find(".//*[@name='ExcludeProtocols']/Array")
    protocolList = {"TLSv1" : False, "TLSv1.1" : False}
    for item in array:
        if item.text in protocolList:
            protocolList[item.text] = True

    for protocol in protocolList:
        if protocolList[protocol] is False:
            item = etree.SubElement(array, 'Item')
            item.text = protocol

    with open(JETTY_VUM_SSL_XML, 'wb') as outfile:
        outfile.write(etree.tostring(jettyConfigTree, xml_declaration=True, pretty_print=True, encoding="UTF-8"))
