"""
Linux File Access Control Lists

The Linux ACL state module requires the `getfacl` and `setfacl` binaries.

Ensure a Linux ACL is present

.. code-block:: yaml

     root:
       acl.present:
         - name: /root
         - acl_type: user
         - acl_name: damian
         - perms: rwx

Ensure a Linux ACL does not exist

.. code-block:: yaml

     root:
       acl.absent:
         - name: /root
         - acl_type: user
         - acl_name: damian
         - perms: rwx

Ensure a Linux ACL list is present

.. code-block:: yaml

     root:
       acl.list_present:
         - name: /root
         - acl_type: user
         - acl_names:
           - damian
           - homer
         - perms: rwx

Ensure a Linux ACL list does not exist

.. code-block:: yaml

     root:
       acl.list_absent:
         - name: /root
         - acl_type: user
         - acl_names:
           - damian
           - homer
         - perms: rwx
"""

import logging
import os

import salt.utils.path
from salt.exceptions import CommandExecutionError

log = logging.getLogger(__name__)

__virtualname__ = "acl"


def __virtual__():
    """
    Ensure getfacl & setfacl exist
    """
    if salt.utils.path.which("getfacl") and salt.utils.path.which("setfacl"):
        return __virtualname__

    return (
        False,
        "The linux_acl state cannot be loaded: the getfacl or setfacl binary is not in"
        " the path.",
    )


def present(name, acl_type, acl_name="", perms="", recurse=False, force=False):
    """
    Ensure a Linux ACL is present

    name
        The acl path

    acl_type
        The type of the acl is used for it can be 'user' or 'group'

    acl_name
        The  user or group

    perms
        Set the permissions eg.: rwx

    recurse
        Set the permissions recursive in the path

    force
        Wipe out old permissions and ensure only the new permissions are set
    """
    ret = {"name": name, "result": True, "changes": {}, "comment": ""}

    _octal = {"r": 4, "w": 2, "x": 1, "-": 0}
    _octal_lookup = {0: "-", 1: "r", 2: "w", 4: "x"}

    if not os.path.exists(name):
        ret["comment"] = f"{name} does not exist"
        ret["result"] = False
        return ret

    __current_perms = __salt__["acl.getfacl"](name, recursive=recurse)

    if acl_type.startswith(("d:", "default:")):
        _acl_type = ":".join(acl_type.split(":")[1:])
        _current_perms = __current_perms[name].get("defaults", {})
        _default = True
    else:
        _acl_type = acl_type
        _current_perms = __current_perms[name]
        _default = False

    # The getfacl execution module lists default with empty names as being
    # applied to the user/group that owns the file, e.g.,
    # default:group::rwx would be listed as default:group:root:rwx
    # In this case, if acl_name is empty, we really want to search for root
    # but still uses '' for other

    # We search through the dictionary getfacl returns for the owner of the
    # file if acl_name is empty.
    if acl_name == "":
        _search_name = __current_perms[name].get("comment").get(_acl_type, "")
    else:
        _search_name = acl_name

    if _current_perms.get(_acl_type, None) or _default:
        try:
            user = [
                i
                for i in _current_perms[_acl_type]
                if next(iter(i.keys())) == _search_name
            ].pop()
        except (AttributeError, IndexError, StopIteration, KeyError):
            user = None

        if user:
            octal_sum = sum(_octal.get(i, i) for i in perms)
            need_refresh = False
            # If recursive check all paths retrieved via acl.getfacl
            if recurse:
                for path in __current_perms:
                    acl_found = False
                    if _default:
                        # Recusive default acls only apply to directories
                        if not os.path.isdir(path):
                            continue
                        _current_perms_path = __current_perms[path].get("defaults", {})
                    else:
                        _current_perms_path = __current_perms[path]
                    for user_acl in _current_perms_path.get(_acl_type, []):
                        if (
                            _search_name in user_acl
                            and user_acl[_search_name]["octal"] == octal_sum
                        ):
                            acl_found = True
                    if not acl_found:
                        need_refresh = True
                        break

            # Check the permissions from the already located file
            elif user[_search_name]["octal"] == sum(_octal.get(i, i) for i in perms):
                need_refresh = False
            # If they don't match then refresh
            else:
                need_refresh = True

            if not need_refresh:
                ret["comment"] = "Permissions are in the desired state"
            else:
                _num = user[_search_name]["octal"]
                new_perms = "{}{}{}".format(
                    _octal_lookup[_num & 1],
                    _octal_lookup[_num & 2],
                    _octal_lookup[_num & 4],
                )
                changes = {
                    "new": {"acl_name": acl_name, "acl_type": acl_type, "perms": perms},
                    "old": {
                        "acl_name": acl_name,
                        "acl_type": acl_type,
                        "perms": new_perms,
                    },
                }

                if __opts__["test"]:
                    ret.update(
                        {
                            "comment": (
                                "Updated permissions will be applied for "
                                "{}: {} -> {}".format(acl_name, new_perms, perms)
                            ),
                            "result": None,
                            "changes": changes,
                        }
                    )
                    return ret
                try:
                    if force:
                        __salt__["acl.wipefacls"](
                            name, recursive=recurse, raise_err=True
                        )

                    __salt__["acl.modfacl"](
                        acl_type,
                        acl_name,
                        perms,
                        name,
                        recursive=recurse,
                        raise_err=True,
                    )
                    ret.update(
                        {
                            "comment": f"Updated permissions for {acl_name}",
                            "result": True,
                            "changes": changes,
                        }
                    )
                except CommandExecutionError as exc:
                    ret.update(
                        {
                            "comment": "Error updating permissions for {}: {}".format(
                                acl_name, exc.strerror
                            ),
                            "result": False,
                        }
                    )
        else:
            changes = {
                "new": {"acl_name": acl_name, "acl_type": acl_type, "perms": perms}
            }

            if __opts__["test"]:
                ret.update(
                    {
                        "comment": "New permissions will be applied for {}: {}".format(
                            acl_name, perms
                        ),
                        "result": None,
                        "changes": changes,
                    }
                )
                ret["result"] = None
                return ret

            try:
                if force:
                    __salt__["acl.wipefacls"](name, recursive=recurse, raise_err=True)

                __salt__["acl.modfacl"](
                    acl_type, acl_name, perms, name, recursive=recurse, raise_err=True
                )
                ret.update(
                    {
                        "comment": f"Applied new permissions for {acl_name}",
                        "result": True,
                        "changes": changes,
                    }
                )
            except CommandExecutionError as exc:
                ret.update(
                    {
                        "comment": "Error updating permissions for {}: {}".format(
                            acl_name, exc.strerror
                        ),
                        "result": False,
                    }
                )

    else:
        ret["comment"] = "ACL Type does not exist"
        ret["result"] = False

    return ret


def absent(name, acl_type, acl_name="", perms="", recurse=False):
    """
    Ensure a Linux ACL does not exist

    name
        The acl path

    acl_type
        The type of the acl is used for, it can be 'user' or 'group'

    acl_name
        The user or group

    perms
        Remove the permissions eg.: rwx

    recurse
        Set the permissions recursive in the path
    """
    ret = {"name": name, "result": True, "changes": {}, "comment": ""}

    if not os.path.exists(name):
        ret["comment"] = f"{name} does not exist"
        ret["result"] = False
        return ret

    __current_perms = __salt__["acl.getfacl"](name, recursive=recurse)

    if acl_type.startswith(("d:", "default:")):
        _acl_type = ":".join(acl_type.split(":")[1:])
        _current_perms = __current_perms[name].get("defaults", {})
        _default = True
    else:
        _acl_type = acl_type
        _current_perms = __current_perms[name]
        _default = False

    # The getfacl execution module lists default with empty names as being
    # applied to the user/group that owns the file, e.g.,
    # default:group::rwx would be listed as default:group:root:rwx
    # In this case, if acl_name is empty, we really want to search for root
    # but still uses '' for other

    # We search through the dictionary getfacl returns for the owner of the
    # file if acl_name is empty.
    if acl_name == "":
        _search_name = __current_perms[name].get("comment").get(_acl_type, "")
    else:
        _search_name = acl_name

    if _current_perms.get(_acl_type, None) or _default:
        try:
            user = [
                i
                for i in _current_perms[_acl_type]
                if next(iter(i.keys())) == _search_name
            ].pop()
        except (AttributeError, IndexError, StopIteration, KeyError):
            user = None

        need_refresh = False
        for path in __current_perms:
            acl_found = False
            for user_acl in __current_perms[path].get(_acl_type, []):
                if _search_name in user_acl:
                    acl_found = True
                    break
            if acl_found:
                need_refresh = True
                break

        if user or need_refresh:
            ret["comment"] = "Removing permissions"

            if __opts__["test"]:
                ret["result"] = None
                return ret

            __salt__["acl.delfacl"](acl_type, acl_name, perms, name, recursive=recurse)
        else:
            ret["comment"] = "Permissions are in the desired state"

    else:
        ret["comment"] = "ACL Type does not exist"
        ret["result"] = False

    return ret


def list_present(name, acl_type, acl_names=None, perms="", recurse=False, force=False):
    """
    Ensure a Linux ACL list is present

    Takes a list of acl names and add them to the given path

    name
        The acl path

    acl_type
        The type of the acl is used for it can be 'user' or 'group'

    acl_names
        The list of users or groups

    perms
        Set the permissions eg.: rwx

    recurse
        Set the permissions recursive in the path

    force
        Wipe out old permissions and ensure only the new permissions are set
    """
    if acl_names is None:
        acl_names = []
    ret = {"name": name, "result": True, "changes": {}, "comment": ""}

    _octal = {"r": 4, "w": 2, "x": 1, "-": 0}
    _octal_perms = sum(_octal.get(i, i) for i in perms)
    if not os.path.exists(name):
        ret["comment"] = f"{name} does not exist"
        ret["result"] = False
        return ret

    __current_perms = __salt__["acl.getfacl"](name)

    if acl_type.startswith(("d:", "default:")):
        _acl_type = ":".join(acl_type.split(":")[1:])
        _current_perms = __current_perms[name].get("defaults", {})
        _default = True
    else:
        _acl_type = acl_type
        _current_perms = __current_perms[name]
        _default = False
        _origin_group = _current_perms.get("comment", {}).get("group", None)
        _origin_owner = _current_perms.get("comment", {}).get("owner", None)

        _current_acl_types = []
        diff_perms = False
        for key in _current_perms[acl_type]:
            for current_acl_name in key.keys():
                _current_acl_types.append(current_acl_name.encode("utf-8"))
                diff_perms = _octal_perms == key[current_acl_name]["octal"]
        if acl_type == "user":
            try:
                _current_acl_types.remove(_origin_owner)
            except ValueError:
                pass
        else:
            try:
                _current_acl_types.remove(_origin_group)
            except ValueError:
                pass
        diff_acls = set(_current_acl_types) ^ set(acl_names)
        if not diff_acls and diff_perms and not force:
            ret = {
                "name": name,
                "result": True,
                "changes": {},
                "comment": "Permissions and {}s are in the desired state".format(
                    acl_type
                ),
            }
            return ret
    # The getfacl execution module lists default with empty names as being
    # applied to the user/group that owns the file, e.g.,
    # default:group::rwx would be listed as default:group:root:rwx
    # In this case, if acl_names is empty, we really want to search for root
    # but still uses '' for other

    # We search through the dictionary getfacl returns for the owner of the
    # file if acl_names is empty.
    if acl_names == "":
        _search_names = __current_perms[name].get("comment").get(_acl_type, "")
    else:
        _search_names = acl_names

    if _current_perms.get(_acl_type, None) or _default:
        try:
            users = {}
            for i in _current_perms[_acl_type]:
                if i and next(iter(i.keys())) in _search_names:
                    users.update(i)
        except (AttributeError, KeyError):
            users = None

        if users:
            changes = {}
            for count, search_name in enumerate(_search_names):
                if search_name in users:
                    if users[search_name]["octal"] == sum(
                        _octal.get(i, i) for i in perms
                    ):
                        ret["comment"] = "Permissions are in the desired state"
                    else:
                        changes.update(
                            {
                                "new": {
                                    "acl_name": ", ".join(acl_names),
                                    "acl_type": acl_type,
                                    "perms": _octal_perms,
                                },
                                "old": {
                                    "acl_name": ", ".join(acl_names),
                                    "acl_type": acl_type,
                                    "perms": str(users[search_name]["octal"]),
                                },
                            }
                        )
                        if __opts__["test"]:
                            ret.update(
                                {
                                    "comment": (
                                        "Updated permissions will be applied for "
                                        "{}: {} -> {}".format(
                                            acl_names,
                                            str(users[search_name]["octal"]),
                                            perms,
                                        )
                                    ),
                                    "result": None,
                                    "changes": changes,
                                }
                            )
                            return ret
                        try:
                            if force:
                                __salt__["acl.wipefacls"](
                                    name, recursive=recurse, raise_err=True
                                )

                            for acl_name in acl_names:
                                __salt__["acl.modfacl"](
                                    acl_type,
                                    acl_name,
                                    perms,
                                    name,
                                    recursive=recurse,
                                    raise_err=True,
                                )
                            ret.update(
                                {
                                    "comment": "Updated permissions for {}".format(
                                        acl_names
                                    ),
                                    "result": True,
                                    "changes": changes,
                                }
                            )
                        except CommandExecutionError as exc:
                            ret.update(
                                {
                                    "comment": (
                                        "Error updating permissions for {}: {}".format(
                                            acl_names, exc.strerror
                                        )
                                    ),
                                    "result": False,
                                }
                            )
                else:
                    changes = {
                        "new": {
                            "acl_name": ", ".join(acl_names),
                            "acl_type": acl_type,
                            "perms": perms,
                        }
                    }

                    if __opts__["test"]:
                        ret.update(
                            {
                                "comment": (
                                    "New permissions will be applied for {}: {}".format(
                                        acl_names, perms
                                    )
                                ),
                                "result": None,
                                "changes": changes,
                            }
                        )
                        ret["result"] = None
                        return ret

                    try:
                        if force:
                            __salt__["acl.wipefacls"](
                                name, recursive=recurse, raise_err=True
                            )

                        for acl_name in acl_names:
                            __salt__["acl.modfacl"](
                                acl_type,
                                acl_name,
                                perms,
                                name,
                                recursive=recurse,
                                raise_err=True,
                            )
                        ret.update(
                            {
                                "comment": "Applied new permissions for {}".format(
                                    ", ".join(acl_names)
                                ),
                                "result": True,
                                "changes": changes,
                            }
                        )
                    except CommandExecutionError as exc:
                        ret.update(
                            {
                                "comment": (
                                    "Error updating permissions for {}: {}".format(
                                        acl_names, exc.strerror
                                    )
                                ),
                                "result": False,
                            }
                        )

        else:
            changes = {
                "new": {
                    "acl_name": ", ".join(acl_names),
                    "acl_type": acl_type,
                    "perms": perms,
                }
            }

            if __opts__["test"]:
                ret.update(
                    {
                        "comment": "New permissions will be applied for {}: {}".format(
                            acl_names, perms
                        ),
                        "result": None,
                        "changes": changes,
                    }
                )
                ret["result"] = None
                return ret

            try:
                if force:
                    __salt__["acl.wipefacls"](name, recursive=recurse, raise_err=True)

                for acl_name in acl_names:
                    __salt__["acl.modfacl"](
                        acl_type,
                        acl_name,
                        perms,
                        name,
                        recursive=recurse,
                        raise_err=True,
                    )
                ret.update(
                    {
                        "comment": "Applied new permissions for {}".format(
                            ", ".join(acl_names)
                        ),
                        "result": True,
                        "changes": changes,
                    }
                )
            except CommandExecutionError as exc:
                ret.update(
                    {
                        "comment": "Error updating permissions for {}: {}".format(
                            acl_names, exc.strerror
                        ),
                        "result": False,
                    }
                )

    else:
        ret["comment"] = "ACL Type does not exist"
        ret["result"] = False

    return ret


def list_absent(name, acl_type, acl_names=None, recurse=False):
    """
    Ensure a Linux ACL list does not exist

    Takes a list of acl names and remove them from the given path

    name
        The acl path

    acl_type
        The type of the acl is used for, it can be 'user' or 'group'

    acl_names
        The list of users or groups

    perms
        Remove the permissions eg.: rwx

    recurse
        Set the permissions recursive in the path

    """
    if acl_names is None:
        acl_names = []

    ret = {"name": name, "result": True, "changes": {}, "comment": ""}

    if not os.path.exists(name):
        ret["comment"] = f"{name} does not exist"
        ret["result"] = False
        return ret

    __current_perms = __salt__["acl.getfacl"](name)

    if acl_type.startswith(("d:", "default:")):
        _acl_type = ":".join(acl_type.split(":")[1:])
        _current_perms = __current_perms[name].get("defaults", {})
        _default = True
    else:
        _acl_type = acl_type
        _current_perms = __current_perms[name]
        _default = False
    # The getfacl execution module lists default with empty names as being
    # applied to the user/group that owns the file, e.g.,
    # default:group::rwx would be listed as default:group:root:rwx
    # In this case, if acl_names is empty, we really want to search for root
    # but still uses '' for other

    # We search through the dictionary getfacl returns for the owner of the
    # file if acl_names is empty.
    if not acl_names:
        _search_names = set(__current_perms[name].get("comment").get(_acl_type, ""))
    else:
        _search_names = set(acl_names)

    if _current_perms.get(_acl_type, None) or _default:
        try:
            users = {}
            for i in _current_perms[_acl_type]:
                if i and next(iter(i.keys())) in _search_names:
                    users.update(i)
        except (AttributeError, KeyError):
            users = None

        if users:
            ret["comment"] = "Removing permissions"

            if __opts__["test"]:
                ret["result"] = None
                return ret
            for acl_name in acl_names:
                __salt__["acl.delfacl"](acl_type, acl_name, name, recursive=recurse)
        else:
            ret["comment"] = "Permissions are in the desired state"

    else:
        ret["comment"] = "ACL Type does not exist"
        ret["result"] = False

    return ret
