# Copyright 2015 VMware, Inc.  All rights reserved. -- VMware Confidential
#
'''
Script responsible to setup environment and execute the validation hook of vCSA
components. The script calls only those components which have been discovered
in prior discovery phase.
'''

import logging
import json
import hashlib
import os

from extensions import Hook
from patch_specs import Mismatch, ValidationResult
from status_reporting_sdk.json_utils import ObjectJsonConverter, prettyJsonDump
from l10n import msgMetadata as _T, localizedString as _

from vmware_b2b.patching.executor.execution_facade import executeComponentHook
from vmware_b2b.patching.data.model import PatchPhaseContext
from vmware_b2b.patching.utils.reporting_utils import INTERNAL_ERROR_TEXT, INTERNAL_ERROR_RESOLUTION, \
    configureLocalization

logger = logging.getLogger(__name__)

VALIDATION_HASH_FILE = "validation.txt"

NOT_VALID_RESUME_TEXT = _(_T('patch.resume.validation.text',
                            'The provided answers do not match the one that '
                            'have being provided before the resume.'))

def _isSuccessful(validationResults):
    '''Test if validation component hook completed successfully.

    @param validationResult: Lookup of component name cross its ValidationResult
    @type validationResult: dict of str X patch_specs.ValidationResult

    @return: False if any component report an error mismatch, otherwise True
    @rtype: bool
    '''
    for compName, validationResult in validationResults.items():
        if validationResult is None:
            logger.warning("Component %s did not extend validation hook", compName)
            continue

        # Iterate over component mismatches
        for mismatch in validationResult.mismatches:
            if mismatch.severity == Mismatch.ERROR:
                return False

    return True

def _validateComponents(ctx, userData):
    '''Executes the components Validate hook.

    @param ctx: Global CPO patch phase context.
    @type ctx: vmware.patching.data.model.PatchPhaseContext

    @param userData: Customer input, as result of component questions raised
      in @Discovery patching hook.
    @type userData: dict

    @return: Lookup of component name cross its ValidationResult
    @rtype dict of str X patch_specs.ValidationResult

    @raise Exception: if validation hook execution fails.
    '''
    result = {}

    for c in ctx.getPatchableComponents(sort=False):
        logger.info("Validate component %s", c.name)

        validationResult = executeComponentHook(Hook.Validation, ctx, c, userData, None,
                                                expectedResultType=(type(None), ValidationResult))
        result[c.name] = validationResult

    return result

def _reportResults(validationResults, outputFile):
    '''Report validate results into the file.

    @param validationResults: Lookup of component name cross its ValidationResult
    @type validationResults: dict of str X patch_specs.ValidationResult

    @param outputFile: Output file
    @type outputFile: str
    '''
    # TODO iradev: Consider using real software-update specs rather than
    # serializing to dictionary. This would handle eventual property mismatches
    if not outputFile:
        logger.warning("The output file is not provided. Skip writing results")
        return

    mismatches = []
    for validationResult in validationResults.values():
        if validationResult is None:
            continue

        mismatches.extend(validationResult.mismatches)

    prettyJsonDump(outputFile, {"mismatches" : mismatches})

def _createErrorValidationResults(text, description, resolution):
    ''' Creates a validation results with just one error, created based on the
    variables passed to the function

    @param text: The text of the error
    @type text: LocalizableMessage

    @param description: The description of the error
    @type description: LocalizableMessage

    @param resolution: The resolution of the error
    @type resolution: LocalizableMessage

    @rtype: dict
    Example:
        {
            "default_errror_handler" : ValidationResult
        }
    '''
    mismatch = Mismatch(text=text,
                        description=description,
                        resolution=resolution)
    validationResult = ValidationResult(mismatches=[mismatch])
    validationResults = {"default_errror_handler" : validationResult}
    return validationResults

def _generateJsonHash(data):
    ''' Function that generates hash of a json by dumping it and ordering based
    on the keys. It is not safe to be distributed on different machine and it
    won't produce same result on Python2 and Python3 for the same json.

    @param data: Json (or dict) data to be hashed.
    @type data: dict

    @return: md5 hash of the data
    @rtype: str
    '''
    # TODO ensure that works on new version on python.
    plainText = json.dumps(data, sort_keys=True)
    return hashlib.md5(plainText.encode('utf-8')).hexdigest()

def _readHashValidation(hashFilePath):
    ''' Reads the value stored in the hash file provided
    @rtype: str
    '''
    if not os.path.exists(hashFilePath):
        return ''
    with open(hashFilePath) as fp:
        return fp.readline()

def validate(stageDir, userData, outputFile):
    '''The entry point of validate CPO phase.

    The validate phase is responsible to setup environment and execute the
    validate hook of vCSA components. The validation hook is executed only
    on those components which have been discovered in prior discovery phase.

    @param stageDir: global CPO stage directory. All components stage
      directories will be created as sub-directories of global stage directory.
    @type stageDir: str

    @param userData: Customer input, as result of component questions raised
      in @Discovery patching hook.
    @type userData: dict

    @param outputFile: File where the output of the command needs to be written
      to
    @type outputFile: str

    @return: True if the validation phase succeed and False otherwise
    @rtype: bool

    @raise Exception: if validation phase raise unhandled exception
    '''
    try:
        ctx = PatchPhaseContext.load(stageDir)
        configureLocalization(ctx.locale)
        hashFilePath = os.path.join(ctx.stageDir, VALIDATION_HASH_FILE)

        validationResults = {}
        # If upgrade is started it is indication that it is resume
        # also that we have had a successful execution of validate before that
        if ctx.isUpgradeStarted():
            logger.info("Validation is skipped as this is resume.")
            oldHash = _readHashValidation(hashFilePath)
            newHash = _generateJsonHash(userData)
            sameUserData = oldHash == newHash
            if not sameUserData:
                validationResults = _createErrorValidationResults(NOT_VALID_RESUME_TEXT,
                                                                  NOT_VALID_RESUME_TEXT,
                                                                  INTERNAL_ERROR_RESOLUTION)
            logger.info("New answers %s match old %s: %s", newHash, oldHash, sameUserData)
            return sameUserData

        validationResults = _validateComponents(ctx, userData)

        # Log the validation results
        logger.info("Validation completed. Result: %s\n",
                    json.dumps(validationResults,
                               default=ObjectJsonConverter(False).encode,
                               indent=4,
                               sort_keys=True))
        success = _isSuccessful(validationResults)

        if success:  # Only persist success and override prev
            userDataHash = _generateJsonHash(userData)
            with open(hashFilePath, 'w') as fp:
                fp.write(userDataHash)
            logger.info("Persisted hash of data %s", userDataHash)
        return success
    except Exception:
        # In case of exception, generate default error
        validationResults = _createErrorValidationResults(INTERNAL_ERROR_TEXT,
                                                          INTERNAL_ERROR_TEXT,
                                                          INTERNAL_ERROR_RESOLUTION)
        raise
    finally:
        _reportResults(validationResults, outputFile)
