"""PatchRunner integration hook for Content Library

This module integrates Content Library patching scripts with
PatchRunner Framework.
    Ref: https://wiki.eng.vmware.com/VSphere2016/vSphere2016Upgrade/Inplace/Patch_Extensibility

This framework is only supported for vCSA. Even though this framework supports state changes,
as of now, components are not allowed to make any state or database changes, due to lack of support
for this framework for Windows VC.
    Ref: https://wiki.eng.vmware.com/ContentLibrary/2016/BuildToBuildPatching#Resolved

Note:
    When a new Content Library B2B Patch Version is added,
    content-library-firstboot.INSTALL_VERSION needs to be updated.

"""

COMPONENT_NAME = "content-library"

try:
    # Setup python path for Content Library Modules
    from datetime import datetime
    import os
    import sys
    import traceback

    # Prefer the files in the directory of this file when importing.
    # This is important during patching as the VC can have older copies
    # of other files in /usr/lib/vmware-content-library.
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
except:
    # Should not happen, Workaround for PEP8 E402
    sys.exit("Most likely ImportError: %s" % traceback.format_exception(*sys.exc_info()))

import logging
from l10n import msgMetadata as _T, localizedString as _
from base import PatchStepExecutor, PatchStepVerifier, PatchVersion
from db_patch import DBPatchTask
from extensions import extend, Hook
from fss_utils import getTargetFSS
from ndu_config import replication_config
from patch_specs import (DiscoveryResult,
                         PatchInfo,
                         Requirements,
                         RequirementsResult,
                         ValidationResult)
from patches import cls66
from utils import (get_latest_patch_executor,
                   get_source_version,
                   set_source_version,
                   load_patchers,
                   apply_patches,
                   remove_source_config_property_files,
                   PATCH_VERSION_FILE)

from vcsa_utils import getComponentDiscoveryResult, isDisruptiveUpgrade
from patches.add_new_cls_privileges import register_cls
from patches.constants import (CONTENT_LIBRARY_CONFIG_HOME, CLS_TRUST_STORE_FILE,
                               CLS_NDU_MARKER_FILE_SOURCE, CLS_NDU_MARKER_FILE)
from reporting import getProgressReporter


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


logger = logging.getLogger(__name__)

# FSS for NDU FSS
NDU_LIMITED_DOWNTIME = "NDU_Limited_Downtime"

# Following tuple() should include the modules containing patch-logic
MODULES_TO_SCAN_FOR = (cls66, )


@extend(Hook.Discovery)
def discover(patch_context):
    """Decides if the component should participate in the patching process.

    Components can make this decision based on various deployment types and
    applicability of the patch.

    Args:
        patch_context (patch_specs.PatchContext): Patch Context provided by the PatchRunner
            patch_context provides user-input in a dict, locale, and working directory

    Returns:
        DiscoveryResult

    """
    logger.info("Executing Discovery Hook for Content Library")
    discoveryResult = None
    if isDisruptiveUpgrade(patch_context) or not getTargetFSS(NDU_LIMITED_DOWNTIME):
        logger.info("B2B CLS update!!")
        discoveryResult = getComponentDiscoveryResult(COMPONENT_NAME)
        logger.info("Returning Discovery result for Content Library")
        discoveryResult.dependentComponents.append("wcp")
        return discoveryResult
    '''
    This is NDU Update:
    Get the current patch version and the applicable patches,
    perform state changes if applicable.
    '''
    logger.info("NDU CLS update!!")
    '''
    1. Create NDU marker file and replicate it to target VC.
    2. Override default replication config with NDU replication config.
    '''
    open(CLS_NDU_MARKER_FILE_SOURCE, 'a').close()
    discoveryResult = getComponentDiscoveryResult(COMPONENT_NAME,
                                                  replicationConfig=replication_config)

    logger.info("Returning Discovery result for Content Library")
    discoveryResult.dependentComponents.append("wcp")
    return discoveryResult


@extend(Hook.Requirements)
def collect_requirements(patch_context):
    """Checks for any component-specific patching requirements, and if needed
    asks users for input.

    A component may need some user-input (eg. username, password for sso), or it may
    have some system specific requirements (eg. storage space, certain preconditions).
    In this hook, the component should make sure that all the requirements necessary
    for the patch to succeed are satisfied. Any requirement which is not satisfied,
    should be returned via RequirementsResult to the user.

    TODO bagatea: Add a way for an individual patch to specify requirements, if needed.

    Args:
        patch_context (patch_specs.PatchContext): PatchContext provided by PatchRunner

    Returns:
        RequirementsResult

    """
    logger.info("Executing Requirements Hook for Content Library")

    mismatches = []
    requirements = Requirements()
    patch_executor = get_latest_patch_executor(MODULES_TO_SCAN_FOR)
    if patch_executor:
        patch_info = PatchInfo(summary=patch_executor.version.summary)
    else:
        patch_info = PatchInfo()
    # No specific requirement to check here
    return RequirementsResult(requirements, patch_info, mismatches)


@extend(Hook.Validation)
def validate(patch_context):
    """Validates the user-input and checks that it satisfied component requirements.

    In this hook, a component validates any input provided by the user as a result of
    any requirement mismatches detected in Hook.Requirements.
    This method returns ValidationResult containing list of validation mismatches if any.
    Until the list of mismatches is empty, the PatchRunner framework does not proceed
    with the patching process.

    Args:
        patch_context (patch_specs.PatchContext): Patch Context provided by the PatchRunner

    Returns:
        ValidationResult
    """
    mismatches = []
    logger.info("Executing Validation Hook for Content Library")
    # We have no requirements to validate here
    return ValidationResult(mismatches)


@extend(Hook.Prepatch)
def execute_prepatch_actions(patch_context):
    """Runs component specific actions which need to be performed before
    installing the rpms for the components and its dependencies.

    In this hook, components can save state before the patch package is
    installed. After the execution of this hook, required services in the
    vCSA are stopped and the rpms are installed.

    Note: Rollback from failure is not the purpose of this hook.
    Customers are advised to take snapshot of the vCSA and databases,
    before starting the patching process.

    Args:
        patch_context (patch_specs.PatchContext): Patch Context provided by the PatchRunner

    Returns:
        None
    """
    pass


@extend(Hook.Expand)
def execute_expand(patch_context):
    """ Expand hook for content library in the source machine.
    """
    if not getTargetFSS(NDU_LIMITED_DOWNTIME):
        logger.info("FSS is off, all work will be done in patch hook of B2B upgrade")
        return
    sys.path.append(os.getenv("VMWARE_PYTHON_PATH"))
    progressReporter = getProgressReporter()
    progressReporter.updateProgress(0, _(_T("cls.expand.begin",
                                            'Start content library expand')))
    logger.info("Checking content library config DB for current patch versions")
    dbPatchTask = DBPatchTask(COMPONENT_NAME)
    if dbPatchTask.check_db_version():
        from .db_config import upgrade
        logger.info("Preparing to run content library DB expand. ")
        upgrade.execute_expand(dbPatchTask)
    else:
        # TODO : report error, content library without cl_config can not be upgraded.
        logger.error("Content library DB version not found")
    progressReporter.updateProgress(100, _(_T("cls.expand.complete",
                                              'Completed content library expand')))


@extend(Hook.Revert)
def execute_revert(patch_context):
    """ Revert hook for content library in source machine.
    """
    pass


@extend(Hook.Contract)
def execute_contract(patch_context):
    """ Contract hook for content library in the target machine.
    TODO : The non DB contract logic will be added here.
    """
    if not getTargetFSS(NDU_LIMITED_DOWNTIME):
        logger.info("FSS is off, all work will done in Patch hook")
        return
    prog_report = getProgressReporter()
    prog_report.updateProgress(0, _(_T("cls.contract.begin",
                                       'Start content library contract')))
    logger.info("Preparing to run content library DB contract.")
    # Patch the DB in Contract Phase
    dbPatchTask = DBPatchTask(COMPONENT_NAME)
    from .db_config import upgrade
    upgrade.execute_contract(dbPatchTask)
    prog_report.updateProgress(100, _(_T("cls.contract.complete",
                                         'Finish content library contract')))
    # clean up, remove files which were copied from source
    remove_source_config_property_files()


@extend(Hook.Patch)
def execute_patch(patch_context):
    """Executes patching scripts to reconfigure the component state, if
    needed.

    Note: This hook is invoked after the rpms are installed.

    Args:
        patch_context (patch_specs.PatchContext): Patch Context provided by the PatchRunner

    Returns:
        None
    """
    if not isDisruptiveUpgrade(patch_context) and getTargetFSS(NDU_LIMITED_DOWNTIME):
        logger.info("Patch hook in content library NDU update is not executed!!!")
        return
    logger.info("Executing Patch hook for content library")
    # Perform database patches, DB patch task will update DB schema from current
    # version to the latest version.
    logger.info("Patching DB changes for content library")
    dbPatchTask = DBPatchTask(COMPONENT_NAME)
    dbPatchTask.run()

    # Perform State Changes
    source_version = get_source_version()
    if source_version:
        logger.info("Patching state changes for Content Library from version: %s",
                    source_version.version)
        patch_executors, patch_verifiers = load_patchers(MODULES_TO_SCAN_FOR)
        apply_patches(patch_context, patch_executors, patch_verifiers, source_version)
    # (Re)register cls so that it will add any privileges that are new for this release
    register_cls()


@extend(Hook.OnSuccess)
def execute_on_success(patch_context):
    """Perform cleanup and post patch configuration changes if needed

    Args:
        patch_context (patch_specs.PatchContext): Patch Context provided by the PatchRunner

    Returns:
        None
    """
    patch_executor = get_latest_patch_executor(MODULES_TO_SCAN_FOR)
    logger.info("Content library has been successfully patched to version: %s",
                patch_executor.version.version)
