"""This module contains various utils for Content Library B2B 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 tempfile

from base import PatchStepExecutor, PatchStepVerifier, PatchVersion
from patches.constants import VDC_PROPERTIES_FILE

__author__ = 'VMware, Inc.'
__copyright__ = 'Copyright 2016-2017 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.
PATCH_VERSION_FILE = "/etc/vmware-content-library/patch_version.txt"

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


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