"""
Manage the password database on Solaris systems

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

import collections
import os

import salt.utils.files
import salt.utils.stringutils
from salt.exceptions import CommandExecutionError

try:
    import pwd
except ImportError:
    pass  # We're most likely on a Windows machine.


try:
    import salt.utils.pycrypto

    HAS_CRYPT = True
except ImportError:
    HAS_CRYPT = False


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


# The stdlib ``spwd`` module was deprecated in Python 3.11 and removed in
# Python 3.13, so we can no longer rely on ``spwd.getspnam``/``spwd.struct_spwd``
# to read ``/etc/shadow``. Emulate the pieces we need by parsing ``/etc/shadow``
# directly and returning a namedtuple with the same attribute names.
struct_spwd = collections.namedtuple(
    "struct_spwd",
    [
        "sp_namp",
        "sp_pwdp",
        "sp_lstchg",
        "sp_min",
        "sp_max",
        "sp_warn",
        "sp_inact",
        "sp_expire",
        "sp_flag",
    ],
)


def __virtual__():
    """
    Only work on POSIX-like systems
    """
    if __grains__.get("kernel", "") == "SunOS":
        return __virtualname__
    return (
        False,
        "The solaris_shadow execution module failed to load: only available on Solaris"
        " systems.",
    )


def default_hash():
    """
    Returns the default hash used for unset passwords

    CLI Example:

    .. code-block:: bash

        salt '*' shadow.default_hash
    """
    return "!"


def _getspnam(name, root=None):
    """
    Read ``/etc/shadow`` and return an ``spwd.struct_spwd``-compatible
    record for ``name``. Replaces ``spwd.getspnam``, which was removed
    in Python 3.13.
    """
    root = "/" if not root else root
    passwd = os.path.join(root, "etc/shadow")
    with salt.utils.files.fopen(passwd) as fp_:
        for line in fp_:
            line = salt.utils.stringutils.to_unicode(line).rstrip("\n")
            comps = line.split(":")
            if comps[0] == name:
                # Generate a getspnam compatible output
                for i in range(2, 9):
                    if i < len(comps):
                        comps[i] = int(comps[i]) if comps[i] else -1
                    else:
                        comps.append(-1)
                return struct_spwd(*comps[:9])
    raise KeyError


def info(name, root=None):
    """
    Return information for the specified user

    name
        User to get the information for

    root
        Directory to chroot into

    CLI Example:

    .. code-block:: bash

        salt '*' shadow.info root
    """
    try:
        data = _getspnam(name, root=root)
        return {
            "name": data.sp_namp,
            "passwd": data.sp_pwdp,
            "lstchg": data.sp_lstchg,
            "min": data.sp_min,
            "max": data.sp_max,
            "warn": data.sp_warn,
            "inact": data.sp_inact,
            "expire": data.sp_expire,
        }
    except (KeyError, OSError):
        pass

    # /etc/shadow was not readable or the user was not found there.
    # Fall back to what we can learn from pwd + `passwd -s` (SmartOS path).
    ret = {
        "name": "",
        "passwd": "",
        "lstchg": "",
        "min": "",
        "max": "",
        "warn": "",
        "inact": "",
        "expire": "",
    }

    try:
        data = pwd.getpwnam(name)  # pylint: disable=used-before-assignment
        ret.update({"name": name})
    except KeyError:
        return ret

    # To compensate for lack of spwd module, read in password hash from /etc/shadow
    s_file = "/etc/shadow"
    if not os.path.isfile(s_file):
        return ret
    with salt.utils.files.fopen(s_file, "r") as ifile:
        for line in ifile:
            comps = line.strip().split(":")
            if comps[0] == name:
                ret.update({"passwd": comps[1]})

    # For SmartOS `passwd -s <username>` and the output format is:
    #   name status mm/dd/yy min max warn
    #
    # Fields:
    #  1. Name: username
    #  2. Status:
    #      - LK: locked
    #      - NL: no login
    #      - NP: No password
    #      - PS: Password
    #  3. Last password change
    #  4. Minimum age
    #  5. Maximum age
    #  6. Warning period

    output = __salt__["cmd.run_all"](f"passwd -s {name}", python_shell=False)
    if output["retcode"] != 0:
        return ret

    fields = output["stdout"].split()
    if len(fields) == 2:
        # For example:
        #   root      NL
        return ret
    # We have all fields:
    #   buildbot L 05/09/2013 0 99999 7
    ret.update(
        {
            "name": data.pw_name,
            "lstchg": fields[2],
            "min": int(fields[3]),
            "max": int(fields[4]),
            "warn": int(fields[5]),
            "inact": "",
            "expire": "",
        }
    )
    return ret


def set_maxdays(name, maxdays):
    """
    Set the maximum number of days during which a password is valid. See man
    passwd.

    CLI Example:

    .. code-block:: bash

        salt '*' shadow.set_maxdays username 90
    """
    pre_info = info(name)
    if maxdays == pre_info["max"]:
        return True
    cmd = f"passwd -x {maxdays} {name}"
    __salt__["cmd.run"](cmd, python_shell=False)
    post_info = info(name)
    if post_info["max"] != pre_info["max"]:
        return post_info["max"] == maxdays


def set_mindays(name, mindays):
    """
    Set the minimum number of days between password changes. See man passwd.

    CLI Example:

    .. code-block:: bash

        salt '*' shadow.set_mindays username 7
    """
    pre_info = info(name)
    if mindays == pre_info["min"]:
        return True
    cmd = f"passwd -n {mindays} {name}"
    __salt__["cmd.run"](cmd, python_shell=False)
    post_info = info(name)
    if post_info["min"] != pre_info["min"]:
        return post_info["min"] == mindays
    return False


def gen_password(password, crypt_salt=None, algorithm="sha512"):
    """
    .. versionadded:: 2015.8.8

    Generate hashed password

    .. note::

        When called this function is called directly via remote-execution,
        the password argument may be displayed in the system's process list.
        This may be a security risk on certain systems.

    password
        Plaintext password to be hashed.

    crypt_salt
        Crpytographic salt. If not given, a random 8-character salt will be
        generated.

    algorithm
        The following hash algorithms are supported:

        * md5
        * blowfish (not in mainline glibc, only available in distros that add it)
        * sha256
        * sha512 (default)

    CLI Example:

    .. code-block:: bash

        salt '*' shadow.gen_password 'I_am_password'
        salt '*' shadow.gen_password 'I_am_password' crypt_salt='I_am_salt' algorithm=sha256
    """
    if not HAS_CRYPT:
        raise CommandExecutionError(
            "gen_password is not available on this operating system "
            'because the "crypt" python module is not available.'
        )
    return salt.utils.pycrypto.gen_hash(crypt_salt, password, algorithm)


def del_password(name):
    """
    .. versionadded:: 2015.8.8

    Delete the password from name user

    CLI Example:

    .. code-block:: bash

        salt '*' shadow.del_password username
    """
    cmd = f"passwd -d {name}"
    __salt__["cmd.run"](cmd, python_shell=False, output_loglevel="quiet")
    uinfo = info(name)
    return not uinfo["passwd"]


def set_password(name, password):
    """
    Set the password for a named user. The password must be a properly defined
    hash, the password hash can be generated with this command:
    ``openssl passwd -1 <plaintext password>``

    CLI Example:

    .. code-block:: bash

        salt '*' shadow.set_password root $1$UYCIxa628.9qXjpQCjM4a..
    """
    s_file = "/etc/shadow"
    ret = {}
    if not os.path.isfile(s_file):
        return ret
    lines = []
    with salt.utils.files.fopen(s_file, "r") as ifile:
        for line in ifile:
            comps = line.strip().split(":")
            if comps[0] != name:
                lines.append(line)
                continue
            comps[1] = password
            line = ":".join(comps)
            lines.append(f"{line}\n")
    with salt.utils.files.fopen(s_file, "w+") as ofile:
        lines = [salt.utils.stringutils.to_str(_l) for _l in lines]
        ofile.writelines(lines)
    uinfo = info(name)
    return uinfo["passwd"] == password


def set_warndays(name, warndays):
    """
    Set the number of days of warning before a password change is required.
    See man passwd.

    CLI Example:

    .. code-block:: bash

        salt '*' shadow.set_warndays username 7
    """
    pre_info = info(name)
    if warndays == pre_info["warn"]:
        return True
    cmd = f"passwd -w {warndays} {name}"
    __salt__["cmd.run"](cmd, python_shell=False)
    post_info = info(name)
    if post_info["warn"] != pre_info["warn"]:
        return post_info["warn"] == warndays
    return False
