"""
Module for managing Windows Users.

.. important::
    If you feel that Salt should be using this module to manage users on a
    minion, and it is using a different module (or gives an error similar to
    *'user.info' is not available*), see :ref:`here
    <module-provider-override>`.

:depends:
        - pywintypes
        - win32api
        - win32con
        - win32net
        - win32netcon
        - win32profile
        - win32security
        - win32ts
        - wmi

.. note::
    This currently only works with local user accounts, not domain accounts
"""

import ctypes
import logging
import shlex
import time
from datetime import datetime

import salt.utils.args
import salt.utils.dateutils
import salt.utils.platform
import salt.utils.versions
import salt.utils.win_reg
import salt.utils.win_runas
import salt.utils.winapi
from salt.exceptions import CommandExecutionError

log = logging.getLogger(__name__)

try:
    import pywintypes
    import win32api
    import win32con
    import win32net
    import win32netcon
    import win32profile
    import win32security
    import win32ts
    import wmi

    HAS_WIN32NET_MODS = True
except ImportError:
    HAS_WIN32NET_MODS = False

# Define the module's virtual name
__virtualname__ = "user"


def __virtual__():
    """
    Requires Windows and Windows Modules
    """
    if not salt.utils.platform.is_windows():
        return False, "Module win_useradd: Windows Only"

    if not HAS_WIN32NET_MODS:
        return False, "Module win_useradd: Missing Win32 Modules"

    return __virtualname__


def add(
    name,
    password=None,
    fullname=None,
    description=None,
    groups=None,
    home=None,
    homedrive=None,
    profile=None,
    logonscript=None,
):
    """
    Add a user to the minion.

    Args:

        name (str): The username for the new account.

        password (:obj:`str`, optional): User's password in plain text.

            Default is ``None``.

        fullname (:obj:`str`, optional): The user's full name.

            Default is ``None``.

        description (:obj:`str`, optional):
            A brief description of the user account.

            Default is ``None``.

        groups (:obj:`str`, optional):
            A list of groups to add the user to. (see chgroups)

            Default is ``None``.

        home (:obj:`str`, optional):
            The path to the user's home directory.

            Default is ``None``.

        homedrive (:obj:`str`, optional):
            The drive letter to assign to the home directory. Must be the Drive
            Letter followed by a colon. ie: ``U:``.

            Default is ``None``.

        profile (:obj:`str`, optional):
            An explicit path to a profile. Can be a UNC or a folder on the
            system. If left blank, windows uses its default profile directory.

            Default is ``None``.

        logonscript (:obj:`str`, optional):
            Path to a login script to run when the user logs on.

            Default is ``None``.

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.add name password
    """
    user_info = {}
    if name:
        user_info["name"] = str(name)
    else:
        return False
    if password:
        user_info["password"] = str(password)
    else:
        user_info["password"] = None
    user_info["priv"] = win32netcon.USER_PRIV_USER
    user_info["home_dir"] = home
    user_info["comment"] = description
    user_info["flags"] = win32netcon.UF_SCRIPT
    user_info["script_path"] = logonscript

    try:
        win32net.NetUserAdd(None, 1, user_info)
    except win32net.error as exc:
        log.error("Failed to create user %s", name)
        log.error("nbr: %s", exc.winerror)
        log.error("ctx: %s", exc.funcname)
        log.error("msg: %s", exc.strerror)
        return exc.strerror

    update(name=name, homedrive=homedrive, profile=profile, fullname=fullname)

    ret = chgroups(name, groups) if groups else True

    return ret


def update(
    name,
    password=None,
    fullname=None,
    description=None,
    home=None,
    homedrive=None,
    logonscript=None,
    profile=None,
    expiration_date=None,
    expired=None,
    account_disabled=None,
    unlock_account=None,
    password_never_expires=None,
    disallow_change_password=None,
):
    # pylint: disable=anomalous-backslash-in-string
    """
    Updates settings for the Windows user. Name is the only required parameter.
    Settings will only be changed if the parameter is passed a value.

    .. versionadded:: 2015.8.0

    Args:

        name (str): The username to update.

        password (:obj:`str`, optional):
            New user password in plain text.

            Default is ``None``.

        fullname (:obj:`str`, optional):
            The user's full name.

            Default is ``None``.

        description (:obj:`str`, optional):
            A brief description of the user account.

            Default is ``None``.

        home (:obj:`str`, optional):
            The path to the user's home directory.

            Default is ``None``.

        homedrive (:obj:`str`, optional):
            The drive letter to assign to the home directory. Must be the Drive
            Letter followed by a colon. ie: ``U:``.

            Default is ``None``.

        logonscript (:obj:`str`, optional):
            The path to the logon script.

            Default is ``None``.

        profile (:obj:`str`, optional):
            The path to the user's profile directory.

            Default is ``None``.

        expiration_date (:obj:`date`, optional):
            The date and time when the account expires. Can be a valid date/time
            string. To set to never expire pass the string 'Never'.

            Default is ``None``.

        expired (:obj:`bool`, optional):
            Pass ``True`` to expire the account. The user will be prompted to
            change their password at the next logon. Pass ``False`` to mark the
            account as 'not expired'. You can't use this to negate the
            expiration if the expiration was caused by the account expiring.
            You'll have to change the ``expiration_date`` as well.

            Default is ``None``.

        account_disabled (:obj:`bool`, optional):
            ``True`` disables the account. ``False`` enables the account.

            Default is ``None``.

        unlock_account (:obj:`bool`, optional):
            ``True`` unlocks a locked user account. ``False`` is ignored.

            Default is ``None``.

        password_never_expires (:obj:`bool`, optional):
            ``True`` sets the password to never expire. ``False`` allows the
            password to expire.

            Default is ``None``.

        disallow_change_password (:obj:`bool`, optional):
            ``True`` blocks the user from changing the password. ``False``
            allows the user to change the password.

            Default is ``None``.

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.update bob password=secret profile=C:\\Users\\Bob
                 home=\\server\\homeshare\\bob homedrive=U:
    """
    # pylint: enable=anomalous-backslash-in-string
    # Make sure the user exists
    # Return an object containing current settings for the user
    try:
        user_info = win32net.NetUserGetInfo(None, str(name), 4)
    except win32net.error as exc:
        log.error("Failed to update user %s", name)
        log.error("nbr: %s", exc.winerror)
        log.error("ctx: %s", exc.funcname)
        log.error("msg: %s", exc.strerror)
        return exc.strerror

    # Check parameters to update
    # Update the user object with new settings
    if password:
        user_info["password"] = str(password)
    else:
        user_info["password"] = None
    if home:
        user_info["home_dir"] = home
    if homedrive:
        user_info["home_dir_drive"] = homedrive
    if description:
        user_info["comment"] = description
    if logonscript:
        user_info["script_path"] = logonscript
    if fullname:
        user_info["full_name"] = fullname
    if profile:
        user_info["profile"] = profile
    if expiration_date:
        if expiration_date == "Never":
            user_info["acct_expires"] = win32netcon.TIMEQ_FOREVER
        else:
            try:
                dt_obj = salt.utils.dateutils.date_cast(expiration_date)
            except (ValueError, RuntimeError):
                return f"Invalid Date/Time Format: {expiration_date}"
            user_info["acct_expires"] = int(dt_obj.timestamp())
    if expired is not None:
        if expired:
            user_info["password_expired"] = 1
        else:
            user_info["password_expired"] = 0
    if account_disabled is not None:
        if account_disabled:
            user_info["flags"] |= win32netcon.UF_ACCOUNTDISABLE
        else:
            user_info["flags"] &= ~win32netcon.UF_ACCOUNTDISABLE
    if unlock_account is not None:
        # We can only unlock with this flag... we can't unlock
        if unlock_account:
            user_info["flags"] &= ~win32netcon.UF_LOCKOUT
    if password_never_expires is not None:
        if password_never_expires:
            user_info["flags"] |= win32netcon.UF_DONT_EXPIRE_PASSWD
        else:
            user_info["flags"] &= ~win32netcon.UF_DONT_EXPIRE_PASSWD
    if disallow_change_password is not None:
        if disallow_change_password:
            user_info["flags"] |= win32netcon.UF_PASSWD_CANT_CHANGE
        else:
            user_info["flags"] &= ~win32netcon.UF_PASSWD_CANT_CHANGE

    # Apply new settings
    try:
        win32net.NetUserSetInfo(None, str(name), 4, user_info)
    except win32net.error as exc:
        log.error("Failed to update user %s", name)
        log.error("nbr: %s", exc.winerror)
        log.error("ctx: %s", exc.funcname)
        log.error("msg: %s", exc.strerror)
        return exc.strerror

    return True


def delete(name, purge=False, force=False):
    """
    Remove a user from the minion

    Args:

        name (str): The name of the user to delete

        purge (:obj:`bool`, optional):
            Boolean value indicating that the user profile should also be
            removed when the user account is deleted. If set to ``True`` the
            profile will be removed.

            Default is ``False``.

        force (:obj:`bool`, optional):
            Boolean value indicating that the user account should be deleted
            even if the user is logged in. ``True`` will log the user out and
            delete user.

            Default is ``False``.

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.delete name
    """
    # Check if the user exists
    try:
        user_info = win32net.NetUserGetInfo(None, str(name), 4)
    except win32net.error as exc:
        log.error("User not found: %s", name)
        log.error("nbr: %s", exc.winerror)
        log.error("ctx: %s", exc.funcname)
        log.error("msg: %s", exc.strerror)
        return exc.strerror

    # Check if the user is logged in
    # Return a list of logged in users
    try:
        sess_list = win32ts.WTSEnumerateSessions()
    except win32ts.error as exc:
        log.error("No logged in users found")
        log.error("nbr: %s", exc.winerror)
        log.error("ctx: %s", exc.funcname)
        log.error("msg: %s", exc.strerror)

    # Is the user one that is logged in
    logged_in = False
    session_id = None
    for sess in sess_list:
        if (
            win32ts.WTSQuerySessionInformation(
                None, sess["SessionId"], win32ts.WTSUserName
            )
            == name
        ):
            session_id = sess["SessionId"]
            logged_in = True

    # If logged in and set to force, log the user out and continue
    # If logged in and not set to force, return false
    if logged_in:
        if force:
            try:
                win32ts.WTSLogoffSession(
                    win32ts.WTS_CURRENT_SERVER_HANDLE, session_id, True
                )
            except win32ts.error as exc:
                log.error("User not found: %s", name)
                log.error("nbr: %s", exc.winerror)
                log.error("ctx: %s", exc.funcname)
                log.error("msg: %s", exc.strerror)
                return exc.strerror
        else:
            log.error("User %s is currently logged in.", name)
            return False

    # Remove the User Profile directory
    if purge:
        try:
            sid = get_user_sid(name)
            win32profile.DeleteProfile(sid)
        except pywintypes.error as exc:
            (number, context, message) = exc.args
            if number == 2:  # Profile Folder Not Found
                pass
            else:
                log.error("Failed to remove profile for %s", name)
                log.error("nbr: %s", exc.winerror)
                log.error("ctx: %s", exc.funcname)
                log.error("msg: %s", exc.strerror)
                return exc.strerror

    # And finally remove the user account
    try:
        win32net.NetUserDel(None, str(name))
    except win32net.error as exc:
        log.error("Failed to delete user %s", name)
        log.error("nbr: %s", exc.winerror)
        log.error("ctx: %s", exc.funcname)
        log.error("msg: %s", exc.strerror)
        return exc.strerror

    return True


def getUserSid(username):
    """
    Deprecated function. Please use get_user_sid instead

    CLI Example:

    .. code-block:: bash

        salt '*' user.get_user_sid jsnuffy
    """
    salt.utils.versions.warn_until(
        version=3009,
        message="'getUserSid' is being deprecated. Please use get_user_sid instead",
    )
    return get_user_sid(username)


def get_user_sid(username):
    """
    Get the Security ID for the user

    Args:

        username (str): The username for which to look up the SID

    Returns:
        str: The user SID

    CLI Example:

    .. code-block:: bash

        salt '*' user.get_user_sid jsnuffy
    """
    if not isinstance(username, str):
        username = str(username)
    domain = win32api.GetComputerName()
    if username.find("\\") != -1:
        domain = username.split("\\")[0]
        username = username.split("\\")[-1]
    domain = domain.upper()
    return win32security.ConvertSidToStringSid(
        win32security.LookupAccountName(None, domain + "\\" + username)[0]
    )


def setpassword(name, password):
    """
    Set the user's password

    Args:

        name (str): The username for which to set the password

        password (str): The new password

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.setpassword jsnuffy sup3rs3cr3t
    """
    # update() returns the Win32 error string on failure (e.g. when the user
    # does not exist), which is truthy and would otherwise be misreported as
    # success by callers that check the return value as a boolean. Verify the
    # user exists first so we can return a proper ``False`` and surface a
    # clear log message.
    if not info(name):
        log.error("User '%s' does not exist", name)
        return False
    return update(name=name, password=password) is True


def addgroup(name, group):
    """
    Add user to a group

    Args:

        name (str): The username to add to the group

        group (str): The name of the group to which to add the user

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.addgroup jsnuffy 'Power Users'
    """
    name = shlex.quote(str(name))
    group = shlex.quote(group).lstrip("'").rstrip("'")

    user = info(name)
    if not user:
        return False
    if group in user["groups"]:
        return True

    cmd = f'net localgroup "{group}" {name} /add'
    ret = __salt__["cmd.run_all"](cmd, python_shell=True)

    return ret["retcode"] == 0


def removegroup(name, group):
    """
    Remove user from a group

    Args:

        name (str): The username to remove from the group

        group (str): The name of the group from which to remove the user

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.removegroup jsnuffy 'Power Users'
    """
    name = shlex.quote(str(name))
    group = shlex.quote(group).lstrip("'").rstrip("'")

    user = info(name)

    if not user:
        return False

    if group not in user["groups"]:
        return True

    cmd = f'net localgroup "{group}" {name} /delete'
    ret = __salt__["cmd.run_all"](cmd, python_shell=True)

    return ret["retcode"] == 0


def chhome(name, home, **kwargs):
    """
    Change the home directory of the user, pass ``True`` for persist to move
    files to the new home directory if the old home directory exist.

    Args:

        name (str): The name of the user whose home directory you wish to change

        home (str): The new location of the home directory

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.chhome foo \\\\fileserver\\home\\foo True
    """
    kwargs = salt.utils.args.clean_kwargs(**kwargs)
    persist = kwargs.pop("persist", False)
    if kwargs:
        salt.utils.args.invalid_kwargs(kwargs)
    if persist:
        log.info("Ignoring unsupported 'persist' argument to user.chhome")

    pre_info = info(name)

    if not pre_info:
        return False

    if home == pre_info["home"]:
        return True

    if not update(name=name, home=home):
        return False

    post_info = info(name)
    if post_info["home"] != pre_info["home"]:
        return post_info["home"] == home

    return False


def chprofile(name, profile):
    """
    Change the profile directory of the user

    Args:

        name (str): The name of the user whose profile you wish to change

        profile (str): The new location of the profile

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.chprofile foo \\\\fileserver\\profiles\\foo
    """
    return update(name=name, profile=profile)


def chfullname(name, fullname):
    """
    Change the full name of the user

    Args:

        name (str): The username for which to change the full name

        fullname (str): The new value for the full name

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.chfullname user 'First Last'
    """
    return update(name=name, fullname=fullname)


def chgroups(name, groups, append=True):
    """
    Change the groups this user belongs to, add append=False to make the user a
    member of only the specified groups

    Args:

        name (str): The username for which to change groups

        groups (str, list):
            A single group or a list of groups to assign to the user. For
            multiple groups this can be a comma-delimited string or a list.

        append (:obj:`bool`, optional):
            ``True`` adds the passed groups to the user's current groups.
            ``False`` sets the user's groups to the passed groups only.

            Default is ``True``.

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.chgroups jsnuffy Administrators,Users True
    """
    if isinstance(groups, str):
        groups = groups.split(",")

    groups = [x.strip(" *") for x in groups]
    current_groups = set(list_groups(name))
    expected_groups = set()

    name = shlex.quote(str(name))

    if not append:
        # We don't want to append to the list, remove groups not in the new set
        # of groups
        for group in current_groups:
            group = shlex.quote(group).lstrip("'").rstrip("'")
            if group not in groups:
                cmd = f'net localgroup "{group}" {name} /delete'
                __salt__["cmd.run_all"](cmd, python_shell=True)
            else:
                expected_groups.add(group)
    else:
        # We're appending to the current list of groups. If they already match
        # then bail
        if current_groups == set(groups):
            return True
        else:
            expected_groups = current_groups.union(set(groups))

    for group in groups:
        if group in current_groups:
            continue
        group = shlex.quote(group).lstrip("'").rstrip("'")
        cmd = f'net localgroup "{group}" {name} /add'
        out = __salt__["cmd.run_all"](cmd, python_shell=True)
        if out["retcode"] != 0:
            log.error(out["stdout"])
            return False

    new_groups = set(list_groups(name))

    return len(expected_groups - new_groups) == 0


def info(name):
    """
    Return user information

    Args:

        name (str): Username for which to display information

    Returns:
        dict: A dictionary containing user information
            - fullname
            - username
            - SID
            - passwd (will always return None)
            - comment (same as description, left here for backwards compatibility)
            - description
            - active
            - logonscript
            - profile
            - home
            - homedrive
            - groups
            - password_changed
            - successful_logon_attempts
            - failed_logon_attempts
            - last_logon
            - account_disabled
            - account_locked
            - expiration_date
            - password_never_expires
            - disallow_change_password
            - gid

    CLI Example:

    .. code-block:: bash

        salt '*' user.info jsnuffy
    """
    ret = {}
    items = {}
    server = None

    user_name, domain_name = salt.utils.win_runas.split_username(name)
    log.debug("user_name: %s", user_name)
    log.debug("domain_name: %s", domain_name)

    if domain_name != ".":
        try:
            dc_info = win32security.DsGetDcName(None, domain_name)
            server = dc_info["DomainControllerName"]
            log.debug("Found DC: %s", server)
        except pywintypes.error:
            # Restore username to original
            log.debug("DC not found. Using username: %s", str(name))
            user_name = str(name)

    try:
        items = win32net.NetUserGetInfo(server, user_name, 4)
    except win32net.error:
        pass

    if items:
        groups = []
        try:
            groups = win32net.NetUserGetLocalGroups(server, user_name)
        except win32net.error:
            pass

        ret["fullname"] = items["full_name"]
        ret["name"] = items["name"]
        ret["uid"] = win32security.ConvertSidToStringSid(items["user_sid"])
        ret["passwd"] = items["password"]
        ret["comment"] = items["comment"]
        ret["description"] = items["comment"]
        ret["active"] = not bool(items["flags"] & win32netcon.UF_ACCOUNTDISABLE)
        ret["logonscript"] = items["script_path"]
        ret["profile"] = items["profile"]
        ret["failed_logon_attempts"] = items["bad_pw_count"]
        ret["successful_logon_attempts"] = items["num_logons"]
        secs = time.mktime(datetime.now().timetuple()) - items["password_age"]
        ret["password_changed"] = datetime.fromtimestamp(secs).strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        if items["last_logon"] == 0:
            ret["last_logon"] = "Never"
        else:
            ret["last_logon"] = datetime.fromtimestamp(items["last_logon"]).strftime(
                "%Y-%m-%d %H:%M:%S"
            )

        # If the value is -1 or 0xFFFFFFFF, it is set to never expire
        if items["acct_expires"] == ctypes.c_ulong(win32netcon.TIMEQ_FOREVER).value:
            ret["expiration_date"] = "Never"
        else:
            ret["expiration_date"] = datetime.fromtimestamp(
                items["acct_expires"]
            ).strftime("%Y-%m-%d %H:%M:%S")

        ret["expired"] = items["password_expired"] == 1
        if not ret["profile"]:
            ret["profile"] = _get_userprofile_from_registry(name, ret["uid"])
        ret["home"] = items["home_dir"]
        ret["homedrive"] = items["home_dir_drive"]
        if not ret["home"]:
            ret["home"] = ret["profile"]
        ret["groups"] = groups
        if items["flags"] & win32netcon.UF_DONT_EXPIRE_PASSWD == 0:
            ret["password_never_expires"] = False
        else:
            ret["password_never_expires"] = True
        if items["flags"] & win32netcon.UF_ACCOUNTDISABLE == 0:
            ret["account_disabled"] = False
        else:
            ret["account_disabled"] = True
        if items["flags"] & win32netcon.UF_LOCKOUT == 0:
            ret["account_locked"] = False
        else:
            ret["account_locked"] = True
        if items["flags"] & win32netcon.UF_PASSWD_CANT_CHANGE == 0:
            ret["disallow_change_password"] = False
        else:
            ret["disallow_change_password"] = True

        ret["gid"] = ""

        return ret

    else:

        return {}


def _get_userprofile_from_registry(user, sid):
    """
    In case net user doesn't return the userprofile we can get it from the
    registry

    Args:
        user (str): The username, used in debug message

        sid (str): The sid to lookup in the registry

    Returns:
        str: Profile directory
    """
    profile_dir = salt.utils.win_reg.read_value(
        hive="HKEY_LOCAL_MACHINE",
        key=f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{sid}",
        vname="ProfileImagePath",
    )["vdata"]
    log.debug('user %s with sid=%s profile is located at "%s"', user, sid, profile_dir)
    return profile_dir


def list_groups(name):
    """
    Return a list of groups the named user belongs to

    Args:

        name (str): The username for which to list groups

    Returns:
        list: A list of groups to which the user belongs

    CLI Example:

    .. code-block:: bash

        salt '*' user.list_groups foo
    """
    ugrp = set()
    try:
        user = info(name)["groups"]
    except KeyError:
        return False
    for group in user:
        ugrp.add(group.strip(" *"))

    return sorted(list(ugrp))


def getent(refresh=False):
    """
    Return the list of all info for all users

    Args:

        refresh (:obj:`bool`, optional):
            Refresh the cached user information. Useful when used from within a
            state function.

            Default is ``False``.

    Returns:
        dict: A dictionary containing information about all users on the system

    CLI Example:

    .. code-block:: bash

        salt '*' user.getent
    """
    if "user.getent" in __context__ and not refresh:
        return __context__["user.getent"]

    ret = []
    for user in list_users():
        stuff = {}
        user_info = info(user)

        stuff["gid"] = ""
        stuff["groups"] = user_info["groups"]
        stuff["home"] = user_info["home"]
        stuff["name"] = user_info["name"]
        stuff["passwd"] = user_info["passwd"]
        stuff["shell"] = ""
        stuff["uid"] = user_info["uid"]

        ret.append(stuff)

    __context__["user.getent"] = ret
    return ret


def list_users():
    """
    Return a list of all users on Windows

    Returns:
        list: A list of all users on the system

    CLI Example:

    .. code-block:: bash

        salt '*' user.list_users
    """
    res = 0
    user_list = []
    dowhile = True
    try:
        while res or dowhile:
            dowhile = False
            (users, _, res) = win32net.NetUserEnum(
                None,
                0,
                win32netcon.FILTER_NORMAL_ACCOUNT,
                res,
                win32netcon.MAX_PREFERRED_LENGTH,
            )
            for user in users:
                user_list.append(user["name"])
        return user_list
    except win32net.error:
        pass


def rename(name, new_name):
    """
    Change the username for a named user

    Args:

        name (str): The username to change

        new_name (str): The new name for the current user

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

    CLI Example:

    .. code-block:: bash

        salt '*' user.rename jsnuffy jshmoe
    """
    # Load information for the current name
    current_info = info(name)
    if not current_info:
        raise CommandExecutionError(f"User '{name}' does not exist")

    # Look for an existing user with the new name
    new_info = info(new_name)
    if new_info:
        raise CommandExecutionError(f"User '{new_name}' already exists")

    # Rename the user account
    # Connect to WMI
    with salt.utils.winapi.Com():
        c = wmi.WMI(find_classes=0)

        # Get the user object
        try:
            user = c.Win32_UserAccount(Name=name)[0]
        except IndexError:
            raise CommandExecutionError(f"User '{name}' does not exist")

        # Rename the user
        result = user.Rename(new_name)[0]

        # Check the result (0 means success)
        if not result == 0:
            # Define Error Dict
            error_dict = {
                0: "Success",
                1: "Instance not found",
                2: "Instance required",
                3: "Invalid parameter",
                4: "User not found",
                5: "Domain not found",
                6: (
                    "Operation is allowed only on the primary domain controller of the"
                    " domain"
                ),
                7: "Operation is not allowed on the last administrative account",
                8: (
                    "Operation is not allowed on specified special groups: user, admin,"
                    " local, or guest"
                ),
                9: "Other API error",
                10: "Internal error",
            }
            raise CommandExecutionError(
                "There was an error renaming '{}' to '{}'. Error: {}".format(
                    name, new_name, error_dict[result]
                )
            )

    return info(new_name).get("name") == new_name


def current(sam=False):
    """
    Get the username that salt-minion is running under. If salt-minion is
    running as a service it should return the Local System account. If salt is
    running from a command prompt it should return the username that started the
    command prompt.

    .. versionadded:: 2015.5.6

    Args:

        sam (:obj:`bool`, optional):
            ``False`` returns just the username without any domain notation.
            ``True`` returns the domain with the username in the SAM format. Ie:
            ``domain\\username``.

    Returns:
        str: Returns username

    CLI Example:

    .. code-block:: bash

        salt '*' user.current
    """
    try:
        if sam:
            user_name = win32api.GetUserNameEx(win32con.NameSamCompatible)
        else:
            user_name = win32api.GetUserName()
    except pywintypes.error as exc:
        log.error("Failed to get current user")
        log.error("nbr: %s", exc.winerror)
        log.error("ctx: %s", exc.funcname)
        log.error("msg: %s", exc.strerror)
        raise CommandExecutionError("Failed to get current user", info=exc)

    if not user_name:
        raise CommandExecutionError("Failed to get current user")

    return user_name
