"""This module contains various utils for Content Library B2B/NDU Patching

    Examples:

    Get source version from non-existing version file
        >>> get_source_version("this_file_doesnt_exist") is None
        True

    Set source version to 6.5.0
        >>> from tests import test00, test65, test70
        >>> file_path = "./test.txt"
        >>> with open(file_path, "w") as f:
        ...     f.write("6.5.0")

    Load and perform patching with an empty module:
        >>> MODULES_TO_SCAN = [test00]
        >>> executors, verifiers = load_patchers(MODULES_TO_SCAN)
        >>> len(executors), len(verifiers)
        (0, 0)
        >>> source_version = get_source_version(file_path)
        >>> source_version.version
        '6.5.0'
        >>> apply_patches(dict(), executors, verifiers, source_version, file_path)
        >>> get_source_version(file_path).version
        '6.5.0'

    Load and perform patching with a single module:
        >>> MODULES_TO_SCAN = [test65]
        >>> executors, verifiers = load_patchers(MODULES_TO_SCAN)
        >>> len(executors), len(verifiers)
        (2, 2)
        >>> source_version = get_source_version(file_path)
        >>> source_version.version
        '6.5.0'
        >>> apply_patches(dict(), executors, verifiers, source_version, file_path)
        >>> get_source_version(file_path).version
        '6.5.2'

    Load and perform patching with a two modules:
        >>> MODULES_TO_SCAN = [test65, test70]
        >>> executors, verifiers = load_patchers(MODULES_TO_SCAN)
        >>> len(executors), len(verifiers)
        (4, 4)
        >>> source_version = get_source_version(file_path)
        >>> source_version.version
        '6.5.2'
        >>> apply_patches(dict(), executors, verifiers, source_version, file_path)
        >>> get_source_version(file_path).version
        '7.0.3'

        >>> import os
        >>> os.remove(file_path)

"""
import logging
import os
import six.moves.configparser
import sys
import tempfile
from base import PatchStepExecutor, PatchStepVerifier, PatchVersion

from patches.constants import (PATCH_VERSION_FILE, VDC_PROPERTIES_FILE)

if not os.environ['VMWARE_PYTHON_PATH'] in sys.path:
    sys.path.append(os.environ['VMWARE_PYTHON_PATH'])

__author__ = 'VMware, Inc.'
__copyright__ = 'Copyright 2016-2017, 2021 VMware, Inc. All rights reserved.'


logger = logging.getLogger(__name__)

PATCH_VERSION = "_patch_version"
PATCH_TEST_VERSION = "_patch_test_version"
# Following file stores the last successful content-library patch version that was
# applied to the given system.
# The initial version is persisted by the content-library-firstboot.py, during fresh-install and
# upgrade.
# Subsequent patch-methods update this file at the end of the successful patching
# operation.

FAKE_SEC_HEAD_SECTION = 'root'
HEADER = '[%s]\n' % FAKE_SEC_HEAD_SECTION


def get_source_version(file_path=PATCH_VERSION_FILE):
    """Gets the component patch version for the source vCSA.

    Args:
        file_path (str) : path to the file which stores the patch version of the latest
            successful patch

    Returns:
        PatchVersion: Version of the latest successful patch applied to the source system
        None: if the version file at the file_path does not exist

    """
    if not os.path.exists(file_path):
        return None
    # patch_version.txt contains just the PatchVersion.version string
    with open(file_path, 'r') as f:
        return PatchVersion(f.read().strip(), None)


def set_source_version(version, file_path=PATCH_VERSION_FILE):
    """Persist the patch version to the version file

    Args:
        version (PatchVersion): patch version to persist. This typically is the
            version of latest successful patch applied.
        file_path (str): path to the file to persist this patch version info

    Returns:
        None

    Examples:
        >>> file_path = "./test.txt"
        >>> version = PatchVersion("6.5.0", None)
        >>> version.version
        '6.5.0'
        >>> set_source_version(version, file_path)
        >>> get_source_version(file_path).version
        '6.5.0'
        >>> version.version = "6.5.1\\n"
        >>> set_source_version(version, file_path)
        >>> get_source_version(file_path).version
        '6.5.1'
        >>> import os
        >>> os.remove(file_path)

    """
    with open(file_path, 'w') as f:
        return f.write(version.version)


def get_latest_patch_executor(modules):
    """Gets the latest patch executor from the list of provided patch-methods.

    The latest patch executor is the target patch version and is used to populate the
    PatchInfo.summary, which provides info to user as to what this patch contains.

    Args:
        modules (iterable): list of modules to scan for patch-methods

    Returns:
        PatchStepExecutor: the patch executor with the highest version from the
            patch-methods found in modules
        None: if no patch-methods are found from the provided modules

    """
    patch_executors, patch_verifiers = load_patchers(modules)
    if not patch_executors:
        return None

    latest_patch_version = sorted(patch_executors)[-1]
    return patch_executors[latest_patch_version]


def load_patchers(modules):
    """Load Content Library patch methods from all the provided modules.

    Args:
        modules (iterable): modules to look for patch executors and verifiers

    Returns:
        dict, dict: first element of the tuple is a  map of PatchVersions to PatchStepExecutors,
            while the second element of the tuple is a map of PatchVersions to PatchVerifiers

    """
    patch_executors = dict()
    patch_verifiers = dict()
    for module in modules:
        executors, verifiers = _load_patchers(module)
        patch_executors.update(executors)
        patch_verifiers.update(verifiers)
    return patch_executors, patch_verifiers


def _load_patchers(module):
    """Load Content Library patch methods (with tests) from a provided module.

    Args:
        module (module): module to look for patch executors and patch verifiers

    Returns:
        dict, dict: first element of the tuple is a  map of PatchVersions to PatchStepExecutors,
            while the second element of the tuple is a map of PatchVersions to PatchVerifiers

    """
    patch_executors = dict()
    patch_verifiers = dict()
    for entity in module.__dict__.values():
        if hasattr(entity, PATCH_VERSION):
            version = PatchVersion(entity._patch_version, entity._patch_summary)
            patch_executors[version] = PatchStepExecutor(version, entity)
        elif hasattr(entity, PATCH_TEST_VERSION):
            version = PatchVersion(entity._patch_test_version, None)
            patch_verifiers[version] = PatchStepVerifier(version, entity)
    return patch_executors, patch_verifiers


def apply_patches(patch_context, patch_executors, patch_verifiers,
                  source_version, file_path=PATCH_VERSION_FILE):
    """Executes patching logic to get the product from source version
    to the target version.

    Args:
        patch_context (dict): provided by PatchRunner
        patch_executors (dict): PatchVersion to PatchStepExecutor map
        patch_verifiers (dict): PatchVersion to PatchStepVerifier map
        source_version (PatchVersion): current version the product is at
        file_path (str): Path to the component version file

    Returns:
        None

    """
    patch_version = None
    for patch_version in sorted(patch_executors):
        if source_version < patch_version:
            patch_executors[patch_version].execute(patch_context)
            if patch_version in patch_verifiers:
                patch_verifiers[patch_version].verify(patch_context)
    # Persist the patch version only when the patch is applied
    if patch_version and source_version < patch_version:
        set_source_version(patch_version, file_path)


def find_vdc_property(property_entry):
    """
    Find the specified property entry in vdc.properties

    Args:
        property_entry: property entry

    Returns:
        True if matched property entry exists, false otherwise
    """
    with open(VDC_PROPERTIES_FILE) as fp:
        for line in fp:
            if line.strip() == property_entry:
                return True
    return False


def read_config(filepath):
    """
    Customized ConfigParser to understand java properties file.
    This adds a fake header [root] to the property file,
    and deals with : and = being escaped.
    TODO: this should be shared with upgrade by putting in install common lib

    Args:
        file_path (str): file path to a config file

    Returns:
        dict[str, str]: map of config key to config value
    """
    safe_config_parser = six.moves.configparser.SafeConfigParser()
    # Make the config reader case-sensitive
    safe_config_parser.optionxform = str

    tmp_config_fp = tempfile.NamedTemporaryFile(mode='w', delete=False)
    tmp_config_filepath = tmp_config_fp.name

    orig = open(filepath, 'r')
    tmp_config_fp.write(HEADER)
    for line in orig.readlines():
        line = _remove_escaping_for_java_properties_library(line)
        tmp_config_fp.write(line)
    orig.close()
    tmp_config_fp.close()

    with open(tmp_config_filepath) as fp:
        safe_config_parser.readfp(fp)

    config_items = safe_config_parser.items(FAKE_SEC_HEAD_SECTION)
    properties_map = {}
    for config_item in config_items:
        properties_map[config_item[0]] = config_item[1]

    os.remove(tmp_config_filepath)
    return properties_map


def _remove_escaping_for_java_properties_library(property_value):
    """
    Update the value by replace escaping separators with separator only

    Args:
        property_value (str): property value

    Return:
        property value with escaping separators updated
    """
    property_value = property_value.replace('\:', ':')
    property_value = property_value.replace('\=', '=')
    return property_value


def remove_source_config_property_files():
    '''
    This method deletes the content library configurtion files
    which were copied from source. It reads ndu replication config
    and removes the files which were copied from source VC.
    This is called from the contract hook of RDU/NDU update.
    '''
    from ndu_config import replication_config
    for real_file_name, source_file_name in list(replication_config.items()):
        # Ignore the replication files which are not updated
        # from the source VC.
        if not source_file_name:
            continue
        # Ignore same file replication from source
        if(real_file_name == source_file_name):
            continue
        if not os.path.isfile(source_file_name):
            logger.warning("Source file not found:'%s'" % source_file_name)
            continue
        try:
            logger.info("Deleting source file:'%s' file." % source_file_name)
            os.remove(source_file_name)
        except Exception as ex:
            logger.error("Error deleting source file:'%s'" % source_file_name)
            logger.error("Exception is::%s" % ex)


if __name__ == "__main__":
    import doctest
    doctest.testmod()
