"""
Manage groups on Windows

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

import logging

import salt.utils.platform
import salt.utils.win_functions
import salt.utils.winapi

try:
    import pywintypes
    import win32api
    import win32com.client

    HAS_DEPENDENCIES = True
except ImportError:
    HAS_DEPENDENCIES = False

log = logging.getLogger(__name__)

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


def __virtual__():
    """
    Set the group module if the kernel is Windows
    """
    if not salt.utils.platform.is_windows():
        return False, "win_groupadd: only works on Windows systems"
    if not HAS_DEPENDENCIES:
        return False, "win_groupadd: missing dependencies"
    return __virtualname__


def _get_computer_object():
    """
    A helper function to get the object for the local machine

    Returns:
        object: Returns the computer object for the local machine
    """
    with salt.utils.winapi.Com():
        nt = win32com.client.Dispatch("AdsNameSpaces")
        return nt.GetObject("", "WinNT://.,computer")


def _get_group_object(name):
    """
    A helper function to get a specified group object

    Args:

        name (str): The name of the object

    Returns:
        object: The specified group object
    """
    with salt.utils.winapi.Com():
        nt = win32com.client.Dispatch("AdsNameSpaces")
        return nt.GetObject("", f"WinNT://./{name},group")


def _get_all_groups():
    """
    A helper function that gets a list of group objects for all groups on the
    machine

    Returns:
        iter: A list of objects for all groups on the machine
    """
    with salt.utils.winapi.Com():
        nt = win32com.client.Dispatch("AdsNameSpaces")
        results = nt.GetObject("", "WinNT://.")
        results.Filter = ["group"]
        return results


def _get_username(member):
    """
    Resolve the username from the member object returned from a group query

    Returns:
        str: The username converted to domain\\username format
    """
    return member.ADSPath.replace("WinNT://", "").replace("/", "\\")


def add(name, **kwargs):
    """
    Add the specified group

    Args:

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

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

    CLI Example:

    .. code-block:: bash

        salt '*' group.add foo
    """
    if not info(name):
        comp_obj = _get_computer_object()
        try:
            new_group = comp_obj.Create("group", name)
            new_group.SetInfo()
            log.info("Successfully created group %s", name)
        except pywintypes.com_error as exc:
            log.error(
                "Failed to create group %s. %s",
                name,
                win32api.FormatMessage(exc.excepinfo[5]),
            )
            return False
    else:
        log.warning("The group %s already exists.", name)
        return False
    return True


def delete(name, **kwargs):
    """
    Remove the named group

    Args:

        name (str):
            The name of the group to remove

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

    CLI Example:

    .. code-block:: bash

        salt '*' group.delete foo
    """
    if info(name):
        comp_obj = _get_computer_object()
        try:
            comp_obj.Delete("group", name)
            log.info("Successfully removed group %s", name)
        except pywintypes.com_error as exc:
            log.error(
                "Failed to remove group %s. %s",
                name,
                win32api.FormatMessage(exc.excepinfo[5]),
            )
            return False
    else:
        log.warning("The group %s does not exist.", name)
        return False

    return True


def info(name):
    """
    Return information about a group

    Args:

        name (str):
            The name of the group for which to get information

    Returns:
        dict: A dictionary of information about the group

    CLI Example:

    .. code-block:: bash

        salt '*' group.info foo
    """
    try:
        groupObj = _get_group_object(name)
        gr_name = groupObj.Name
        gr_mem = [_get_username(x) for x in groupObj.members()]
    except pywintypes.com_error as exc:
        log.debug(
            "Failed to access group %s. %s",
            name,
            win32api.FormatMessage(exc.excepinfo[5]),
        )
        return False

    if not gr_name:
        return False

    return {"name": gr_name, "passwd": None, "gid": None, "members": gr_mem}


def getent(refresh=False):
    """
    Return info on all groups

    Args:

        refresh (bool):
            Refresh the info for all groups in ``__context__``. If False only
            the groups in ``__context__`` will be returned. If True the
            ``__context__`` will be refreshed with current data and returned.
            Default is False

    Returns:
        A list of groups and their information

    CLI Example:

    .. code-block:: bash

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

    ret = []

    results = _get_all_groups()

    for result in results:
        group = {
            "gid": __salt__["file.group_to_gid"](result.Name),
            "members": [_get_username(x) for x in result.members()],
            "name": result.Name,
            "passwd": "x",
        }
        ret.append(group)
    __context__["group.getent"] = ret
    return ret


def adduser(name, username, **kwargs):
    """
    Add a user to a group

    Args:

        name (str):
            The name of the group to modify

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

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

    CLI Example:

    .. code-block:: bash

        salt '*' group.adduser foo username
    """
    try:
        group_obj = _get_group_object(name)
    except pywintypes.com_error as exc:
        log.error(
            "Failed to access group %s. %s",
            name,
            win32api.FormatMessage(exc.excepinfo[5]),
        )
        return False

    existing_members = [_get_username(x) for x in group_obj.members()]
    username = salt.utils.win_functions.get_sam_name(username)

    try:
        if username not in existing_members:
            group_obj.Add("WinNT://" + username.replace("\\", "/"))
            log.info("Added user %s", username)
        else:
            log.warning("User %s is already a member of %s", username, name)
            return False
    except pywintypes.com_error as exc:
        log.error("Failed to add %s to group %s. %s", username, name, exc.excepinfo[2])
        return False

    return True


def deluser(name, username, **kwargs):
    """
    Remove a user from a group

    Args:

        name (str):
            The name of the group to modify

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

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

    CLI Example:

    .. code-block:: bash

        salt '*' group.deluser foo username
    """
    try:
        group_obj = _get_group_object(name)
    except pywintypes.com_error as exc:
        log.error(
            "Failed to access group %s. %s",
            name,
            win32api.FormatMessage(exc.excepinfo[5]),
        )
        return False

    existing_members = [_get_username(x) for x in group_obj.members()]

    try:
        if salt.utils.win_functions.get_sam_name(username) in existing_members:
            group_obj.Remove("WinNT://" + username.replace("\\", "/"))
            log.info("Removed user %s", username)
        else:
            log.warning("User %s is not a member of %s", username, name)
            return False
    except pywintypes.com_error as exc:
        log.error(
            "Failed to remove %s from group %s. %s",
            username,
            name,
            win32api.FormatMessage(exc.excepinfo[5]),
        )
        return False

    return True


def members(name, members_list, **kwargs):
    """
    Ensure a group contains only the members in the list

    Args:

        name (str):
            The name of the group to modify

        members_list (str):
            A single user or a comma separated list of users. The group will
            contain only the users specified in this list.

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

    CLI Example:

    .. code-block:: bash

        salt '*' group.members foo 'user1,user2,user3'
    """
    members_list = [
        salt.utils.win_functions.get_sam_name(m) for m in members_list.split(",")
    ]
    if not isinstance(members_list, list):
        log.debug("member_list is not a list")
        return False

    try:
        obj_group = _get_group_object(name)
    except pywintypes.com_error as exc:
        # Group probably doesn't exist, but we'll log the error
        log.error(
            "Failed to access group %s. %s",
            name,
            win32api.FormatMessage(exc.excepinfo[5]),
        )
        return False

    existing_members = [_get_username(x) for x in obj_group.members()]
    existing_members.sort()
    members_list.sort()

    if existing_members == members_list:
        log.info("%s membership is correct", name)
        return True

    # add users
    success = True
    for member in members_list:
        if member not in existing_members:
            try:
                obj_group.Add("WinNT://" + member.replace("\\", "/"))
                log.info("User added: %s", member)
            except pywintypes.com_error as exc:
                log.error(
                    "Failed to add %s to %s. %s",
                    member,
                    name,
                    win32api.FormatMessage(exc.excepinfo[5]),
                )
                success = False

    # remove users not in members_list
    for member in existing_members:
        if member not in members_list:
            try:
                obj_group.Remove("WinNT://" + member.replace("\\", "/"))
                log.info("User removed: %s", member)
            except pywintypes.com_error as exc:
                log.error(
                    "Failed to remove %s from %s. %s",
                    member,
                    name,
                    win32api.FormatMessage(exc.excepinfo[5]),
                )
                success = False

    return success


def list_groups(refresh=False):
    """
    Return a list of groups

    Args:

        refresh (bool):
            Refresh the info for all groups in ``__context__``. If False only
            the groups in ``__context__`` will be returned. If True, the
            ``__context__`` will be refreshed with current data and returned.
            Default is False

    Returns:
        list: A list of groups on the machine

    CLI Example:

    .. code-block:: bash

        salt '*' group.list_groups
    """
    if "group.list_groups" in __context__ and not refresh:
        return __context__["group.list_groups"]

    results = _get_all_groups()

    ret = []

    for result in results:
        ret.append(result.Name)

    __context__["group.list_groups"] = ret

    return ret
