"""
Manage RabbitMQ Users
=====================

Example:

.. code-block:: yaml

    rabbit_user:
      rabbitmq_user.present:
        - password: password
        - force: True
        - tags:
          - monitoring
          - user
        - perms:
          - '/':
            - '.*'
            - '.*'
            - '.*'
        - runas: rabbitmq
"""

import logging

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

log = logging.getLogger(__name__)


def __virtual__():
    """
    Only load if RabbitMQ is installed.
    """
    if salt.utils.path.which("rabbitmqctl"):
        return True
    return (False, "Command not found: rabbitmqctl")


def _check_perms_changes(name, newperms, runas=None, existing=None):
    """
    Check whether Rabbitmq user's permissions need to be changed.
    """
    if not newperms:
        return False

    if existing is None:
        try:
            existing = __salt__["rabbitmq.list_user_permissions"](name, runas=runas)
        except CommandExecutionError as err:
            log.error("Error: %s", err)
            return False

    empty_perms = {"configure": "", "write": "", "read": ""}
    perm_need_change = False
    for vhost_perms in newperms:
        for vhost, perms in vhost_perms.items():
            if vhost in existing:
                new_perms = {"configure": perms[0], "write": perms[1], "read": perms[2]}
                existing_vhost = existing[vhost]
                if new_perms != existing_vhost:
                    # This checks for setting permissions to nothing in the state,
                    # when previous state runs have already set permissions to
                    # nothing. We don't want to report a change in this case.
                    if existing_vhost == empty_perms and perms == empty_perms:
                        continue
                    perm_need_change = True
            else:
                perm_need_change = True

    return perm_need_change


def _get_current_tags(name, runas=None):
    """
    Whether Rabbitmq user's tags need to be changed
    """
    try:
        return list(__salt__["rabbitmq.list_users"](runas=runas)[name])
    except CommandExecutionError as err:
        log.error("Error: %s", err)
        return []


def present(name, password=None, force=False, tags=None, perms=(), runas=None):
    """
    Ensure the RabbitMQ user exists.

    name
        User name
    password
        The user's password
    force
        If force is ``True``, the password will be automatically updated without extra password change check.
    tags
        Optional list of tags for the user
    perms
        A list of dicts with vhost keys and 3-tuple values
    runas
        Name of the user to run the command
    """
    ret = {"name": name, "result": False, "comment": "", "changes": {}}

    try:
        user = __salt__["rabbitmq.user_exists"](name, runas=runas)
    except CommandExecutionError as err:
        ret["comment"] = f"Error: {err}"
        return ret

    passwd_reqs_update = False
    if user and password is not None:
        try:
            if not __salt__["rabbitmq.check_password"](name, password, runas=runas):
                passwd_reqs_update = True
                log.debug("RabbitMQ user %s password update required", name)
        except CommandExecutionError as err:
            ret["comment"] = f"Error: {err}"
            return ret

    if user and not any((force, perms, tags, passwd_reqs_update)):
        log.debug(
            "RabbitMQ user '%s' exists, password is up to date and force is not set.",
            name,
        )
        ret["comment"] = f"User '{name}' is already present."
        ret["result"] = True
        return ret

    if not user:
        ret["changes"].update({"user": {"old": "", "new": name}})
        if __opts__["test"]:
            ret["result"] = None
            ret["comment"] = f"User '{name}' is set to be created."
            return ret

        log.debug("RabbitMQ user '%s' doesn't exist - Creating.", name)
        try:
            __salt__["rabbitmq.add_user"](name, password, runas=runas)
        except CommandExecutionError as err:
            ret["comment"] = f"Error: {err}"
            return ret
    else:
        log.debug("RabbitMQ user '%s' exists", name)
        if force or passwd_reqs_update:
            if password is not None:
                if not __opts__["test"]:
                    try:
                        __salt__["rabbitmq.change_password"](
                            name, password, runas=runas
                        )
                    except CommandExecutionError as err:
                        ret["comment"] = f"Error: {err}"
                        return ret
                ret["changes"].update({"password": {"old": "", "new": "Set password."}})
            else:
                if not __opts__["test"]:
                    log.debug("Password for %s is not set - Clearing password.", name)
                    try:
                        __salt__["rabbitmq.clear_password"](name, runas=runas)
                    except CommandExecutionError as err:
                        ret["comment"] = f"Error: {err}"
                        return ret
                ret["changes"].update(
                    {"password": {"old": "Removed password.", "new": ""}}
                )

    if tags is not None:
        current_tags = _get_current_tags(name, runas=runas)
        if isinstance(tags, str):
            tags = tags.split()
        # Diff the tags sets. Symmetric difference operator ^ will give us
        # any element in one set, but not both
        if set(tags) ^ set(current_tags):
            if not __opts__["test"]:
                try:
                    __salt__["rabbitmq.set_user_tags"](name, tags, runas=runas)
                except CommandExecutionError as err:
                    ret["comment"] = f"Error: {err}"
                    return ret
            ret["changes"].update({"tags": {"old": current_tags, "new": tags}})
    try:
        existing_perms = __salt__["rabbitmq.list_user_permissions"](name, runas=runas)
    except CommandExecutionError as err:
        ret["comment"] = f"Error: {err}"
        return ret

    if _check_perms_changes(name, perms, runas=runas, existing=existing_perms):
        for vhost_perm in perms:
            for vhost, perm in vhost_perm.items():
                if not __opts__["test"]:
                    try:
                        __salt__["rabbitmq.set_permissions"](
                            vhost, name, perm[0], perm[1], perm[2], runas=runas
                        )
                    except CommandExecutionError as err:
                        ret["comment"] = f"Error: {err}"
                        return ret
                new_perms = {
                    vhost: {"configure": perm[0], "write": perm[1], "read": perm[2]}
                }
                if vhost in existing_perms:
                    if existing_perms[vhost] != new_perms[vhost]:
                        if ret["changes"].get("perms") is None:
                            ret["changes"].update({"perms": {"old": {}, "new": {}}})
                        ret["changes"]["perms"]["old"].update(existing_perms[vhost])
                        ret["changes"]["perms"]["new"].update(new_perms)
                else:
                    ret["changes"].update({"perms": {"new": {}}})
                    ret["changes"]["perms"]["new"].update(new_perms)

    ret["result"] = True
    if ret["changes"] == {}:
        ret["comment"] = f"'{name}' is already in the desired state."
        return ret

    if __opts__["test"]:
        ret["result"] = None
        ret["comment"] = f"Configuration for '{name}' will change."
        return ret

    ret["comment"] = f"'{name}' was configured."
    return ret


def absent(name, runas=None):
    """
    Ensure the named user is absent

    name
        The name of the user to remove
    runas
        User to run the command
    """
    ret = {"name": name, "result": False, "comment": "", "changes": {}}

    try:
        user_exists = __salt__["rabbitmq.user_exists"](name, runas=runas)
    except CommandExecutionError as err:
        ret["comment"] = f"Error: {err}"
        return ret

    if user_exists:
        if not __opts__["test"]:
            try:
                __salt__["rabbitmq.delete_user"](name, runas=runas)
            except CommandExecutionError as err:
                ret["comment"] = f"Error: {err}"
                return ret
        ret["changes"].update({"name": {"old": name, "new": ""}})
    else:
        ret["result"] = True
        ret["comment"] = f"The user '{name}' is not present."
        return ret

    if __opts__["test"] and ret["changes"]:
        ret["result"] = None
        ret["comment"] = f"The user '{name}' will be removed."
        return ret

    ret["result"] = True
    ret["comment"] = f"The user '{name}' was removed."
    return ret
