"""
Win System Utils

Functions shared with salt.modules.win_system and salt.grains.pending_reboot

.. versionadded:: 3001
"""
# NOTE: DO NOT USE RAW STRINGS IN THIS MODULE! UNICODE_LITERALS DOES NOT PLAY
# NICELY WITH RAW STRINGS CONTAINING \u or \U.

import logging

import salt.utils.win_reg
import salt.utils.win_update

try:
    import win32api
    import win32con

    HAS_WIN32_MODS = True
except ImportError:
    HAS_WIN32_MODS = False


log = logging.getLogger(__name__)

# Define the module's virtual name
__virtualname__ = "win_system"
MINION_VOLATILE_KEY = "SYSTEM\\CurrentControlSet\\Services\\salt-minion\\Volatile-Data"
REBOOT_REQUIRED_NAME = "Reboot required"


def __virtual__():
    """
    Only works on Windows systems
    """
    if not salt.utils.platform.is_windows():
        return (
            False,
            "win_system salt util failed to load: "
            "The util will only run on Windows systems",
        )
    if not HAS_WIN32_MODS:
        return (
            False,
            "win_system salt util failed to load: "
            "The util will only run on Windows systems",
        )
    return __virtualname__


def get_computer_name():
    """
    Get the Windows computer name. Uses the win32api to get the current computer
    name.

    .. versionadded:: 3001

    Returns:
        str: Returns the computer name if found. Otherwise returns ``False``.

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_computer_name()
    """
    name = win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname)
    return name if name else False


def get_pending_computer_name():
    """
    Get a pending computer name. If the computer name has been changed, and the
    change is pending a system reboot, this function will return the pending
    computer name. Otherwise, ``None`` will be returned. If there was an error
    retrieving the pending computer name, ``False`` will be returned, and an
    error message will be logged to the minion log.

    .. versionadded:: 3001

    Returns:
        str:
            Returns the pending name if pending restart. Returns ``None`` if not
            pending restart.

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_computer_name()
    """
    current = get_computer_name()
    try:
        pending = salt.utils.win_reg.read_value(
            hive="HKLM",
            key="SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters",
            vname="NV Hostname",
        )["vdata"]
    except TypeError:
        # This should never happen as the above key and vname are system names
        # and should always be present
        return None
    if pending:
        return pending if pending.lower() != current.lower() else None


def get_pending_component_servicing():
    """
    Determine whether there are pending Component Based Servicing tasks that
    require a reboot.

    If any the following registry keys exist then a reboot is pending:

    ``HKLM:\\\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending``
    ``HKLM:\\\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootInProgress``
    ``HKLM:\\\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackagesPending``

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if there are pending Component Based Servicing tasks,
        otherwise ``False``

    CLI Example:

    .. code-block:: bash

        salt '*' system.get_pending_component_servicing
    """
    # So long as one of the registry keys exists, a reboot is pending
    base_key = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing"
    sub_keys = ("RebootPending", "RebootInProgress", "PackagesPending")
    for sub_key in sub_keys:
        key = "\\".join((base_key, sub_key))
        if salt.utils.win_reg.key_exists(hive="HKLM", key=key):
            return True

    return False


def get_pending_domain_join():
    """
    Determine whether there is a pending domain join action that requires a
    reboot.

    If any the following registry keys exist then a reboot is pending:

    ``HKLM:\\\\SYSTEM\\CurrentControlSet\\Services\\Netlogon\\AvoidSpnSet``
    ``HKLM:\\\\SYSTEM\\CurrentControlSet\\Services\\Netlogon\\JoinDomain``

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if there is a pending domain join action, otherwise
        ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_domain_join()
    """
    base_key = "SYSTEM\\CurrentControlSet\\Services\\Netlogon"
    sub_keys = ("AvoidSpnSet", "JoinDomain")

    # If any keys are present then there is a reboot pending.
    for sub_key in sub_keys:
        key = "\\".join((base_key, sub_key))
        if salt.utils.win_reg.key_exists(hive="HKLM", key=key):
            return True

    return False


def get_pending_file_rename():
    """
    Determine whether there are pending file rename operations that require a
    reboot.

    A reboot is pending if any of the following value names exist and have value
    data set:

    - ``PendingFileRenameOperations``
    - ``PendingFileRenameOperations2``

    in the following registry key:

    ``HKLM:\\\\SYSTEM\\CurrentControlSet\\Control\\Session Manager``

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if there are pending file rename operations, otherwise
        ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_file_rename()
    """
    vnames = ("PendingFileRenameOperations", "PendingFileRenameOperations2")
    key = "SYSTEM\\CurrentControlSet\\Control\\Session Manager"
    for vname in vnames:
        reg_ret = salt.utils.win_reg.read_value(hive="HKLM", key=key, vname=vname)
        if reg_ret["success"]:
            if reg_ret["vdata"] and (reg_ret["vdata"] != "(value not set)"):
                return True
    return False


def get_pending_servermanager():
    """
    Determine whether there are pending Server Manager tasks that require a
    reboot.

    A reboot is pending if the ``CurrentRebootAttempts`` value name exists and
    has an integer value. The value name resides in the following registry key:

    ``HKLM:\\\\SOFTWARE\\Microsoft\\ServerManager``

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if there are pending Server Manager tasks, otherwise
        ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_servermanager()
    """
    vname = "CurrentRebootAttempts"
    key = "SOFTWARE\\Microsoft\\ServerManager"

    # There are situations where it's possible to have '(value not set)' as
    # the value data, and since an actual reboot won't be pending in that
    # instance, just catch instances where we try unsuccessfully to cast as int.

    reg_ret = salt.utils.win_reg.read_value(hive="HKLM", key=key, vname=vname)
    if reg_ret["success"]:
        try:
            if int(reg_ret["vdata"]) > 0:
                return True
        except ValueError:
            pass
    return False


def get_pending_dvd_reboot():
    """
    Determine whether the DVD Reboot flag is set.

    The system requires a reboot if the ``DVDRebootSignal`` value name exists
    at the following registry location:

    ``HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce``

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if the above condition is met, otherwise ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_dvd_reboot()
    """
    # So long as the registry key exists, a reboot is pending.
    return salt.utils.win_reg.value_exists(
        hive="HKLM",
        key="SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce",
        vname="DVDRebootSignal",
    )


def get_pending_update():
    """
    Determine whether there are pending updates that require a reboot.

    If either of the following registry keys exists, a reboot is pending:

    ``HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired``
    ``HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\PostRebootReporting``

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if any of the above conditions are met, otherwise
        ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_update()
    """
    # So long as any of the registry keys exists, a reboot is pending.
    base_key = (
        "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update"
    )
    sub_keys = ("RebootRequired", "PostRebootReporting")
    for sub_key in sub_keys:
        key = "\\".join((base_key, sub_key))
        if salt.utils.win_reg.key_exists(hive="HKLM", key=key):
            return True

    return False


def get_reboot_required_witnessed():
    """
    Determine if at any time during the current boot session the salt minion
    witnessed an event indicating that a reboot is required.

    This function will return ``True`` if an install completed with exit
    code 3010 during the current boot session and can be extended where
    appropriate in the future.

    If the ``Reboot required`` value name exists in the following location and
    has a value of ``1`` then the system is pending reboot:

    ``HKLM:\\\\SYSTEM\\CurrentControlSet\\Services\\salt-minion\\Volatile-Data``

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if the ``Requires reboot`` registry flag is set to ``1``,
        otherwise ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_reboot_required_witnessed()

    """
    value_dict = salt.utils.win_reg.read_value(
        hive="HKLM", key=MINION_VOLATILE_KEY, vname=REBOOT_REQUIRED_NAME
    )
    return value_dict["vdata"] == 1


def set_reboot_required_witnessed():
    """
    This function is used to remember that an event indicating that a reboot is
    required was witnessed. This function relies on the salt-minion's ability to
    create the following volatile registry key in the *HKLM* hive:

       *SYSTEM\\CurrentControlSet\\Services\\salt-minion\\Volatile-Data*

    Because this registry key is volatile, it will not persist beyond the
    current boot session. Also, in the scope of this key, the name *'Reboot
    required'* will be assigned the value of *1*.

    For the time being, this function is being used whenever an install
    completes with exit code 3010 and can be extended where appropriate in the
    future.

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if successful, otherwise ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.set_reboot_required_witnessed()
    """
    return salt.utils.win_reg.set_value(
        hive="HKLM",
        key=MINION_VOLATILE_KEY,
        volatile=True,
        vname=REBOOT_REQUIRED_NAME,
        vdata=1,
        vtype="REG_DWORD",
    )


def get_pending_update_exe_volatile():
    """
    Determine whether there is a volatile update exe that requires a reboot.

    Checks ``HKLM:\\Microsoft\\Updates``. If the ``UpdateExeVolatile`` value
    name is anything other than 0 there is a reboot pending

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if there is a volatile exe, otherwise ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_update_exe_volatile()
    """
    key = "SOFTWARE\\Microsoft\\Updates"
    reg_ret = salt.utils.win_reg.read_value(
        hive="HKLM", key=key, vname="UpdateExeVolatile"
    )
    if reg_ret["success"]:
        try:
            if int(reg_ret["vdata"]) != 0:
                return True
        except ValueError:
            pass
    return False


def get_pending_windows_update():
    """
    Check the Windows Update system for a pending reboot state.

    This leverages the Windows Update System to determine if the system is
    pending a reboot.

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if the Windows Update system reports a pending update,
        otherwise ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_windows_update()
    """
    return salt.utils.win_update.needs_reboot()


def get_pending_reboot():
    """
    Determine whether there is a reboot pending.

    .. versionadded:: 3001

    Returns:
        bool: ``True`` if the system is pending reboot, otherwise ``False``

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_reboot()
    """
    # Order the checks for reboot pending in most to least likely.
    checks = (
        get_pending_update,
        get_pending_windows_update,
        get_pending_update_exe_volatile,
        get_pending_file_rename,
        get_pending_servermanager,
        get_pending_component_servicing,
        get_pending_dvd_reboot,
        get_reboot_required_witnessed,
        get_pending_computer_name,
        get_pending_domain_join,
    )

    for check in checks:
        if check():
            return True

    return False


def get_pending_reboot_details():
    """
    Determine which check is signalling that the system is pending a reboot.
    Useful in determining why your system is signalling that it needs a reboot.

    .. versionadded:: 3001

    Returns:
        dict: A dictionary of the results of each function that checks for a
        pending reboot

    Example:

    .. code-block:: python

        import salt.utils.win_system
        salt.utils.win_system.get_pending_reboot_details()
    """
    return {
        "Pending Component Servicing": get_pending_component_servicing(),
        "Pending Computer Rename": get_pending_computer_name() is not None,
        "Pending DVD Reboot": get_pending_dvd_reboot(),
        "Pending File Rename": get_pending_file_rename(),
        "Pending Join Domain": get_pending_domain_join(),
        "Pending ServerManager": get_pending_servermanager(),
        "Pending Update": get_pending_update(),
        "Pending Windows Update": get_pending_windows_update(),
        "Reboot Required Witnessed": get_reboot_required_witnessed(),
        "Volatile Update Exe": get_pending_update_exe_volatile(),
    }
