#!/usr/bin/env python
#
# Copyright 2017 VMware, Inc.  All rights reserved. -- VMware Confidential
#
"""
This module is the entry point for the Patch Runner.
"""

import logging
import os
import shutil

from l10n import localizedString, msgMetadata as _T
from extensions import Hook, extend
from patch_errors import PermanentError, UserError
from patch_specs import PatchContext, DiscoveryResult, RequirementsResult, PatchInfo, \
    ValidationResult
from reporting import getProgressReporter
from vcsa_utils import getComponentDiscoveryResult, getComponentDefintion
from .patch_util import get_applicable_patches, get_all_patches
from .version_util import get_client_version, save_client_version

VSPHERE_UI_ROOT_DIR = '/usr/lib/vmware-vsphere-ui'

logger = logging.getLogger(__name__)


@extend(Hook.Discovery)
def discover(patch_context):
    """
    Determine whether the vSphere Client should be patched.
    The function always return valid {DiscoveryResult}. This means that the other
    patch hooks will always be invoked.

    :param patch_context: Context given by the patch framework
    :type patch_context: PatchContext

    :return: Always return {DiscoveryResult}.
    :rtype: DiscoveryResult
    """

    # Get the vSphere Client service name from services.json using
    # serviceId = "vsphere-ui"
    service_name = _get_service_name()

    logger.info('Retrieving the discovery result from services.json file')
    result = getComponentDiscoveryResult('vsphere-ui', patchServices=[service_name])
    return result


@extend(Hook.Requirements)
def collect_requirements(patch_context):
    """
    Return the patch requirements.
    This function is invoked by the patch framework if the {discover} function return
    a valid {DiscoveryResult}. This means that there is at least one applicable patch.

    :param patch_context: Context given by the patch framework
    :type patch_context: PatchContext

    :return: The summary of the latest patch that should be installed
    :rtype: RequirementsResult
    """

    if _should_patch():
        # Get instances of all patch classes.
        logger.info('Read all patch scripts...')
        patches = get_all_patches()  # list of 'PatchBase' objects

        # Get the description of the latest patch that will be applied
        latest_patch = patches[-1]
        summary = latest_patch.get_summary()
        logger.info('Latest patch that will be applied is: %s' % latest_patch.get_version())

        patch_info = PatchInfo(timeToInstall=5, summary=summary)

    else:
        patch_info = PatchInfo()

    result = RequirementsResult(patchInfo=patch_info)
    return result


@extend(Hook.Patch)
def do_patching(patch_context):
    """
    1. Delete the virgo work folder.
    2. Check if there are applicable patches.
    3. Perform incremental patching, if there are applicable patches.

    :param patch_context: Context given by the patch framework
    :type patch_context: PatchContext
    """

    _delete_virgo_work_folder()

    if not _should_patch():
        logger.info('No need to patch the vSphere Client.')
        return None

    progress_reporter = getProgressReporter()
    cur_version, user_error = _do_incremental_patching(progress_reporter, patch_context)
    save_client_version(cur_version)

    if user_error:
        raise user_error


def _should_patch():
    """
    :return: Return True if there is at least one patch that should be applied.
    :rtype: bool
    """

    cur_version = get_client_version()
    applicable_patches = get_applicable_patches(cur_version)
    return len(applicable_patches) != 0


def _do_incremental_patching(progress_reporter, patch_context):
    """
    Incrementally applies all applicable patches.

    :param progress_reporter: Progress reporter that is updated on every successfully
    applied patch
    :type progress_reporter: _ProgressReporter

    :param patch_context: Context given by the patch framework
    :type patch_context: PatchContext

    :return: Tuple (last_patch_version, user_error)
        last_patch_version: version of the latest successfully applied patch
        user_error: error occurred during patching
    :rtype: (LooseVersion, UserError)
    """

    user_error = None

    logger.info('Get applicable patches')
    cur_version = get_client_version()
    applicable_patches = get_applicable_patches(cur_version)
    if len(applicable_patches) == 0:
        # This can happen only if this phase is invoked directly.
        logger.info('No applicable patches found.')
        return cur_version, user_error

    cur_progress = 0
    progress_step = int(100 / len(applicable_patches))

    for patch in applicable_patches:
        # Apply the patch
        try:
            logger.info('Executing patch version %s' % patch.get_version())
            patch.do_patching(patch_context)
            cur_version = patch.get_version()
            logger.info('Successfully applied patch.')

        except UserError as e:
            logger.exception('Failed to apply patch with version %s' %
                             patch.get_version())
            user_error = e
            break

        except Exception:
            logger.exception('Failed to apply patch with version %s' %
                             patch.get_version())
            cause = localizedString(_T(
                    'vsphere.ui.patch.fail.generic',
                    'Error when applying vSphere Client patch version $s'),
                patch.get_version())
            user_error = UserError(cause)
            break

        # Update the progress
        cur_progress += progress_step
        progress_reporter.updateProgress(cur_progress, patch.get_summary())

    if not user_error:
        logger.info('All patches applied successfully')
        progress_reporter.updateProgress(100, localizedString(_T(
                'vsphere.ui.patch.success',
                'vSphere Client was patched successfully.')))

    elif cur_progress != 0:
        logger.error('Not all patches were applied. Latest applied patch is %s' %
                     cur_version)
        progress_reporter.updateProgress(100, localizedString(_T(
                'vsphere.ui.patch.fail.partial',
                'vSphere Client was patched partially to version %s'), cur_version))

    else:
        logger.error('Failed to patch the vSphere Client')
        progress_reporter.updateProgress(100, localizedString(_T(
                'vsphere.ui.patch.fail.all',
                'vSphere Client patching failed')))

    return cur_version, user_error


def _get_service_name():
    """
    Read the vSphere Client service name from services.json

    :return: vSphere Client service name:
        - vsphere-ui - on Windows
        - vsphere-ui - on Linux
    :rtype: str
    """

    logger.info('Retrieving service name...')
    comp_def = getComponentDefintion('vsphere-ui')
    service_name = comp_def.serviceName
    logger.info('serviceName = %s' % service_name)
    return service_name


def _delete_virgo_work_folder():
    """
    Delete the virgo work folder, if exists.
    """

    work_dir = os.path.join(VSPHERE_UI_ROOT_DIR, 'server', 'work')
    if os.path.exists(work_dir):
        logger.info('Deleting vSphere Client work folder: %s' % work_dir)
        shutil.rmtree(work_dir)
        logger.info('Done deleting vSphere Client work folder')
    else:
        logger.info('Work folder does not exist: %s' % work_dir)
