"""
Manage users with the pw command

.. 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>`.
"""

# Notes:
# ------
#
# Format of the master.passwd file:
#
# - name      User's login name.
# - password  User's encrypted password.
# - uid       User's id.
# - gid       User's login group id.
# - class     User's login class.
# - change    Password change time.
# - expire    Account expiration time.
# - gecos     General information about the user.
# - home_dir  User's home directory.
# - shell     User's login shell.
#
# The usershow command allows viewing of an account in a format that is
# identical to the format used in /etc/master.passwd (with the password field
# replaced with a ‘*’.)
#
# Example:
# % pw usershow -n someuser
# someuser:*:1001:1001::0:0:SomeUser Name:/home/someuser:/bin/sh


import copy
import logging

import salt.utils.args
import salt.utils.data
import salt.utils.user
from salt.exceptions import CommandExecutionError

try:
    import pwd

    HAS_PWD = True
except ImportError:
    HAS_PWD = False


log = logging.getLogger(__name__)

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


def __virtual__():
    """
    Set the user module if the kernel is FreeBSD or DragonFly
    """
    if HAS_PWD and __grains__["kernel"] in ("FreeBSD", "DragonFly"):
        return __virtualname__
    return (
        False,
        "The pw_user execution module cannot be loaded: the pwd python module is not"
        " available or the system is not FreeBSD.",
    )


def _get_gecos(name):
    """
    Retrieve GECOS field info and return it in dictionary form
    """
    try:
        gecos_field = pwd.getpwnam(name).pw_gecos.split(",", 3)
    except KeyError:
        raise CommandExecutionError(f"User '{name}' does not exist")
    if not gecos_field:
        return {}
    else:
        # Assign empty strings for any unspecified trailing GECOS fields
        while len(gecos_field) < 4:
            gecos_field.append("")
        return {
            "fullname": salt.utils.data.decode(gecos_field[0]),
            "roomnumber": salt.utils.data.decode(gecos_field[1]),
            "workphone": salt.utils.data.decode(gecos_field[2]),
            "homephone": salt.utils.data.decode(gecos_field[3]),
        }


def _build_gecos(gecos_dict):
    """
    Accepts a dictionary entry containing GECOS field names and their values,
    and returns a full GECOS comment string, to be used with pw usermod.
    """
    return "{},{},{},{}".format(
        gecos_dict.get("fullname", ""),
        gecos_dict.get("roomnumber", ""),
        gecos_dict.get("workphone", ""),
        gecos_dict.get("homephone", ""),
    )


def _update_gecos(name, key, value):
    """
    Common code to change a user's GECOS information
    """
    if not isinstance(value, str):
        value = str(value)
    pre_info = _get_gecos(name)
    if not pre_info:
        return False
    if value == pre_info[key]:
        return True
    gecos_data = copy.deepcopy(pre_info)
    gecos_data[key] = value
    cmd = ["pw", "usermod", name, "-c", _build_gecos(gecos_data)]
    __salt__["cmd.run"](cmd, python_shell=False)
    post_info = info(name)
    return _get_gecos(name).get(key) == value


def add(
    name,
    uid=None,
    gid=None,
    groups=None,
    home=None,
    shell=None,
    unique=True,
    fullname="",
    roomnumber="",
    workphone="",
    homephone="",
    createhome=True,
    loginclass=None,
    **kwargs,
):
    """
    Add a user to the minion

    CLI Example:

    .. code-block:: bash

        salt '*' user.add name <uid> <gid> <groups> <home> <shell>
    """
    kwargs = salt.utils.args.clean_kwargs(**kwargs)
    if salt.utils.data.is_true(kwargs.pop("system", False)):
        log.warning("pw_user module does not support the 'system' argument")
    if kwargs:
        log.warning("Invalid kwargs passed to user.add")

    if isinstance(groups, str):
        groups = groups.split(",")
    cmd = ["pw", "useradd"]
    if uid:
        cmd.extend(["-u", uid])
    if gid:
        cmd.extend(["-g", gid])
    if groups:
        cmd.extend(["-G", ",".join(groups)])
    if home is not None:
        cmd.extend(["-d", home])
    if createhome is True:
        cmd.append("-m")
    if loginclass:
        cmd.extend(["-L", loginclass])
    if shell:
        cmd.extend(["-s", shell])
    if not salt.utils.data.is_true(unique):
        cmd.append("-o")
    gecos_field = _build_gecos(
        {
            "fullname": fullname,
            "roomnumber": roomnumber,
            "workphone": workphone,
            "homephone": homephone,
        }
    )
    cmd.extend(["-c", gecos_field])
    cmd.extend(["-n", name])
    return __salt__["cmd.retcode"](cmd, python_shell=False) == 0


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

    CLI Example:

    .. code-block:: bash

        salt '*' user.delete name remove=True force=True
    """
    if salt.utils.data.is_true(force):
        log.error(
            "pw userdel does not support force-deleting user while user is logged in"
        )
    cmd = ["pw", "userdel"]
    if remove:
        cmd.append("-r")
    cmd.extend(["-n", name])
    return __salt__["cmd.retcode"](cmd, python_shell=False) == 0


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

    CLI Example:

    .. code-block:: bash

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

    ret = []
    for data in pwd.getpwall():
        ret.append(info(data.pw_name))
    __context__["user.getent"] = ret
    return ret


def chuid(name, uid):
    """
    Change the uid for a named user

    CLI Example:

    .. code-block:: bash

        salt '*' user.chuid foo 4376
    """
    pre_info = info(name)
    if not pre_info:
        raise CommandExecutionError(f"User '{name}' does not exist")
    if uid == pre_info["uid"]:
        return True
    cmd = ["pw", "usermod", "-u", uid, "-n", name]
    __salt__["cmd.run"](cmd, python_shell=False)
    return info(name).get("uid") == uid


def chgid(name, gid):
    """
    Change the default group of the user

    CLI Example:

    .. code-block:: bash

        salt '*' user.chgid foo 4376
    """
    pre_info = info(name)
    if not pre_info:
        raise CommandExecutionError(f"User '{name}' does not exist")
    if gid == pre_info["gid"]:
        return True
    cmd = ["pw", "usermod", "-g", gid, "-n", name]
    __salt__["cmd.run"](cmd, python_shell=False)
    return info(name).get("gid") == gid


def chshell(name, shell):
    """
    Change the default shell of the user

    CLI Example:

    .. code-block:: bash

        salt '*' user.chshell foo /bin/zsh
    """
    pre_info = info(name)
    if not pre_info:
        raise CommandExecutionError(f"User '{name}' does not exist")
    if shell == pre_info["shell"]:
        return True
    cmd = ["pw", "usermod", "-s", shell, "-n", name]
    __salt__["cmd.run"](cmd, python_shell=False)
    return info(name).get("shell") == shell


def chhome(name, home, persist=False):
    """
    Set a new home directory for an existing user

    name
        Username to modify

    home
        New home directory to set

    persist : False
        Set to ``True`` to prevent configuration files in the new home
        directory from being overwritten by the files from the skeleton
        directory.

    CLI Example:

    .. code-block:: bash

        salt '*' user.chhome foo /home/users/foo True
    """
    pre_info = info(name)
    if not pre_info:
        raise CommandExecutionError(f"User '{name}' does not exist")
    if home == pre_info["home"]:
        return True
    cmd = ["pw", "usermod", name, "-d", home]
    if persist:
        cmd.append("-m")
    __salt__["cmd.run"](cmd, python_shell=False)
    return info(name).get("home") == home


def chgroups(name, groups, append=False):
    """
    Change the groups to which a user belongs

    name
        Username to modify

    groups
        List of groups to set for the user. Can be passed as a comma-separated
        list or a Python list.

    append : False
        Set to ``True`` to append these groups to the user's existing list of
        groups. Otherwise, the specified groups will replace any existing
        groups for the user.

    CLI Example:

    .. code-block:: bash

        salt '*' user.chgroups foo wheel,root True
    """
    if isinstance(groups, str):
        groups = groups.split(",")
    ugrps = set(list_groups(name))
    if ugrps == set(groups):
        return True
    if append:
        groups += ugrps
    cmd = ["pw", "usermod", "-G", ",".join(groups), "-n", name]
    return __salt__["cmd.retcode"](cmd, python_shell=False) == 0


def chfullname(name, fullname):
    """
    Change the user's Full Name

    CLI Example:

    .. code-block:: bash

        salt '*' user.chfullname foo "Foo Bar"
    """
    return _update_gecos(name, "fullname", fullname)


def chroomnumber(name, roomnumber):
    """
    Change the user's Room Number

    CLI Example:

    .. code-block:: bash

        salt '*' user.chroomnumber foo 123
    """
    return _update_gecos(name, "roomnumber", roomnumber)


def chworkphone(name, workphone):
    """
    Change the user's Work Phone

    CLI Example:

    .. code-block:: bash

        salt '*' user.chworkphone foo "7735550123"
    """
    return _update_gecos(name, "workphone", workphone)


def chhomephone(name, homephone):
    """
    Change the user's Home Phone

    CLI Example:

    .. code-block:: bash

        salt '*' user.chhomephone foo "7735551234"
    """
    return _update_gecos(name, "homephone", homephone)


def chloginclass(name, loginclass, root=None):
    """
    Change the default login class of the user

    .. versionadded:: 2016.3.5

    CLI Example:

    .. code-block:: bash

        salt '*' user.chloginclass foo staff
    """
    if loginclass == get_loginclass(name):
        return True

    cmd = ["pw", "usermod", "-L", f"{loginclass}", "-n", f"{name}"]

    __salt__["cmd.run"](cmd, python_shell=False)

    return get_loginclass(name) == loginclass


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

    CLI Example:

    .. code-block:: bash

        salt '*' user.info root
    """
    ret = {}
    try:
        data = pwd.getpwnam(name)
        ret["gid"] = data.pw_gid
        ret["groups"] = list_groups(name)
        ret["home"] = data.pw_dir
        ret["name"] = data.pw_name
        ret["passwd"] = data.pw_passwd
        ret["shell"] = data.pw_shell
        ret["uid"] = data.pw_uid
        # Put GECOS info into a list
        gecos_field = data.pw_gecos.split(",", 3)
        # Assign empty strings for any unspecified GECOS fields
        while len(gecos_field) < 4:
            gecos_field.append("")
        ret["fullname"] = gecos_field[0]
        ret["roomnumber"] = gecos_field[1]
        ret["workphone"] = gecos_field[2]
        ret["homephone"] = gecos_field[3]
    except KeyError:
        return {}
    return ret


def get_loginclass(name):
    """
    Get the login class of the user

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' user.get_loginclass foo

    """

    userinfo = __salt__["cmd.run_stdout"](["pw", "usershow", "-n", name])
    userinfo = userinfo.split(":")

    return userinfo[4] if len(userinfo) == 10 else ""


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

    CLI Example:

    .. code-block:: bash

        salt '*' user.list_groups foo
    """
    return salt.utils.user.get_group_list(name)


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

    CLI Example:

    .. code-block:: bash

        salt '*' user.list_users
    """
    return sorted(user.pw_name for user in pwd.getpwall())


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

    CLI Example:

    .. code-block:: bash

        salt '*' user.rename name new_name
    """
    current_info = info(name)
    if not current_info:
        raise CommandExecutionError(f"User '{name}' does not exist")
    new_info = info(new_name)
    if new_info:
        raise CommandExecutionError(f"User '{new_name}' already exists")
    cmd = ["pw", "usermod", "-l", new_name, "-n", name]
    __salt__["cmd.run"](cmd)
    post_info = info(new_name)
    if post_info["name"] != current_info["name"]:
        return post_info["name"] == new_name
    return False
